postgresdk 0.16.14 → 0.17.0

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.
@@ -0,0 +1,34 @@
1
+ import type { Model } from "./introspect";
2
+ import type { Config } from "./types";
3
+ export interface CacheData {
4
+ schemaHash: string;
5
+ lastRun: string;
6
+ filesGenerated: number;
7
+ config: {
8
+ outDir: string | {
9
+ server: string;
10
+ client: string;
11
+ };
12
+ schema: string;
13
+ };
14
+ }
15
+ /**
16
+ * Compute a deterministic hash of the schema and config
17
+ */
18
+ export declare function computeSchemaHash(model: Model, config: Config): string;
19
+ /**
20
+ * Get the cache directory path
21
+ */
22
+ export declare function getCacheDir(baseDir?: string): string;
23
+ /**
24
+ * Read cache data if it exists
25
+ */
26
+ export declare function readCache(baseDir?: string): Promise<CacheData | null>;
27
+ /**
28
+ * Write cache data
29
+ */
30
+ export declare function writeCache(data: CacheData, baseDir?: string): Promise<void>;
31
+ /**
32
+ * Append an entry to the history log
33
+ */
34
+ export declare function appendToHistory(entry: string, baseDir?: string): Promise<void>;
package/dist/cli.js CHANGED
@@ -471,7 +471,7 @@ var require_config = __commonJS(() => {
471
471
  });
472
472
 
473
473
  // src/utils.ts
474
- import { mkdir, writeFile } from "fs/promises";
474
+ import { mkdir, writeFile, readFile } from "fs/promises";
475
475
  import { dirname } from "path";
