postgresdk 0.9.2 → 0.9.5

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,14 @@
1
+ import type { Config } from "./types";
2
+ export interface ConfigField {
3
+ key: string;
4
+ value: any;
5
+ description: string;
6
+ isRequired?: boolean;
7
+ isCommented?: boolean;
8
+ }
9
+ export declare function parseExistingConfig(configPath: string): {
10
+ config: Config | null;
11
+ raw: string;
12
+ };
13
+ export declare function extractConfigFields(configContent: string): ConfigField[];
14
+ export declare function generateMergedConfig(existingFields: ConfigField[], mergeStrategy: "keep-existing" | "use-defaults" | "interactive", userChoices?: Map<string, any> | undefined): string;
package/dist/cli.js CHANGED
@@ -602,8 +602,12 @@ function generateIncludeMethods(table, graph, opts, allTables) {
602
602
  continue;
603
603
  const [key1, edge1] = entry1;
604
604
  const [key2, edge2] = entry2;
605
- if (opts.skipJunctionTables && (edge1.target.includes("_") || edge2.target.includes("_"))) {
606
- continue;
605
+ if (opts.skipJunctionTables && allTables) {
606
+ const table1 = allTables.find((t) => t.name === edge1.target);
607
+ const table2 = allTables.find((t) => t.name === edge2.target);
608
+ if (table1 && isJunctionTable(table1) || table2 && isJunctionTable(table2)) {
609
+ continue;
610
+ }
607
611
  }
608
612
  const combinedPath = [key1, key2];
609
613
  const combinedSuffix = `With${pascal(key1)}And${pascal(key2)}`;
@@ -1302,21 +1306,411 @@ var init_emit_sdk_contract = __esm(() => {
1302
1306
  init_emit_include_methods();
1303
1307
  });
1304
1308
 
1309
+ // src/cli-config-utils.ts
1310
+ function extractConfigFields(configContent) {
1311
+ const fields = [];
1312
+ const connectionMatch = configContent.match(/connectionString:\s*(.+),?$/m);
1313
+ if (connectionMatch) {
1314
+ fields.push({
1315
+ key: "connectionString",
1316
+ value: connectionMatch[1]?.trim(),
1317
+ description: "PostgreSQL connection string",
1318
+ isRequired: true,
1319
+ isCommented: false
1320
+ });
1321
+ }
1322
+ const schemaMatch = configContent.match(/^\s*(\/\/)?\s*schema:\s*"(.+)"/m);
1323
+ if (schemaMatch) {
1324
+ fields.push({
1325
+ key: "schema",
1326
+ value: schemaMatch[2],
1327
+ description: "Database schema to introspect",
1328
+ isCommented: !!schemaMatch[1]
1329
+ });
1330
+ }
1331
+ const outServerMatch = configContent.match(/^\s*(\/\/)?\s*outServer:\s*"(.+)"/m);
1332
+ if (outServerMatch) {
1333
+ fields.push({
1334
+ key: "outServer",
1335
+ value: outServerMatch[2],
1336
+ description: "Output directory for server-side code",
1337
+ isCommented: !!outServerMatch[1]
1338
+ });
1339
+ }
1340
+ const outClientMatch = configContent.match(/^\s*(\/\/)?\s*outClient:\s*"(.+)"/m);
1341
+ if (outClientMatch) {
1342
+ fields.push({
1343
+ key: "outClient",
1344
+ value: outClientMatch[2],
1345
+ description: "Output directory for client SDK",
1346
+ isCommented: !!outClientMatch[1]
1347
+ });
1348
+ }
1349
+ const softDeleteMatch = configContent.match(/^\s*(\/\/)?\s*softDeleteColumn:\s*(.+),?$/m);
1350
+ if (softDeleteMatch) {
1351
+ fields.push({
1352
+ key: "softDeleteColumn",
1353
+ value: softDeleteMatch[2]?.trim().replace(/,$/, "").replace(/["']/g, ""),
1354
+ description: "Column name for soft deletes",
1355
+ isCommented: !!softDeleteMatch[1]
1356
+ });
1357
+ }
1358
+ const depthMatch = configContent.match(/^\s*(\/\/)?\s*includeDepthLimit:\s*(\d+)/m);
1359
+ if (depthMatch) {
1360
+ fields.push({
1361
+ key: "includeDepthLimit",
1362
+ value: parseInt(depthMatch[2]),
1363
+ description: "Maximum depth for nested relationship includes",
1364
+ isCommented: !!depthMatch[1]
1365
+ });
1366
+ }
1367
+ const frameworkMatch = configContent.match(/^\s*(\/\/)?\s*serverFramework:\s*"(.+)"/m);
1368
+ if (frameworkMatch) {
1369
+ fields.push({
1370
+ key: "serverFramework",
1371
+ value: frameworkMatch[2],
1372
+ description: "Server framework for generated API routes",
1373
+ isCommented: !!frameworkMatch[1]
1374
+ });
1375
+ }
1376
+ const jsExtMatch = configContent.match(/^\s*(\/\/)?\s*useJsExtensions:\s*(true|false)/m);
1377
+ if (jsExtMatch) {
1378
+ fields.push({
1379
+ key: "useJsExtensions",
1380
+ value: jsExtMatch[2] === "true",
1381
+ description: "Use .js extensions in server imports",
1382
+ isCommented: !!jsExtMatch[1]
1383
+ });
1384
+ }
1385
+ const jsExtClientMatch = configContent.match(/^\s*(\/\/)?\s*useJsExtensionsClient:\s*(true|false)/m);
1386
+ if (jsExtClientMatch) {
1387
+ fields.push({
1388
+ key: "useJsExtensionsClient",
1389
+ value: jsExtClientMatch[2] === "true",
1390
+ description: "Use .js extensions in client SDK imports",
1391
+ isCommented: !!jsExtClientMatch[1]
1392
+ });
1393
+ }
1394
+ const testsMatch = configContent.match(/^\s*(\/\/)?\s*tests:\s*\{/m);
1395
+ if (testsMatch) {
1396
+ fields.push({
1397
+ key: "tests",
1398
+ value: "configured",
1399
+ description: "Test generation configuration",
1400
+ isCommented: !!testsMatch[1]
1401
+ });
1402
+ }
1403
+ const authMatch = configContent.match(/^\s*(\/\/)?\s*auth:\s*\{/m);
1404
+ if (authMatch) {
1405
+ fields.push({
1406
+ key: "auth",
1407
+ value: "configured",
1408
+ description: "Authentication configuration",
1409
+ isCommented: !!authMatch[1]
1410
+ });
1411
+ }
1412
+ const pullMatch = configContent.match(/^\s*(\/\/)?\s*pull:\s*\{/m);
1413
+ if (pullMatch) {
1414
+ fields.push({
1415
+ key: "pull",
1416
+ value: "configured",
1417
+ description: "SDK distribution configuration",
1418
+ isCommented: !!pullMatch[1]
1419
+ });
1420
+ }
1421
+ return fields;
1422
+ }
1423
+ function generateMergedConfig(existingFields, mergeStrategy, userChoices = undefined) {
1424
+ const template = `/**
1425
+ * PostgreSDK Configuration
1426
+ *
1427
+ * This file configures how postgresdk generates your SDK.
1428
+ * Environment variables are automatically loaded from .env files.
1429
+ */
1430
+
1431
+ export default {
1432
+ // ========== DATABASE CONNECTION (Required) ==========
1433
+
1434
+ /**
1435
+ * PostgreSQL connection string
1436
+ * Format: postgres://user:password@host:port/database
1437
+ */
1438
+ connectionString: ${getFieldValue("connectionString", existingFields, mergeStrategy, userChoices)},
1439
+
1440
+ // ========== BASIC OPTIONS ==========
1441
+
1442
+ /**
1443
+ * Database schema to introspect
1444
+ * @default "public"
1445
+ */
1446
+ ${getFieldLine("schema", existingFields, mergeStrategy, '"public"', userChoices)}
1447
+
1448
+ /**
1449
+ * Output directory for server-side code (routes, validators, etc.)
1450
+ * @default "./api/server"
1451
+ */
1452
+ ${getFieldLine("outServer", existingFields, mergeStrategy, '"./api/server"', userChoices)}
1453
+
1454
+ /**
1455
+ * Output directory for client SDK
1456
+ * @default "./api/client"
1457
+ */
1458
+ ${getFieldLine("outClient", existingFields, mergeStrategy, '"./api/client"', userChoices)}
1459
+
1460
+ // ========== ADVANCED OPTIONS ==========
1461
+
1462
+ /**
1463
+ * Column name for soft deletes. When set, DELETE operations will update
1464
+ * this column instead of removing rows.
1465
+ * @default null (hard deletes)
1466
+ * @example "deleted_at"
1467
+ */
1468
+ ${getFieldLine("softDeleteColumn", existingFields, mergeStrategy, "null", userChoices)}
1469
+
1470
+ /**
1471
+ * Maximum depth for nested relationship includes to prevent infinite loops
1472
+ * @default 3
1473
+ */
1474
+ ${getFieldLine("includeDepthLimit", existingFields, mergeStrategy, "3", userChoices)}
1475
+
1476
+
1477
+ /**
1478
+ * Server framework for generated API routes
1479
+ * - "hono": Lightweight, edge-compatible web framework (default)
1480
+ * - "express": Traditional Node.js framework (planned)
1481
+ * - "fastify": High-performance Node.js framework (planned)
1482
+ * @default "hono"
1483
+ */
1484
+ ${getFieldLine("serverFramework", existingFields, mergeStrategy, '"hono"', userChoices)}
1485
+
1486
+ /**
1487
+ * Use .js extensions in server imports (for Vercel Edge, Deno, etc.)
1488
+ * @default false
1489
+ */
1490
+ ${getFieldLine("useJsExtensions", existingFields, mergeStrategy, "false", userChoices)}
1491
+
1492
+ /**
1493
+ * Use .js extensions in client SDK imports (rarely needed)
1494
+ * @default false
1495
+ */
1496
+ ${getFieldLine("useJsExtensionsClient", existingFields, mergeStrategy, "false", userChoices)}
1497
+
1498
+ // ========== TEST GENERATION ==========
1499
+
1500
+ /**
1501
+ * Generate basic SDK tests
1502
+ * Uncomment to enable test generation with Docker setup
1503
+ */
1504
+ // tests: {
1505
+ // generate: true,
1506
+ // output: "./api/tests",
1507
+ // framework: "vitest" // or "jest" or "bun"
1508
+ // },
1509
+
1510
+ // ========== AUTHENTICATION ==========
1511
+
1512
+ /**
1513
+ * Authentication configuration for your API
1514
+ *
1515
+ * Simple syntax examples:
1516
+ * auth: { apiKey: process.env.API_KEY }
1517
+ * auth: { jwt: process.env.JWT_SECRET }
1518
+ *
1519
+ * Multiple API keys:
1520
+ * auth: { apiKeys: [process.env.KEY1, process.env.KEY2] }
1521
+ *
1522
+ * Full syntax for advanced options:
1523
+ */
1524
+ // auth: {
1525
+ // // Strategy: "none" | "api-key" | "jwt-hs256"
1526
+ // strategy: "none",
1527
+ //
1528
+ // // For API Key authentication
1529
+ // apiKeyHeader: "x-api-key", // Header name for API key
1530
+ // apiKeys: [ // List of valid API keys
1531
+ // process.env.API_KEY_1,
1532
+ // process.env.API_KEY_2,
1533
+ // ],
1534
+ //
1535
+ // // For JWT (HS256) authentication
1536
+ // jwt: {
1537
+ // sharedSecret: process.env.JWT_SECRET, // Secret for signing/verifying
1538
+ // issuer: "my-app", // Optional: validate 'iss' claim
1539
+ // audience: "my-users", // Optional: validate 'aud' claim
1540
+ // }
1541
+ // },
1542
+
1543
+ // ========== SDK DISTRIBUTION (Pull Configuration) ==========
1544
+
1545
+ /**
1546
+ * Configuration for pulling SDK from a remote API
1547
+ * Used when running 'postgresdk pull' command
1548
+ */
1549
+ // pull: {
1550
+ // from: "https://api.myapp.com", // API URL to pull SDK from
1551
+ // output: "./src/sdk", // Local directory for pulled SDK
1552
+ // token: process.env.API_TOKEN, // Optional authentication token
1553
+ // },
1554
+ };
1555
+ `;
1556
+ return template;
1557
+ }
1558
+ function getFieldValue(key, existingFields, mergeStrategy, userChoices) {
1559
+ const existing = existingFields.find((f) => f.key === key);
1560
+ if (mergeStrategy === "keep-existing" && existing && !existing.isCommented) {
1561
+ const value = existing.value.toString().replace(/,\s*$/, "");
1562
+ return value;
1563
+ }
1564
+ if (mergeStrategy === "interactive" && userChoices?.has(key)) {
1565
+ return userChoices.get(key);
1566
+ }
1567
+ return 'process.env.DATABASE_URL || "postgres://user:password@localhost:5432/mydb"';
1568
+ }
1569
+ function getFieldLine(key, existingFields, mergeStrategy, defaultValue, userChoices) {
1570
+ const existing = existingFields.find((f) => f.key === key);
1571
+ const shouldUseExisting = mergeStrategy === "keep-existing" && existing && !existing.isCommented || mergeStrategy === "interactive" && userChoices?.get(key) === "keep";
1572
+ const shouldUseNew = mergeStrategy === "use-defaults" || mergeStrategy === "interactive" && userChoices?.get(key) === "new";
1573
+ if (shouldUseExisting && existing) {
1574
+ const value = typeof existing.value === "string" && !existing.value.startsWith('"') ? `"${existing.value}"` : existing.value;
1575
+ return `${key}: ${value},`;
1576
+ }
1577
+ if (shouldUseNew) {
1578
+ return `${key}: ${defaultValue},`;
1579
+ }
1580
+ return `// ${key}: ${defaultValue},`;
1581
+ }
1582
+ var init_cli_config_utils = () => {};
1583
+
1305
1584
  // src/cli-init.ts
1306
1585
  var exports_cli_init = {};
1307
1586
  __export(exports_cli_init, {
1308
1587
  initCommand: () => initCommand
1309
1588
  });
1310
- import { existsSync, writeFileSync } from "fs";
1589
+ import { existsSync, writeFileSync, readFileSync, copyFileSync } from "fs";
1311
1590
  import { resolve } from "path";
1591
+ import prompts from "prompts";
1312
1592
  async function initCommand(args) {
1313
1593
  console.log(`\uD83D\uDE80 Initializing postgresdk configuration...
1314
1594
  `);
1595
+ const forceError = args.includes("--force-error");
1315
1596
  const configPath = resolve(process.cwd(), "postgresdk.config.ts");
1316
1597
  if (existsSync(configPath)) {
1317
- console.error("❌ Error: postgresdk.config.ts already exists");
1318
- console.log(" To reinitialize, please remove or rename the existing file first.");
1319
- process.exit(1);
1598
+ if (forceError) {
1599
+ console.error(" Error: postgresdk.config.ts already exists");
1600
+ console.log(" To reinitialize, please remove or rename the existing file first.");
1601
+ process.exit(1);
1602
+ }
1603
+ console.log(`⚠️ Found existing postgresdk.config.ts
1604
+ `);
1605
+ const existingContent = readFileSync(configPath, "utf-8");
1606
+ const existingFields = extractConfigFields(existingContent);
1607
+ console.log("\uD83D\uDCCB Existing configuration detected:");
1608
+ existingFields.forEach((field) => {
1609
+ if (!field.isCommented) {
1610
+ console.log(` • ${field.description}: ${field.value}`);
1611
+ }
1612
+ });
1613
+ console.log();
1614
+ const { mergeStrategy } = await prompts({
1615
+ type: "select",
1616
+ name: "mergeStrategy",
1617
+ message: "How would you like to proceed?",
1618
+ choices: [
1619
+ {
1620
+ title: "Keep existing values and add new options",
1621
+ value: "keep-existing",
1622
+ description: "Preserves your current settings while adding any new configuration options"
1623
+ },
1624
+ {
1625
+ title: "Interactive merge (recommended)",
1626
+ value: "interactive",
1627
+ description: "Review each setting and choose what to keep"
1628
+ },
1629
+ {
1630
+ title: "Replace with fresh defaults",
1631
+ value: "use-defaults",
1632
+ description: "Creates a new config with default values (backs up existing)"
1633
+ },
1634
+ {
1635
+ title: "Cancel",
1636
+ value: "cancel",
1637
+ description: "Exit without making changes"
1638
+ }
1639
+ ],
1640
+ initial: 1
1641
+ });
1642
+ if (!mergeStrategy || mergeStrategy === "cancel") {
1643
+ console.log(`
1644
+ ✅ Cancelled. No changes made.`);
1645
+ process.exit(0);
1646
+ }
1647
+ let userChoices;
1648
+ if (mergeStrategy === "interactive") {
1649
+ userChoices = new Map;
1650
+ console.log(`
1651
+ \uD83D\uDD04 Let's review your configuration:
1652
+ `);
1653
+ for (const field of existingFields.filter((f) => !f.isCommented)) {
1654
+ const { choice } = await prompts({
1655
+ type: "select",
1656
+ name: "choice",
1657
+ message: `${field.description}:
1658
+ Current: ${field.value}
1659
+ Action:`,
1660
+ choices: [
1661
+ { title: "Keep current value", value: "keep" },
1662
+ { title: "Use default value", value: "new" }
1663
+ ],
1664
+ initial: 0
1665
+ });
1666
+ if (!choice) {
1667
+ console.log(`
1668
+ ✅ Cancelled. No changes made.`);
1669
+ process.exit(0);
1670
+ }
1671
+ userChoices.set(field.key, choice);
1672
+ }
1673
+ const newOptions = [
1674
+ { key: "tests", description: "Enable test generation" },
1675
+ { key: "auth", description: "Add authentication" },
1676
+ { key: "pull", description: "Configure SDK distribution" }
1677
+ ];
1678
+ const existingKeys = new Set(existingFields.map((f) => f.key));
1679
+ const missingOptions = newOptions.filter((opt) => !existingKeys.has(opt.key));
1680
+ if (missingOptions.length > 0) {
1681
+ console.log(`
1682
+ \uD83D\uDCE6 New configuration options available:
1683
+ `);
1684
+ for (const option of missingOptions) {
1685
+ const { addOption } = await prompts({
1686
+ type: "confirm",
1687
+ name: "addOption",
1688
+ message: `Add ${option.description} configuration? (commented out by default)`,
1689
+ initial: false
1690
+ });
1691
+ if (addOption) {
1692
+ userChoices.set(option.key, "add-commented");
1693
+ }
1694
+ }
1695
+ }
1696
+ }
1697
+ const backupPath = configPath + ".backup." + Date.now();
1698
+ copyFileSync(configPath, backupPath);
1699
+ console.log(`
1700
+ \uD83D\uDCBE Backed up existing config to: ${backupPath}`);
1701
+ const mergedConfig = generateMergedConfig(existingFields, mergeStrategy, userChoices);
1702
+ try {
1703
+ writeFileSync(configPath, mergedConfig, "utf-8");
1704
+ console.log("✅ Updated postgresdk.config.ts with merged configuration");
1705
+ console.log(`
1706
+ \uD83D\uDCA1 Your previous config has been backed up.`);
1707
+ console.log(" Run 'postgresdk generate' to create your SDK with the new config.");
1708
+ } catch (error) {
1709
+ console.error("❌ Error updating config file:", error);
1710
+ console.log(` Your backup is available at: ${backupPath}`);
1711
+ process.exit(1);
1712
+ }
1713
+ return;
1320
1714
  }
1321
1715
  const envPath = resolve(process.cwd(), ".env");
1322
1716
  const hasEnv = existsSync(envPath);
@@ -1473,7 +1867,9 @@ export default {
1473
1867
  // },
1474
1868
  };
1475
1869
  `;
1476
- var init_cli_init = () => {};
1870
+ var init_cli_init = __esm(() => {
1871
+ init_cli_config_utils();
1872
+ });
1477
1873
 
1478
1874
  // src/cli-pull.ts
1479
1875
  var exports_cli_pull = {};
@@ -4234,12 +4630,12 @@ async function generate(configPath) {
4234
4630
  // src/cli.ts
4235
4631
  var import_config2 = __toESM(require_config(), 1);
4236
4632
  import { resolve as resolve3 } from "node:path";
4237
- import { readFileSync } from "node:fs";
4633
+ import { readFileSync as readFileSync2 } from "node:fs";
4238
4634
  import { fileURLToPath } from "node:url";
4239
4635
  import { dirname as dirname3, join as join3 } from "node:path";
4240
4636
  var __filename2 = fileURLToPath(import.meta.url);
4241
4637
  var __dirname2 = dirname3(__filename2);
4242
- var packageJson = JSON.parse(readFileSync(join3(__dirname2, "../package.json"), "utf-8"));
4638
+ var packageJson = JSON.parse(readFileSync2(join3(__dirname2, "../package.json"), "utf-8"));
4243
4639
  var VERSION = packageJson.version;
4244
4640
  var args = process.argv.slice(2);
4245
4641
  var command = args[0];
package/dist/index.js CHANGED
@@ -601,8 +601,12 @@ function generateIncludeMethods(table, graph, opts, allTables) {
601
601
  continue;
602
602
  const [key1, edge1] = entry1;
603
603
  const [key2, edge2] = entry2;
604
- if (opts.skipJunctionTables && (edge1.target.includes("_") || edge2.target.includes("_"))) {
605
- continue;
604
+ if (opts.skipJunctionTables && allTables) {
605
+ const table1 = allTables.find((t) => t.name === edge1.target);
606
+ const table2 = allTables.find((t) => t.name === edge2.target);
607
+ if (table1 && isJunctionTable(table1) || table2 && isJunctionTable(table2)) {
608
+ continue;
609
+ }
606
610
  }
607
611
  const combinedPath = [key1, key2];
608
612
  const combinedSuffix = `With${pascal(key1)}And${pascal(key2)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.9.2",
3
+ "version": "0.9.5",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "node": ">=18.17"
22
22
  },
23
23
  "scripts": {
24
- "build": "bun build src/cli.ts src/index.ts --outdir dist --target node --format esm --external=pg --external=zod --external=hono --external=node:* && tsc -p tsconfig.build.json --emitDeclarationOnly",
24
+ "build": "bun build src/cli.ts src/index.ts --outdir dist --target node --format esm --external=pg --external=zod --external=hono --external=prompts --external=node:* && tsc -p tsconfig.build.json --emitDeclarationOnly",
25
25
  "test": "bun test:init && bun test:gen && bun test:gen-with-tests && bun test:pull && bun test:typecheck && bun test:drizzle-e2e",
26
26
  "test:init": "bun test/test-init.ts",
27
27
  "test:gen": "bun test/test-gen.ts",
@@ -36,9 +36,11 @@
36
36
  "publish:major": "./publish.sh"
37
37
  },
38
38
  "dependencies": {
39
+ "@types/prompts": "^2.4.9",
39
40
  "dotenv": "^17.2.1",
40
41
  "hono": "^4.9.0",
41
42
  "pg": "^8.16.3",
43
+ "prompts": "^2.4.2",
42
44
  "zod": "^4.0.15"
43
45
  },
44
46
  "devDependencies": {