476
476
  async function writeFiles(files) {
477
477
  for (const f of files) {
@@ -701,7 +701,6 @@ function generateUnifiedContract(model, config, graph) {
701
701
  }
702
702
  const contract = {
703
703
  version: "2.0.0",
704
- generatedAt: new Date().toISOString(),
705
704
  description: "Unified API and SDK contract - your one-stop reference for all operations",
706
705
  sdk: {
707
706
  initialization: generateSDKInitExamples(),
@@ -1237,7 +1236,6 @@ function generateUnifiedContractMarkdown(contract) {
1237
1236
  lines.push(contract.description);
1238
1237
  lines.push("");
1239
1238
  lines.push(`**Version:** ${contract.version}`);
1240
- lines.push(`**Generated:** ${new Date(contract.generatedAt).toLocaleString()}`);
1241
1239
  lines.push("");
1242
1240
  lines.push("## SDK Setup");
1243
1241
  lines.push("");
@@ -1688,6 +1686,102 @@ var init_emit_sdk_contract = __esm(() => {
1688
1686
  init_emit_include_methods();
1689
1687
  });
1690
1688
 
1689
+ // src/cache.ts
1690
+ import { createHash } from "crypto";
1691
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, appendFile } from "fs/promises";
1692
+ import { existsSync } from "fs";
1693
+ import { join } from "path";
1694
+ function computeSchemaHash(model, config) {
1695
+ const payload = {
1696
+ schema: model.schema,
1697
+ tables: model.tables,
1698
+ enums: model.enums,
1699
+ config: {
1700
+ outDir: config.outDir,
1701
+ schema: config.schema,
1702
+ softDeleteColumn: config.softDeleteColumn,
1703
+ includeMethodsDepth: config.includeMethodsDepth,
1704
+ serverFramework: config.serverFramework,
1705
+ useJsExtensions: config.useJsExtensions,
1706
+ useJsExtensionsClient: config.useJsExtensionsClient,
1707
+ numericMode: config.numericMode,
1708
+ skipJunctionTables: config.skipJunctionTables,
1709
+ apiPathPrefix: config.apiPathPrefix,
1710
+ auth: config.auth,
1711
+ tests: config.tests
1712
+ }
1713
+ };
1714
+ const json = JSON.stringify(payload, Object.keys(payload).sort());
1715
+ return createHash("sha256").update(json).digest("hex");
1716
+ }
1717
+ function getCacheDir(baseDir = process.cwd()) {
1718
+ return join(baseDir, ".postgresdk");
1719
+ }
1720
+ async function ensureGitignore(baseDir = process.cwd()) {
1721
+ const gitignorePath = join(baseDir, ".gitignore");
1722
+ if (!existsSync(gitignorePath)) {
1723
+ return;
1724
+ }
1725
+ try {
1726
+ const content = await readFile2(gitignorePath, "utf-8");
1727
+ if (content.includes(".postgresdk")) {
1728
+ return;
1729
+ }
1730
+ const entry = `
1731
+ # PostgreSDK cache and history
1732
+ .postgresdk/
1733
+ `;
1734
+ await appendFile(gitignorePath, entry);
1735
+ console.log("✓ Added .postgresdk/ to .gitignore");
1736
+ } catch {}
1737
+ }
1738
+ async function readCache(baseDir) {
1739
+ const cachePath = join(getCacheDir(baseDir), "cache.json");
1740
+ if (!existsSync(cachePath)) {
1741
+ return null;
1742
+ }
1743
+ try {
1744
+ const content = await readFile2(cachePath, "utf-8");
1745
+ return JSON.parse(content);
1746
+ } catch {
1747
+ return null;
1748
+ }
1749
+ }
1750
+ async function writeCache(data, baseDir) {
1751
+ const cacheDir = getCacheDir(baseDir);
1752
+ const isNewCache = !existsSync(cacheDir);
1753
+ await mkdir2(cacheDir, { recursive: true });
1754
+ if (isNewCache) {
1755
+ await ensureGitignore(baseDir);
1756
+ }
1757
+ const cachePath = join(cacheDir, "cache.json");
1758
+ await writeFile2(cachePath, JSON.stringify(data, null, 2), "utf-8");
1759
+ }
1760
+ async function appendToHistory(entry, baseDir) {
1761
+ const cacheDir = getCacheDir(baseDir);
1762
+ const isNewCache = !existsSync(cacheDir);
1763
+ await mkdir2(cacheDir, { recursive: true });
1764
+ if (isNewCache) {
1765
+ await ensureGitignore(baseDir);
1766
+ }
1767
+ const historyPath = join(cacheDir, "history.md");
1768
+ const timestamp = new Date().toISOString().replace("T", " ").substring(0, 19);
1769
+ const formattedEntry = `## ${timestamp} - ${entry}
1770
+
1771
+ `;
1772
+ try {
1773
+ const existing = existsSync(historyPath) ? await readFile2(historyPath, "utf-8") : `# PostgreSDK Generation History
1774
+
1775
+ `;
1776
+ await writeFile2(historyPath, existing + formattedEntry, "utf-8");
1777
+ } catch {
1778
+ await writeFile2(historyPath, `# PostgreSDK Generation History
1779
+
1780
+ ${formattedEntry}`, "utf-8");
1781
+ }
1782
+ }
1783
+ var init_cache = () => {};
1784
+
1691
1785
  // src/cli-config-utils.ts
1692
1786
  function extractConfigFields(configContent) {
1693
1787
  const fields = [];
@@ -2107,7 +2201,7 @@ var exports_cli_init = {};
2107
2201
  __export(exports_cli_init, {
2108
2202
  initCommand: () => initCommand
2109
2203
  });
2110
- import { existsSync as existsSync2, writeFileSync, readFileSync, copyFileSync } from "fs";
2204
+ import { existsSync as existsSync3, writeFileSync, readFileSync, copyFileSync } from "fs";
2111
2205
  import { resolve } from "path";
2112
2206
  import prompts from "prompts";
2113
2207
  async function initCommand(args) {
@@ -2117,7 +2211,7 @@ async function initCommand(args) {
2117
2211
  const isApiSide = args.includes("--api");
2118
2212
  const isSdkSide = args.includes("--sdk") || args[0] === "pull";
2119
2213
  const configPath = resolve(process.cwd(), "postgresdk.config.ts");
2120
- if (existsSync2(configPath)) {
2214
+ if (existsSync3(configPath)) {
2121
2215
  if (forceError) {
2122
2216
  console.error("❌ Error: postgresdk.config.ts already exists");
2123
2217
  console.log(" To reinitialize, please remove or rename the existing file first.");
@@ -2272,7 +2366,7 @@ async function initCommand(args) {
2272
2366
  }
2273
2367
  const template = projectType === "api" ? CONFIG_TEMPLATE_API : CONFIG_TEMPLATE_SDK;
2274
2368
  const envPath = resolve(process.cwd(), ".env");
2275
- const hasEnv = existsSync2(envPath);
2369
+ const hasEnv = existsSync3(envPath);
2276
2370
  try {
2277
2371
  writeFileSync(configPath, template, "utf-8");
2278
2372
  console.log("✅ Created postgresdk.config.ts");
@@ -2516,9 +2610,9 @@ var exports_cli_pull = {};
2516
2610
  __export(exports_cli_pull, {
2517
2611
  pullCommand: () => pullCommand
2518
2612
  });
2519
- import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
2520
- import { join as join2, dirname as dirname2, resolve as resolve2 } from "path";
2521
- import { existsSync as existsSync3 } from "fs";
2613
+ import { writeFile as writeFile3, mkdir as mkdir3, readFile as readFile3 } from "fs/promises";
2614
+ import { join as join3, dirname as dirname3, resolve as resolve2 } from "path";
2615
+ import { existsSync as existsSync4 } from "fs";
2522
2616
  import { pathToFileURL as pathToFileURL2 } from "url";
2523
2617
  async function pullCommand(args) {
2524
2618
  let configPath = "postgresdk.config.ts";
@@ -2528,7 +2622,7 @@ async function pullCommand(args) {
2528
2622
  }
2529
2623
  let fileConfig = {};
2530
2624
  const fullConfigPath = resolve2(process.cwd(), configPath);
2531
- if (existsSync3(fullConfigPath)) {
2625
+ if (existsSync4(fullConfigPath)) {
2532
2626
  console.log(`\uD83D\uDCCB Loading ${configPath}`);
2533
2627
  try {
2534
2628
  const configUrl = pathToFileURL2(fullConfigPath).href;
@@ -2586,7 +2680,6 @@ Options:`);
2586
2680
  }
2587
2681
  const manifest = await manifestRes.json();
2588
2682
  console.log(`\uD83D\uDCE6 SDK version: ${manifest.version}`);
2589
- console.log(`\uD83D\uDCC5 Generated: ${manifest.generated}`);
2590
2683
  console.log(`\uD83D\uDCC4 Files: ${manifest.files.length}`);
2591
2684
  const sdkRes = await fetch(`${config.from}/_psdk/sdk/download`, { headers });
2592
2685
  if (!sdkRes.ok) {
@@ -2600,31 +2693,78 @@ Options:`);
2600
2693
  throw new Error(`Failed to download SDK: ${errorMsg}`);
2601
2694
  }
2602
2695
  const sdk = await sdkRes.json();
2696
+ let filesWritten = 0;
2697
+ let filesUnchanged = 0;
2698
+ const changedFiles = [];
2603
2699
  for (const [path, content] of Object.entries(sdk.files)) {
2604
- const fullPath = join2(config.output, path);
2605
- await mkdir2(dirname2(fullPath), { recursive: true });
2606
- await writeFile2(fullPath, content, "utf-8");
2607
- console.log(` ✓ ${path}`);
2700
+ const fullPath = join3(config.output, path);
2701
+ await mkdir3(dirname3(fullPath), { recursive: true });
2702
+ let shouldWrite = true;
2703
+ if (existsSync4(fullPath)) {
2704
+ const existing = await readFile3(fullPath, "utf-8");
2705
+ if (existing === content) {
2706
+ shouldWrite = false;
2707
+ filesUnchanged++;
2708
+ }
2709
+ }
2710
+ if (shouldWrite) {
2711
+ await writeFile3(fullPath, content, "utf-8");
2712
+ filesWritten++;
2713
+ changedFiles.push(path);
2714
+ console.log(` ✓ ${path}`);
2715
+ }
2608
2716
  }
2609
- await writeFile2(join2(config.output, ".postgresdk.json"), JSON.stringify({
2717
+ const metadataPath = join3(config.output, ".postgresdk.json");
2718
+ const metadata = {
2610
2719
  version: sdk.version,
2611
- generated: sdk.generated,
2612
- pulledFrom: config.from,
2613
- pulledAt: new Date().toISOString()
2614
- }, null, 2));
2615
- console.log(`✅ SDK pulled successfully to ${config.output}`);
2720
+ pulledFrom: config.from
2721
+ };
2722
+ let metadataChanged = true;
2723
+ if (existsSync4(metadataPath)) {
2724
+ const existing = await readFile3(metadataPath, "utf-8");
2725
+ if (existing === JSON.stringify(metadata, null, 2)) {
2726
+ metadataChanged = false;
2727
+ }
2728
+ }
2729
+ if (metadataChanged) {
2730
+ await writeFile3(metadataPath, JSON.stringify(metadata, null, 2));
2731
+ }
2732
+ if (filesWritten === 0 && !metadataChanged) {
2733
+ console.log(`✅ SDK up-to-date (${filesUnchanged} files unchanged)`);
2734
+ await appendToHistory(`Pull
2735
+ ✅ SDK up-to-date
2736
+ - Pulled from: ${config.from}
2737
+ - Files checked: ${filesUnchanged}`);
2738
+ } else {
2739
+ console.log(`✅ SDK pulled successfully to ${config.output}`);
2740
+ console.log(` Updated: ${filesWritten} files, Unchanged: ${filesUnchanged} files`);
2741
+ let logEntry = `Pull
2742
+ ✅ Updated ${filesWritten} files from ${config.from}
2743
+ - SDK version: ${sdk.version}
2744
+ - Files unchanged: ${filesUnchanged}`;
2745
+ if (changedFiles.length > 0 && changedFiles.length <= 10) {
2746
+ logEntry += `
2747
+ - Modified: ${changedFiles.join(", ")}`;
2748
+ } else if (changedFiles.length > 10) {
2749
+ logEntry += `
2750
+ - Modified: ${changedFiles.slice(0, 10).join(", ")} and ${changedFiles.length - 10} more...`;
2751
+ }
2752
+ await appendToHistory(logEntry);
2753
+ }
2616
2754
  } catch (err) {
2617
2755
  console.error(`❌ Pull failed:`, err);
2618
2756
  process.exit(1);
2619
2757
  }
2620
2758
  }
2621
- var init_cli_pull = () => {};
2759
+ var init_cli_pull = __esm(() => {
2760
+ init_cache();
2761
+ });
2622
2762
 
2623
2763
  // src/index.ts
2624
2764
  var import_config = __toESM(require_config(), 1);
2625
- import { join, relative } from "node:path";
2765
+ import { join as join2, relative } from "node:path";
2626
2766
  import { pathToFileURL } from "node:url";
2627
- import { existsSync } from "node:fs";
2767
+ import { existsSync as existsSync2 } from "node:fs";
2628
2768
 
2629
2769
  // src/introspect.ts
2630
2770
  import { Client } from "pg";
@@ -4987,7 +5127,6 @@ ${pullToken ? `
4987
5127
  router.get("/_psdk/sdk/manifest", (c) => {
4988
5128
  return c.json({
4989
5129
  version: SDK_MANIFEST.version,
4990
- generated: SDK_MANIFEST.generated,
4991
5130
  files: Object.keys(SDK_MANIFEST.files)
4992
5131
  });
4993
5132
  });
@@ -5089,7 +5228,6 @@ function emitSdkBundle(clientFiles, clientDir) {
5089
5228
  }
5090
5229
  }
5091
5230
  const version = `1.0.0`;
5092
- const generated = new Date().toISOString();
5093
5231
  return `/**
5094
5232
  * AUTO-GENERATED FILE - DO NOT EDIT
5095
5233
  *
@@ -5101,7 +5239,6 @@ function emitSdkBundle(clientFiles, clientDir) {
5101
5239
 
5102
5240
  export const SDK_MANIFEST = {
5103
5241
  version: "${version}",
5104
- generated: "${generated}",
5105
5242
  files: ${JSON.stringify(files, null, 2)}
5106
5243
  };
5107
5244
  `;
@@ -6513,8 +6650,9 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
6513
6650
  // src/index.ts
6514
6651
  init_emit_sdk_contract();
6515
6652
  init_utils();
6653
+ init_cache();
6516
6654
  async function generate(configPath) {
6517
- if (!existsSync(configPath)) {
6655
+ if (!existsSync2(configPath)) {
6518
6656
  throw new Error(`Config file not found: ${configPath}
6519
6657
 
6520
6658
  ` + `Run 'postgresdk init' to create a config file, or specify a custom path with:
@@ -6527,43 +6665,58 @@ async function generate(configPath) {
6527
6665
  const cfg = { ...rawCfg, auth: normalizedAuth };
6528
6666
  console.log("\uD83D\uDD0D Introspecting database...");
6529
6667
  const model = await introspect(cfg.connectionString, cfg.schema || "public");
6668
+ const schemaHash = computeSchemaHash(model, cfg);
6669
+ const cache = await readCache();
6670
+ let serverDir;
6671
+ if (typeof cfg.outDir === "string") {
6672
+ serverDir = cfg.outDir;
6673
+ } else if (cfg.outDir && typeof cfg.outDir === "object") {
6674
+ serverDir = cfg.outDir.server;
6675
+ } else {
6676
+ serverDir = "./api/server";
6677
+ }
6678
+ const currentOutDir = typeof cfg.outDir === "string" ? cfg.outDir : `${serverDir}`;
6679
+ const cachedOutDir = typeof cache?.config.outDir === "string" ? cache.config.outDir : cache?.config.outDir?.server;
6680
+ if (cache && cache.schemaHash === schemaHash && existsSync2(serverDir) && currentOutDir === cachedOutDir) {
6681
+ console.log("✅ Schema unchanged, skipping generation");
6682
+ await appendToHistory(`Generate
6683
+ ✅ Schema unchanged, skipped generation
6684
+ - Schema hash: ${schemaHash.substring(0, 12)}...`);
6685
+ return;
6686
+ }
6530
6687
  console.log("\uD83D\uDD17 Building relationship graph...");
6531
6688
  const graph = buildGraph(model);
6532
- let serverDir;
6533
6689
  let originalClientDir;
6534
6690
  if (typeof cfg.outDir === "string") {
6535
- serverDir = cfg.outDir;
6536
6691
  originalClientDir = cfg.outDir;
6537
6692
  } else if (cfg.outDir && typeof cfg.outDir === "object") {
6538
- serverDir = cfg.outDir.server;
6539
6693
  originalClientDir = cfg.outDir.client;
6540
6694
  } else {
6541
- serverDir = "./api/server";
6542
6695
  originalClientDir = "./api/client";
6543
6696
  }
6544
6697
  const sameDirectory = serverDir === originalClientDir;
6545
6698
  let clientDir = originalClientDir;
6546
6699
  if (sameDirectory) {
6547
- clientDir = join(originalClientDir, "sdk");
6700
+ clientDir = join2(originalClientDir, "sdk");
6548
6701
  }
6549
6702
  const serverFramework = cfg.serverFramework || "hono";
6550
6703
  const generateTests = cfg.tests?.generate ?? false;
6551
6704
  const originalTestDir = cfg.tests?.output || "./api/tests";
6552
6705
  let testDir = originalTestDir;
6553
6706
  if (generateTests && (originalTestDir === serverDir || originalTestDir === originalClientDir)) {
6554
- testDir = join(originalTestDir, "tests");
6707
+ testDir = join2(originalTestDir, "tests");
6555
6708
  }
6556
6709
  const testFramework = cfg.tests?.framework || "vitest";
6557
6710
  console.log("\uD83D\uDCC1 Creating directories...");
6558
6711
  const dirs = [
6559
6712
  serverDir,
6560
- join(serverDir, "types"),
6561
- join(serverDir, "zod"),
6562
- join(serverDir, "routes"),
6713
+ join2(serverDir, "types"),
6714
+ join2(serverDir, "zod"),
6715
+ join2(serverDir, "routes"),
6563
6716
  clientDir,
6564
- join(clientDir, "types"),
6565
- join(clientDir, "zod"),
6566
- join(clientDir, "params")
6717
+ join2(clientDir, "types"),
6718
+ join2(clientDir, "zod"),
6719
+ join2(clientDir, "params")
6567
6720
  ];
6568
6721
  if (generateTests) {
6569
6722
  dirs.push(testDir);
@@ -6571,26 +6724,26 @@ async function generate(configPath) {
6571
6724
  await ensureDirs(dirs);
6572
6725
  const files = [];
6573
6726
  const includeSpec = emitIncludeSpec(graph);
6574
- files.push({ path: join(serverDir, "include-spec.ts"), content: includeSpec });
6575
- files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
6576
- files.push({ path: join(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
6577
- files.push({ path: join(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
6578
- files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
6579
- files.push({ path: join(clientDir, "where-types.ts"), content: emitWhereTypes() });
6727
+ files.push({ path: join2(serverDir, "include-spec.ts"), content: includeSpec });
6728
+ files.push({ path: join2(clientDir, "include-spec.ts"), content: includeSpec });
6729
+ files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
6730
+ files.push({ path: join2(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
6731
+ files.push({ path: join2(clientDir, "base-client.ts"), content: emitBaseClient() });
6732
+ files.push({ path: join2(clientDir, "where-types.ts"), content: emitWhereTypes() });
6580
6733
  files.push({
6581
- path: join(serverDir, "include-builder.ts"),
6734
+ path: join2(serverDir, "include-builder.ts"),
6582
6735
  content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
6583
6736
  });
6584
6737
  files.push({
6585
- path: join(serverDir, "include-loader.ts"),
6738
+ path: join2(serverDir, "include-loader.ts"),
6586
6739
  content: emitIncludeLoader(graph, model, cfg.includeMethodsDepth || 2, cfg.useJsExtensions)
6587
6740
  });
6588
- files.push({ path: join(serverDir, "logger.ts"), content: emitLogger() });
6741
+ files.push({ path: join2(serverDir, "logger.ts"), content: emitLogger() });
6589
6742
  if (getAuthStrategy(normalizedAuth) !== "none") {
6590
- files.push({ path: join(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
6743
+ files.push({ path: join2(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
6591
6744
  }
6592
6745
  files.push({
6593
- path: join(serverDir, "core", "operations.ts"),
6746
+ path: join2(serverDir, "core", "operations.ts"),
6594
6747
  content: emitCoreOperations()
6595
6748
  });
6596
6749
  if (process.env.SDK_DEBUG) {
@@ -6599,13 +6752,13 @@ async function generate(configPath) {
6599
6752
  for (const table of Object.values(model.tables)) {
6600
6753
  const numericMode = cfg.numericMode ?? "auto";
6601
6754
  const typesSrc = emitTypes(table, { numericMode }, model.enums);
6602
- files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
6603
- files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
6755
+ files.push({ path: join2(serverDir, "types", `${table.name}.ts`), content: typesSrc });
6756
+ files.push({ path: join2(clientDir, "types", `${table.name}.ts`), content: typesSrc });
6604
6757
  const zodSrc = emitZod(table, { numericMode }, model.enums);
6605
- files.push({ path: join(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
6606
- files.push({ path: join(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
6758
+ files.push({ path: join2(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
6759
+ files.push({ path: join2(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
6607
6760
  const paramsZodSrc = emitParamsZod(table, graph);
6608
- files.push({ path: join(clientDir, "params", `${table.name}.ts`), content: paramsZodSrc });
6761
+ files.push({ path: join2(clientDir, "params", `${table.name}.ts`), content: paramsZodSrc });
6609
6762
  let routeContent;
6610
6763
  if (serverFramework === "hono") {
6611
6764
  routeContent = emitHonoRoutes(table, graph, {
@@ -6619,11 +6772,11 @@ async function generate(configPath) {
6619
6772
  throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
6620
6773
  }
6621
6774
  files.push({
6622
- path: join(serverDir, "routes", `${table.name}.ts`),
6775
+ path: join2(serverDir, "routes", `${table.name}.ts`),
6623
6776
  content: routeContent
6624
6777
  });
6625
6778
  files.push({
6626
- path: join(clientDir, `${table.name}.ts`),
6779
+ path: join2(clientDir, `${table.name}.ts`),
6627
6780
  content: emitClient(table, graph, {
6628
6781
  useJsExtensions: cfg.useJsExtensionsClient,
6629
6782
  includeMethodsDepth: cfg.includeMethodsDepth ?? 2,
@@ -6632,12 +6785,12 @@ async function generate(configPath) {
6632
6785
  });
6633
6786
  }
6634
6787
  files.push({
6635
- path: join(clientDir, "index.ts"),
6788
+ path: join2(clientDir, "index.ts"),
6636
6789
  content: emitClientIndex(Object.values(model.tables), cfg.useJsExtensionsClient)
6637
6790
  });
6638
6791
  if (serverFramework === "hono") {
6639
6792
  files.push({
6640
- path: join(serverDir, "router.ts"),
6793
+ path: join2(serverDir, "router.ts"),
6641
6794
  content: emitHonoRouter(Object.values(model.tables), getAuthStrategy(normalizedAuth) !== "none", cfg.useJsExtensions, cfg.pullToken)
6642
6795
  });
6643
6796
  }
@@ -6647,59 +6800,72 @@ async function generate(configPath) {
6647
6800
  }
6648
6801
  const contract = generateUnifiedContract2(model, cfg, graph);
6649
6802
  files.push({
6650
- path: join(serverDir, "CONTRACT.md"),
6803
+ path: join2(serverDir, "CONTRACT.md"),
6651
6804
  content: generateUnifiedContractMarkdown2(contract)
6652
6805
  });
6653
6806
  files.push({
6654
- path: join(clientDir, "CONTRACT.md"),
6807
+ path: join2(clientDir, "CONTRACT.md"),
6655
6808
  content: generateUnifiedContractMarkdown2(contract)
6656
6809
  });
6657
6810
  const contractCode = emitUnifiedContract(model, cfg, graph);
6658
6811
  files.push({
6659
- path: join(serverDir, "contract.ts"),
6812
+ path: join2(serverDir, "contract.ts"),
6660
6813
  content: contractCode
6661
6814
  });
6662
6815
  const clientFiles = files.filter((f) => {
6663
6816
  return f.path.includes(clientDir);
6664
6817
  });
6665
6818
  files.push({
6666
- path: join(serverDir, "sdk-bundle.ts"),
6819
+ path: join2(serverDir, "sdk-bundle.ts"),
6667
6820
  content: emitSdkBundle(clientFiles, clientDir)
6668
6821
  });
6669
6822
  if (generateTests) {
6670
6823
  console.log("\uD83E\uDDEA Generating tests...");
6671
6824
  const relativeClientPath = relative(testDir, clientDir);
6672
6825
  files.push({
6673
- path: join(testDir, "setup.ts"),
6826
+ path: join2(testDir, "setup.ts"),
6674
6827
  content: emitTestSetup(relativeClientPath, testFramework)
6675
6828
  });
6676
6829
  files.push({
6677
- path: join(testDir, "docker-compose.yml"),
6830
+ path: join2(testDir, "docker-compose.yml"),
6678
6831
  content: emitDockerCompose()
6679
6832
  });
6680
6833
  files.push({
6681
- path: join(testDir, "run-tests.sh"),
6834
+ path: join2(testDir, "run-tests.sh"),
6682
6835
  content: emitTestScript(testFramework, testDir)
6683
6836
  });
6684
6837
  files.push({
6685
- path: join(testDir, ".gitignore"),
6838
+ path: join2(testDir, ".gitignore"),
6686
6839
  content: emitTestGitignore()
6687
6840
  });
6688
6841
  if (testFramework === "vitest") {
6689
6842
  files.push({
6690
- path: join(testDir, "vitest.config.ts"),
6843
+ path: join2(testDir, "vitest.config.ts"),
6691
6844
  content: emitVitestConfig()
6692
6845
  });
6693
6846
  }
6694
6847
  for (const table of Object.values(model.tables)) {
6695
6848
  files.push({
6696
- path: join(testDir, `${table.name}.test.ts`),
6849
+ path: join2(testDir, `${table.name}.test.ts`),
6697
6850
  content: emitTableTest(table, model, relativeClientPath, testFramework)
6698
6851
  });
6699
6852
  }
6700
6853
  }
6701
6854
  console.log("✍️ Writing files...");
6702
6855
  await writeFiles(files);
6856
+ const outDirStr = typeof cfg.outDir === "string" ? cfg.outDir : `${cfg.outDir?.server || "./api/server"}`;
6857
+ await writeCache({
6858
+ schemaHash,
6859
+ lastRun: new Date().toISOString(),
6860
+ filesGenerated: files.length,
6861
+ config: {
6862
+ outDir: cfg.outDir || "./api",
6863
+ schema: cfg.schema || "public"
6864
+ }
6865
+ });
6866
+ await appendToHistory(`Generate
6867
+ ✅ Generated ${files.length} files
6868
+ - Schema hash: ${schemaHash.substring(0, 12)}...`);
6703
6869
  console.log(`✅ Generated ${files.length} files`);
6704
6870
  console.log(` Server: ${serverDir}`);
6705
6871
  console.log(` Client: ${sameDirectory ? clientDir + " (in sdk subdir due to same output dir)" : clientDir}`);
@@ -6738,10 +6904,10 @@ var import_config2 = __toESM(require_config(), 1);
6738
6904
  import { resolve as resolve3 } from "node:path";
6739
6905
  import { readFileSync as readFileSync2 } from "node:fs";
6740
6906
  import { fileURLToPath } from "node:url";
6741
- import { dirname as dirname3, join as join3 } from "node:path";
6907
+ import { dirname as dirname4, join as join4 } from "node:path";
6742
6908
  var __filename2 = fileURLToPath(import.meta.url);
6743
- var __dirname2 = dirname3(__filename2);
6744
- var packageJson = JSON.parse(readFileSync2(join3(__dirname2, "../package.json"), "utf-8"));
6909
+ var __dirname2 = dirname4(__filename2);
6910
+ var packageJson = JSON.parse(readFileSync2(join4(__dirname2, "../package.json"), "utf-8"));
6745
6911
  var VERSION = packageJson.version;
6746
6912
  var args = process.argv.slice(2);
6747
6913
  var command = args[0];
@@ -3,7 +3,6 @@ import type { Config, AuthConfig } from "./types";
3
3
  import type { Graph } from "./rel-classify";
4
4
  export interface UnifiedContract {
5
5
  version: string;
6
- generatedAt: string;
7
6
  description: string;
8
7
  sdk: {
9
8
  initialization: SDKInitExample[];
package/dist/index.js CHANGED
@@ -470,7 +470,7 @@ var require_config = __commonJS(() => {
470
470
  });
471
471
 
472
472
  // src/utils.ts
473
- import { mkdir, writeFile } from "fs/promises";
473
+ import { mkdir, writeFile, readFile } from "fs/promises";
474
474
  import { dirname } from "path";
475
475
  async function writeFiles(files) {
476
476
  for (const f of files) {
@@ -700,7 +700,6 @@ function generateUnifiedContract(model, config, graph) {
700
700
  }
701
701
  const contract = {
702
702
  version: "2.0.0",
703
- generatedAt: new Date().toISOString(),
704
703
  description: "Unified API and SDK contract - your one-stop reference for all operations",
705
704
  sdk: {
706
705
  initialization: generateSDKInitExamples(),
@@ -1236,7 +1235,6 @@ function generateUnifiedContractMarkdown(contract) {
1236
1235
  lines.push(contract.description);
1237
1236
  lines.push("");
1238
1237
  lines.push(`**Version:** ${contract.version}`);
1239
- lines.push(`**Generated:** ${new Date(contract.generatedAt).toLocaleString()}`);
1240
1238
  lines.push("");
1241
1239
  lines.push("## SDK Setup");
1242
1240
  lines.push("");
@@ -1687,11 +1685,107 @@ var init_emit_sdk_contract = __esm(() => {
1687
1685
  init_emit_include_methods();
1688
1686
  });
1689
1687
 
1688
+ // src/cache.ts
1689
+ import { createHash } from "crypto";
1690
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, appendFile } from "fs/promises";
1691
+ import { existsSync } from "fs";
1692
+ import { join } from "path";
1693
+ function computeSchemaHash(model, config) {
1694
+ const payload = {
1695
+ schema: model.schema,
1696
+ tables: model.tables,
1697
+ enums: model.enums,
1698
+ config: {
1699
+ outDir: config.outDir,
1700
+ schema: config.schema,
1701
+ softDeleteColumn: config.softDeleteColumn,
1702
+ includeMethodsDepth: config.includeMethodsDepth,
1703
+ serverFramework: config.serverFramework,
1704
+ useJsExtensions: config.useJsExtensions,
1705
+ useJsExtensionsClient: config.useJsExtensionsClient,
1706
+ numericMode: config.numericMode,
1707
+ skipJunctionTables: config.skipJunctionTables,
1708
+ apiPathPrefix: config.apiPathPrefix,
1709
+ auth: config.auth,
1710
+ tests: config.tests
1711
+ }
1712
+ };
1713
+ const json = JSON.stringify(payload, Object.keys(payload).sort());
1714
+ return createHash("sha256").update(json).digest("hex");
1715
+ }
1716
+ function getCacheDir(baseDir = process.cwd()) {
1717
+ return join(baseDir, ".postgresdk");
1718
+ }
1719
+ async function ensureGitignore(baseDir = process.cwd()) {
1720
+ const gitignorePath = join(baseDir, ".gitignore");
1721
+ if (!existsSync(gitignorePath)) {
1722
+ return;
1723
+ }
1724
+ try {
1725
+ const content = await readFile2(gitignorePath, "utf-8");
1726
+ if (content.includes(".postgresdk")) {
1727
+ return;
1728
+ }
1729
+ const entry = `
1730
+ # PostgreSDK cache and history
1731
+ .postgresdk/
1732
+ `;
1733
+ await appendFile(gitignorePath, entry);
1734
+ console.log("✓ Added .postgresdk/ to .gitignore");
1735
+ } catch {}
1736
+ }
1737
+ async function readCache(baseDir) {
1738
+ const cachePath = join(getCacheDir(baseDir), "cache.json");
1739
+ if (!existsSync(cachePath)) {
1740
+ return null;
1741
+ }
1742
+ try {
1743
+ const content = await readFile2(cachePath, "utf-8");
1744
+ return JSON.parse(content);
1745
+ } catch {
1746
+ return null;
1747
+ }
1748
+ }
1749
+ async function writeCache(data, baseDir) {
1750
+ const cacheDir = getCacheDir(baseDir);
1751
+ const isNewCache = !existsSync(cacheDir);
1752
+ await mkdir2(cacheDir, { recursive: true });
1753
+ if (isNewCache) {
1754
+ await ensureGitignore(baseDir);
1755
+ }
1756
+ const cachePath = join(cacheDir, "cache.json");
1757
+ await writeFile2(cachePath, JSON.stringify(data, null, 2), "utf-8");
1758
+ }
1759
+ async function appendToHistory(entry, baseDir) {
1760
+ const cacheDir = getCacheDir(baseDir);
1761
+ const isNewCache = !existsSync(cacheDir);
1762
+ await mkdir2(cacheDir, { recursive: true });
1763
+ if (isNewCache) {
1764
+ await ensureGitignore(baseDir);
1765
+ }
1766
+ const historyPath = join(cacheDir, "history.md");
1767
+ const timestamp = new Date().toISOString().replace("T", " ").substring(0, 19);
1768
+ const formattedEntry = `## ${timestamp} - ${entry}
1769
+
1770
+ `;
1771
+ try {
1772
+ const existing = existsSync(historyPath) ? await readFile2(historyPath, "utf-8") : `# PostgreSDK Generation History
1773
+
1774
+ `;
1775
+ await writeFile2(historyPath, existing + formattedEntry, "utf-8");
1776
+ } catch {
1777
+ await writeFile2(historyPath, `# PostgreSDK Generation History
1778
+
1779
+ ${formattedEntry}`, "utf-8");
1780
+ }
1781
+ }
1782
+ var init_cache = () => {};
1783
+
1690
1784
  // src/index.ts
1691
1785
  var import_config = __toESM(require_config(), 1);
1692
- import { join, relative } from "node:path";
1786
+ import { join as join2, relative } from "node:path";
1693
1787
  import { pathToFileURL } from "node:url";
1694
- import { existsSync } from "node:fs";
1788
+ import { existsSync as existsSync2 } from "node:fs";
1695
1789
 
1696
1790
  // src/introspect.ts
1697
1791
  import { Client } from "pg";
@@ -4054,7 +4148,6 @@ ${pullToken ? `
4054
4148
  router.get("/_psdk/sdk/manifest", (c) => {
4055
4149
  return c.json({
4056
4150
  version: SDK_MANIFEST.version,
4057
- generated: SDK_MANIFEST.generated,
4058
4151
  files: Object.keys(SDK_MANIFEST.files)
4059
4152
  });
4060
4153
  });
@@ -4156,7 +4249,6 @@ function emitSdkBundle(clientFiles, clientDir) {
4156
4249
  }
4157
4250
  }
4158
4251
  const version = `1.0.0`;
4159
- const generated = new Date().toISOString();
4160
4252
  return `/**
4161
4253
  * AUTO-GENERATED FILE - DO NOT EDIT
4162
4254
  *
@@ -4168,7 +4260,6 @@ function emitSdkBundle(clientFiles, clientDir) {
4168
4260
 
4169
4261
  export const SDK_MANIFEST = {
4170
4262
  version: "${version}",
4171
- generated: "${generated}",
4172
4263
  files: ${JSON.stringify(files, null, 2)}
4173
4264
  };
4174
4265
  `;
@@ -5580,8 +5671,9 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
5580
5671
  // src/index.ts
5581
5672
  init_emit_sdk_contract();
5582
5673
  init_utils();
5674
+ init_cache();
5583
5675
  async function generate(configPath) {
5584
- if (!existsSync(configPath)) {
5676
+ if (!existsSync2(configPath)) {
5585
5677
  throw new Error(`Config file not found: ${configPath}
5586
5678
 
5587
5679
  ` + `Run 'postgresdk init' to create a config file, or specify a custom path with:
@@ -5594,43 +5686,58 @@ async function generate(configPath) {
5594
5686
  const cfg = { ...rawCfg, auth: normalizedAuth };
5595
5687
  console.log("\uD83D\uDD0D Introspecting database...");
5596
5688
  const model = await introspect(cfg.connectionString, cfg.schema || "public");
5689
+ const schemaHash = computeSchemaHash(model, cfg);
5690
+ const cache = await readCache();
5691
+ let serverDir;
5692
+ if (typeof cfg.outDir === "string") {
5693
+ serverDir = cfg.outDir;
5694
+ } else if (cfg.outDir && typeof cfg.outDir === "object") {
5695
+ serverDir = cfg.outDir.server;
5696
+ } else {
5697
+ serverDir = "./api/server";
5698
+ }
5699
+ const currentOutDir = typeof cfg.outDir === "string" ? cfg.outDir : `${serverDir}`;
5700
+ const cachedOutDir = typeof cache?.config.outDir === "string" ? cache.config.outDir : cache?.config.outDir?.server;
5701
+ if (cache && cache.schemaHash === schemaHash && existsSync2(serverDir) && currentOutDir === cachedOutDir) {
5702
+ console.log("✅ Schema unchanged, skipping generation");
5703
+ await appendToHistory(`Generate
5704
+ ✅ Schema unchanged, skipped generation
5705
+ - Schema hash: ${schemaHash.substring(0, 12)}...`);
5706
+ return;
5707
+ }
5597
5708
  console.log("\uD83D\uDD17 Building relationship graph...");
5598
5709
  const graph = buildGraph(model);
5599
- let serverDir;
5600
5710
  let originalClientDir;
5601
5711
  if (typeof cfg.outDir === "string") {
5602
- serverDir = cfg.outDir;
5603
5712
  originalClientDir = cfg.outDir;
5604
5713
  } else if (cfg.outDir && typeof cfg.outDir === "object") {
5605
- serverDir = cfg.outDir.server;
5606
5714
  originalClientDir = cfg.outDir.client;
5607
5715
  } else {
5608
- serverDir = "./api/server";
5609
5716
  originalClientDir = "./api/client";
5610
5717
  }
5611
5718
  const sameDirectory = serverDir === originalClientDir;
5612
5719
  let clientDir = originalClientDir;
5613
5720
  if (sameDirectory) {
5614
- clientDir = join(originalClientDir, "sdk");
5721
+ clientDir = join2(originalClientDir, "sdk");
5615
5722
  }
5616
5723
  const serverFramework = cfg.serverFramework || "hono";
5617
5724
  const generateTests = cfg.tests?.generate ?? false;
5618
5725
  const originalTestDir = cfg.tests?.output || "./api/tests";
5619
5726
  let testDir = originalTestDir;
5620
5727
  if (generateTests && (originalTestDir === serverDir || originalTestDir === originalClientDir)) {
5621
- testDir = join(originalTestDir, "tests");
5728
+ testDir = join2(originalTestDir, "tests");
5622
5729
  }
5623
5730
  const testFramework = cfg.tests?.framework || "vitest";
5624
5731
  console.log("\uD83D\uDCC1 Creating directories...");
5625
5732
  const dirs = [
5626
5733
  serverDir,
5627
- join(serverDir, "types"),
5628
- join(serverDir, "zod"),
5629
- join(serverDir, "routes"),
5734
+ join2(serverDir, "types"),
5735
+ join2(serverDir, "zod"),
5736
+ join2(serverDir, "routes"),
5630
5737
  clientDir,
5631
- join(clientDir, "types"),
5632
- join(clientDir, "zod"),
5633
- join(clientDir, "params")
5738
+ join2(clientDir, "types"),
5739
+ join2(clientDir, "zod"),
5740
+ join2(clientDir, "params")
5634
5741
  ];
5635
5742
  if (generateTests) {
5636
5743
  dirs.push(testDir);
@@ -5638,26 +5745,26 @@ async function generate(configPath) {
5638
5745
  await ensureDirs(dirs);
5639
5746
  const files = [];
5640
5747
  const includeSpec = emitIncludeSpec(graph);
5641
- files.push({ path: join(serverDir, "include-spec.ts"), content: includeSpec });
5642
- files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
5643
- files.push({ path: join(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
5644
- files.push({ path: join(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
5645
- files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
5646
- files.push({ path: join(clientDir, "where-types.ts"), content: emitWhereTypes() });
5748
+ files.push({ path: join2(serverDir, "include-spec.ts"), content: includeSpec });
5749
+ files.push({ path: join2(clientDir, "include-spec.ts"), content: includeSpec });
5750
+ files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
5751
+ files.push({ path: join2(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
5752
+ files.push({ path: join2(clientDir, "base-client.ts"), content: emitBaseClient() });
5753
+ files.push({ path: join2(clientDir, "where-types.ts"), content: emitWhereTypes() });
5647
5754
  files.push({
5648
- path: join(serverDir, "include-builder.ts"),
5755
+ path: join2(serverDir, "include-builder.ts"),
5649
5756
  content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
5650
5757
  });
5651
5758
  files.push({
5652
- path: join(serverDir, "include-loader.ts"),
5759
+ path: join2(serverDir, "include-loader.ts"),
5653
5760
  content: emitIncludeLoader(graph, model, cfg.includeMethodsDepth || 2, cfg.useJsExtensions)
5654
5761
  });
5655
- files.push({ path: join(serverDir, "logger.ts"), content: emitLogger() });
5762
+ files.push({ path: join2(serverDir, "logger.ts"), content: emitLogger() });
5656
5763
  if (getAuthStrategy(normalizedAuth) !== "none") {
5657
- files.push({ path: join(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
5764
+ files.push({ path: join2(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
5658
5765
  }
5659
5766
  files.push({
5660
- path: join(serverDir, "core", "operations.ts"),
5767
+ path: join2(serverDir, "core", "operations.ts"),
5661
5768
  content: emitCoreOperations()
5662
5769
  });
5663
5770
  if (process.env.SDK_DEBUG) {
@@ -5666,13 +5773,13 @@ async function generate(configPath) {
5666
5773
  for (const table of Object.values(model.tables)) {
5667
5774
  const numericMode = cfg.numericMode ?? "auto";
5668
5775
  const typesSrc = emitTypes(table, { numericMode }, model.enums);
5669
- files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
5670
- files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
5776
+ files.push({ path: join2(serverDir, "types", `${table.name}.ts`), content: typesSrc });
5777
+ files.push({ path: join2(clientDir, "types", `${table.name}.ts`), content: typesSrc });
5671
5778
  const zodSrc = emitZod(table, { numericMode }, model.enums);
5672
- files.push({ path: join(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
5673
- files.push({ path: join(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
5779
+ files.push({ path: join2(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
5780
+ files.push({ path: join2(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
5674
5781
  const paramsZodSrc = emitParamsZod(table, graph);
5675
- files.push({ path: join(clientDir, "params", `${table.name}.ts`), content: paramsZodSrc });
5782
+ files.push({ path: join2(clientDir, "params", `${table.name}.ts`), content: paramsZodSrc });
5676
5783
  let routeContent;
5677
5784
  if (serverFramework === "hono") {
5678
5785
  routeContent = emitHonoRoutes(table, graph, {
@@ -5686,11 +5793,11 @@ async function generate(configPath) {
5686
5793
  throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
5687
5794
  }
5688
5795
  files.push({
5689
- path: join(serverDir, "routes", `${table.name}.ts`),
5796
+ path: join2(serverDir, "routes", `${table.name}.ts`),
5690
5797
  content: routeContent
5691
5798
  });
5692
5799
  files.push({
5693
- path: join(clientDir, `${table.name}.ts`),
5800
+ path: join2(clientDir, `${table.name}.ts`),
5694
5801
  content: emitClient(table, graph, {
5695
5802
  useJsExtensions: cfg.useJsExtensionsClient,
5696
5803
  includeMethodsDepth: cfg.includeMethodsDepth ?? 2,
@@ -5699,12 +5806,12 @@ async function generate(configPath) {
5699
5806
  });
5700
5807
  }
5701
5808
  files.push({
5702
- path: join(clientDir, "index.ts"),
5809
+ path: join2(clientDir, "index.ts"),
5703
5810
  content: emitClientIndex(Object.values(model.tables), cfg.useJsExtensionsClient)
5704
5811
  });
5705
5812
  if (serverFramework === "hono") {
5706
5813
  files.push({
5707
- path: join(serverDir, "router.ts"),
5814
+ path: join2(serverDir, "router.ts"),
5708
5815
  content: emitHonoRouter(Object.values(model.tables), getAuthStrategy(normalizedAuth) !== "none", cfg.useJsExtensions, cfg.pullToken)
5709
5816
  });
5710
5817
  }
@@ -5714,59 +5821,72 @@ async function generate(configPath) {
5714
5821
  }
5715
5822
  const contract = generateUnifiedContract2(model, cfg, graph);
5716
5823
  files.push({
5717
- path: join(serverDir, "CONTRACT.md"),
5824
+ path: join2(serverDir, "CONTRACT.md"),
5718
5825
  content: generateUnifiedContractMarkdown2(contract)
5719
5826
  });
5720
5827
  files.push({
5721
- path: join(clientDir, "CONTRACT.md"),
5828
+ path: join2(clientDir, "CONTRACT.md"),
5722
5829
  content: generateUnifiedContractMarkdown2(contract)
5723
5830
  });
5724
5831
  const contractCode = emitUnifiedContract(model, cfg, graph);
5725
5832
  files.push({
5726
- path: join(serverDir, "contract.ts"),
5833
+ path: join2(serverDir, "contract.ts"),
5727
5834
  content: contractCode
5728
5835
  });
5729
5836
  const clientFiles = files.filter((f) => {
5730
5837
  return f.path.includes(clientDir);
5731
5838
  });
5732
5839
  files.push({
5733
- path: join(serverDir, "sdk-bundle.ts"),
5840
+ path: join2(serverDir, "sdk-bundle.ts"),
5734
5841
  content: emitSdkBundle(clientFiles, clientDir)
5735
5842
  });
5736
5843
  if (generateTests) {
5737
5844
  console.log("\uD83E\uDDEA Generating tests...");
5738
5845
  const relativeClientPath = relative(testDir, clientDir);
5739
5846
  files.push({
5740
- path: join(testDir, "setup.ts"),
5847
+ path: join2(testDir, "setup.ts"),
5741
5848
  content: emitTestSetup(relativeClientPath, testFramework)
5742
5849
  });
5743
5850
  files.push({
5744
- path: join(testDir, "docker-compose.yml"),
5851
+ path: join2(testDir, "docker-compose.yml"),
5745
5852
  content: emitDockerCompose()
5746
5853
  });
5747
5854
  files.push({
5748
- path: join(testDir, "run-tests.sh"),
5855
+ path: join2(testDir, "run-tests.sh"),
5749
5856
  content: emitTestScript(testFramework, testDir)
5750
5857
  });
5751
5858
  files.push({
5752
- path: join(testDir, ".gitignore"),
5859
+ path: join2(testDir, ".gitignore"),
5753
5860
  content: emitTestGitignore()
5754
5861
  });
5755
5862
  if (testFramework === "vitest") {
5756
5863
  files.push({
5757
- path: join(testDir, "vitest.config.ts"),
5864
+ path: join2(testDir, "vitest.config.ts"),
5758
5865
  content: emitVitestConfig()
5759
5866
  });
5760
5867
  }
5761
5868
  for (const table of Object.values(model.tables)) {
5762
5869
  files.push({
5763
- path: join(testDir, `${table.name}.test.ts`),
5870
+ path: join2(testDir, `${table.name}.test.ts`),
5764
5871
  content: emitTableTest(table, model, relativeClientPath, testFramework)
5765
5872
  });
5766
5873
  }
5767
5874
  }
5768
5875
  console.log("✍️ Writing files...");
5769
5876
  await writeFiles(files);
5877
+ const outDirStr = typeof cfg.outDir === "string" ? cfg.outDir : `${cfg.outDir?.server || "./api/server"}`;
5878
+ await writeCache({
5879
+ schemaHash,
5880
+ lastRun: new Date().toISOString(),
5881
+ filesGenerated: files.length,
5882
+ config: {
5883
+ outDir: cfg.outDir || "./api",
5884
+ schema: cfg.schema || "public"
5885
+ }
5886
+ });
5887
+ await appendToHistory(`Generate
5888
+ ✅ Generated ${files.length} files
5889
+ - Schema hash: ${schemaHash.substring(0, 12)}...`);
5770
5890
  console.log(`✅ Generated ${files.length} files`);
5771
5891
  console.log(` Server: ${serverDir}`);
5772
5892
  console.log(` Client: ${sameDirectory ? clientDir + " (in sdk subdir due to same output dir)" : clientDir}`);
package/dist/utils.d.ts CHANGED
@@ -3,4 +3,20 @@ export declare function writeFiles(files: Array<{
3
3
  path: string;
4
4
  content: string;
5
5
  }>): Promise<void>;
6
+ /**
7
+ * Write files only if content has changed (idempotent)
8
+ * Returns the count of files actually written
9
+ */
10
+ export declare function writeFilesIfChanged(files: Array<{
11
+ path: string;
12
+ content: string;
13
+ }>): Promise<{
14
+ written: number;
15
+ unchanged: number;
16
+ filesWritten: string[];
17
+ }>;
18
+ /**
19
+ * Compute hash of a string
20
+ */
21
+ export declare function hashContent(content: string): string;
6
22
  export declare function ensureDirs(dirs: string[]): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.16.14",
3
+ "version": "0.17.0",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {