postgresdk 0.6.17 → 0.7.1

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.
Files changed (4) hide show
  1. package/README.md +43 -869
  2. package/dist/cli.js +26 -314
  3. package/dist/index.js +26 -314
  4. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -743,7 +743,7 @@ console.log('Deleted:', deleted);`,
743
743
  responseBody: `${Type}`
744
744
  });
745
745
  }
746
- const fields = table.columns.map((col) => generateFieldContract2(col, table));
746
+ const fields = table.columns.map((col) => generateFieldContract(col, table));
747
747
  return {
748
748
  name: Type,
749
749
  tableName,
@@ -758,13 +758,13 @@ console.log('Deleted:', deleted);`,
758
758
  fields
759
759
  };
760
760
  }
761
- function generateFieldContract2(column, table) {
761
+ function generateFieldContract(column, table) {
762
762
  const field = {
763
763
  name: column.name,
764
- type: postgresTypeToJsonType2(column.pgType),
764
+ type: postgresTypeToJsonType(column.pgType),
765
765
  tsType: postgresTypeToTsType(column),
766
766
  required: !column.nullable && !column.hasDefault,
767
- description: generateFieldDescription2(column, table)
767
+ description: generateFieldDescription(column, table)
768
768
  };
769
769
  const fk = table.fks.find((fk2) => fk2.from.length === 1 && fk2.from[0] === column.name);
770
770
  if (fk) {
@@ -898,7 +898,7 @@ function generateQueryParams(table) {
898
898
  for (const col of table.columns) {
899
899
  if (filterCount >= 3)
900
900
  break;
901
- const type = postgresTypeToJsonType2(col.pgType);
901
+ const type = postgresTypeToJsonType(col.pgType);
902
902
  params[col.name] = `${type} - Filter by ${col.name}`;
903
903
  if (type === "string") {
904
904
  params[`${col.name}_like`] = `string - Search in ${col.name}`;
@@ -911,7 +911,7 @@ function generateQueryParams(table) {
911
911
  params["..."] = "Additional filters for all fields";
912
912
  return params;
913
913
  }
914
- function postgresTypeToJsonType2(pgType) {
914
+ function postgresTypeToJsonType(pgType) {
915
915
  switch (pgType) {
916
916
  case "int":
917
917
  case "integer":
@@ -945,7 +945,7 @@ function postgresTypeToJsonType2(pgType) {
945
945
  return "string";
946
946
  }
947
947
  }
948
- function generateFieldDescription2(column, table) {
948
+ function generateFieldDescription(column, table) {
949
949
  const descriptions = [];
950
950
  if (column.name === "id") {
951
951
  descriptions.push("Primary key");
@@ -1422,9 +1422,6 @@ ${fields}
1422
1422
  });
1423
1423
 
1424
1424
  export const Update${Type}Schema = Insert${Type}Schema.partial();
1425
-
1426
- export type Insert${Type} = z.infer<typeof Insert${Type}Schema>;
1427
- export type Update${Type} = z.infer<typeof Update${Type}Schema>;
1428
1425
  `;
1429
1426
  }
1430
1427
 
@@ -1653,9 +1650,7 @@ function emitClientIndex(tables, useJsExtensions) {
1653
1650
  `;
1654
1651
  }
1655
1652
  out += `
1656
- // Re-export auth types for convenience
1657
- `;
1658
- out += `export type { AuthConfig as SDKAuth, AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${ext}";
1653
+ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${ext}";
1659
1654
 
1660
1655
  `;
1661
1656
  out += `/**
@@ -1683,23 +1678,18 @@ function emitClientIndex(tables, useJsExtensions) {
1683
1678
  `;
1684
1679
  out += `}
1685
1680
 
1686
- `;
1687
- out += `// Export individual table clients
1688
- `;
1689
- for (const t of tables) {
1690
- out += `export { ${pascal(t.name)}Client } from "./${t.name}${ext}";
1691
- `;
1692
- }
1693
- out += `
1694
- // Export base client for custom extensions
1695
1681
  `;
1696
1682
  out += `export { BaseClient } from "./base-client${ext}";
1683
+ `;
1684
+ out += `export * from "./include-spec${ext}";
1697
1685
  `;
1698
1686
  out += `
1699
- // Export include specifications
1687
+ // Zod schemas for form validation
1700
1688
  `;
1701
- out += `export * from "./include-spec${ext}";
1689
+ for (const t of tables) {
1690
+ out += `export { Insert${pascal(t.name)}Schema, Update${pascal(t.name)}Schema } from "./zod/${t.name}${ext}";
1702
1691
  `;
1692
+ }
1703
1693
  return out;
1704
1694
  }
1705
1695
 
@@ -2395,7 +2385,7 @@ function emitHonoRouter(tables, hasAuth, useJsExtensions) {
2395
2385
  return `/* Generated. Do not edit. */
2396
2386
  import { Hono } from "hono";
2397
2387
  import { SDK_MANIFEST } from "./sdk-bundle${ext}";
2398
- import { getApiContract } from "./api-contract${ext}";
2388
+ import { getContract } from "./contract${ext}";
2399
2389
  ${imports}
2400
2390
  ${hasAuth ? `export { authMiddleware } from "./auth${ext}";` : ""}
2401
2391
 
@@ -2462,20 +2452,20 @@ ${registrations}
2462
2452
  const format = c.req.query("format") || "json";
2463
2453
 
2464
2454
  if (format === "markdown") {
2465
- return c.text(getApiContract("markdown") as string, 200, {
2455
+ return c.text(getContract("markdown") as string, 200, {
2466
2456
  "Content-Type": "text/markdown; charset=utf-8"
2467
2457
  });
2468
2458
  }
2469
2459
 
2470
- return c.json(getApiContract("json"));
2460
+ return c.json(getContract("json"));
2471
2461
  });
2472
2462
 
2473
2463
  router.get("/api/contract.json", (c) => {
2474
- return c.json(getApiContract("json"));
2464
+ return c.json(getContract("json"));
2475
2465
  });
2476
2466
 
2477
2467
  router.get("/api/contract.md", (c) => {
2478
- return c.text(getApiContract("markdown") as string, 200, {
2468
+ return c.text(getContract("markdown") as string, 200, {
2479
2469
  "Content-Type": "text/markdown; charset=utf-8"
2480
2470
  });
2481
2471
  });
@@ -3055,6 +3045,8 @@ if [ ! -z "\${MIGRATION_COMMAND}" ]; then
3055
3045
  exit 1
3056
3046
  fi
3057
3047
  echo "✅ Migrations completed successfully"
3048
+ echo "⏳ Waiting for database to settle..."
3049
+ sleep 2
3058
3050
  elif [ "\${SKIP_MIGRATIONS}" = "true" ]; then
3059
3051
  echo "⏭️ Skipping migrations (SKIP_MIGRATIONS=true)"
3060
3052
  else
@@ -3439,282 +3431,6 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
3439
3431
  });` : ""}`;
3440
3432
  }
3441
3433
 
3442
- // src/emit-api-contract.ts
3443
- init_utils();
3444
- function generateApiContract(model, config) {
3445
- const resources = [];
3446
- const relationships = [];
3447
- const tables = Object.values(model.tables || {});
3448
- for (const table of tables) {
3449
- resources.push(generateResourceContract(table, model));
3450
- for (const fk of table.fks) {
3451
- relationships.push({
3452
- from: table.name,
3453
- to: fk.toTable,
3454
- type: "many-to-one",
3455
- description: `Each ${table.name} belongs to one ${fk.toTable}`
3456
- });
3457
- }
3458
- }
3459
- const contract = {
3460
- version: "1.0.0",
3461
- generatedAt: new Date().toISOString(),
3462
- description: "Auto-generated API contract describing all available endpoints, resources, and their relationships",
3463
- resources,
3464
- relationships
3465
- };
3466
- if (config.auth?.strategy && config.auth.strategy !== "none") {
3467
- contract.authentication = {
3468
- type: config.auth.strategy,
3469
- description: getAuthDescription(config.auth.strategy)
3470
- };
3471
- }
3472
- return contract;
3473
- }
3474
- function generateResourceContract(table, model) {
3475
- const Type = pascal(table.name);
3476
- const basePath = `/v1/${table.name}`;
3477
- const endpoints = [
3478
- {
3479
- method: "GET",
3480
- path: basePath,
3481
- description: `List all ${table.name} records with optional filtering, sorting, and pagination`,
3482
- queryParameters: {
3483
- limit: "number - Maximum number of records to return (default: 50)",
3484
- offset: "number - Number of records to skip for pagination",
3485
- order_by: "string - Field to sort by",
3486
- order_dir: "string - Sort direction (asc or desc)",
3487
- include: "string - Comma-separated list of related resources to include",
3488
- ...generateFilterParams(table)
3489
- },
3490
- responseBody: `Array<${Type}>`
3491
- },
3492
- {
3493
- method: "GET",
3494
- path: `${basePath}/:id`,
3495
- description: `Get a single ${table.name} record by ID`,
3496
- queryParameters: {
3497
- include: "string - Comma-separated list of related resources to include"
3498
- },
3499
- responseBody: `${Type}`
3500
- },
3501
- {
3502
- method: "POST",
3503
- path: basePath,
3504
- description: `Create a new ${table.name} record`,
3505
- requestBody: `Insert${Type}`,
3506
- responseBody: `${Type}`
3507
- },
3508
- {
3509
- method: "PATCH",
3510
- path: `${basePath}/:id`,
3511
- description: `Update an existing ${table.name} record`,
3512
- requestBody: `Update${Type}`,
3513
- responseBody: `${Type}`
3514
- },
3515
- {
3516
- method: "DELETE",
3517
- path: `${basePath}/:id`,
3518
- description: `Delete a ${table.name} record`,
3519
- responseBody: `${Type}`
3520
- }
3521
- ];
3522
- const fields = table.columns.map((col) => generateFieldContract(col, table));
3523
- return {
3524
- name: Type,
3525
- tableName: table.name,
3526
- description: `Resource for managing ${table.name} records`,
3527
- endpoints,
3528
- fields
3529
- };
3530
- }
3531
- function generateFieldContract(column, table) {
3532
- const field = {
3533
- name: column.name,
3534
- type: postgresTypeToJsonType(column.pgType),
3535
- required: !column.nullable && !column.hasDefault,
3536
- description: generateFieldDescription(column, table)
3537
- };
3538
- const fk = table.fks.find((fk2) => fk2.from.length === 1 && fk2.from[0] === column.name);
3539
- if (fk) {
3540
- field.foreignKey = {
3541
- table: fk.toTable,
3542
- field: fk.to[0] || "id"
3543
- };
3544
- }
3545
- return field;
3546
- }
3547
- function generateFieldDescription(column, table) {
3548
- const descriptions = [];
3549
- descriptions.push(`${postgresTypeToJsonType(column.pgType)} field`);
3550
- if (column.name === "id") {
3551
- descriptions.push("Unique identifier");
3552
- } else if (column.name === "created_at") {
3553
- descriptions.push("Timestamp when the record was created");
3554
- } else if (column.name === "updated_at") {
3555
- descriptions.push("Timestamp when the record was last updated");
3556
- } else if (column.name === "deleted_at") {
3557
- descriptions.push("Soft delete timestamp");
3558
- } else if (column.name.endsWith("_id")) {
3559
- const relatedTable = column.name.slice(0, -3);
3560
- descriptions.push(`Reference to ${relatedTable}`);
3561
- } else if (column.name.includes("email")) {
3562
- descriptions.push("Email address");
3563
- } else if (column.name.includes("phone")) {
3564
- descriptions.push("Phone number");
3565
- } else if (column.name.includes("name")) {
3566
- descriptions.push("Name field");
3567
- } else if (column.name.includes("description")) {
3568
- descriptions.push("Description text");
3569
- } else if (column.name.includes("status")) {
3570
- descriptions.push("Status indicator");
3571
- } else if (column.name.includes("price") || column.name.includes("amount") || column.name.includes("total")) {
3572
- descriptions.push("Monetary value");
3573
- }
3574
- if (!column.nullable && !column.hasDefault) {
3575
- descriptions.push("(required)");
3576
- } else if (column.nullable) {
3577
- descriptions.push("(optional)");
3578
- }
3579
- return descriptions.join(" - ");
3580
- }
3581
- function postgresTypeToJsonType(pgType) {
3582
- switch (pgType) {
3583
- case "int":
3584
- case "integer":
3585
- case "smallint":
3586
- case "bigint":
3587
- case "decimal":
3588
- case "numeric":
3589
- case "real":
3590
- case "double precision":
3591
- case "float":
3592
- return "number";
3593
- case "boolean":
3594
- case "bool":
3595
- return "boolean";
3596
- case "date":
3597
- case "timestamp":
3598
- case "timestamptz":
3599
- return "date/datetime";
3600
- case "json":
3601
- case "jsonb":
3602
- return "object";
3603
- case "uuid":
3604
- return "uuid";
3605
- case "text[]":
3606
- case "varchar[]":
3607
- return "array<string>";
3608
- case "int[]":
3609
- case "integer[]":
3610
- return "array<number>";
3611
- default:
3612
- return "string";
3613
- }
3614
- }
3615
- function generateFilterParams(table) {
3616
- const filters = {};
3617
- for (const col of table.columns) {
3618
- const type = postgresTypeToJsonType(col.pgType);
3619
- filters[col.name] = `${type} - Filter by exact ${col.name} value`;
3620
- if (type === "number" || type === "date/datetime") {
3621
- filters[`${col.name}_gt`] = `${type} - Filter where ${col.name} is greater than`;
3622
- filters[`${col.name}_gte`] = `${type} - Filter where ${col.name} is greater than or equal`;
3623
- filters[`${col.name}_lt`] = `${type} - Filter where ${col.name} is less than`;
3624
- filters[`${col.name}_lte`] = `${type} - Filter where ${col.name} is less than or equal`;
3625
- }
3626
- if (type === "string") {
3627
- filters[`${col.name}_like`] = `string - Filter where ${col.name} contains text (case-insensitive)`;
3628
- }
3629
- }
3630
- return filters;
3631
- }
3632
- function getAuthDescription(strategy) {
3633
- switch (strategy) {
3634
- case "jwt":
3635
- return "JWT Bearer token authentication. Include token in Authorization header: 'Bearer <token>'";
3636
- case "apiKey":
3637
- return "API Key authentication. Include key in the configured header (e.g., 'x-api-key')";
3638
- default:
3639
- return "Custom authentication strategy";
3640
- }
3641
- }
3642
- function generateApiContractMarkdown(contract) {
3643
- const lines = [];
3644
- lines.push("# API Contract");
3645
- lines.push("");
3646
- lines.push(contract.description);
3647
- lines.push("");
3648
- lines.push(`**Version:** ${contract.version}`);
3649
- lines.push(`**Generated:** ${new Date(contract.generatedAt).toLocaleString()}`);
3650
- lines.push("");
3651
- if (contract.authentication) {
3652
- lines.push("## Authentication");
3653
- lines.push("");
3654
- lines.push(`**Type:** ${contract.authentication.type}`);
3655
- lines.push("");
3656
- lines.push(contract.authentication.description);
3657
- lines.push("");
3658
- }
3659
- lines.push("## Resources");
3660
- lines.push("");
3661
- for (const resource of contract.resources) {
3662
- lines.push(`### ${resource.name}`);
3663
- lines.push("");
3664
- lines.push(resource.description);
3665
- lines.push("");
3666
- lines.push("**Endpoints:**");
3667
- lines.push("");
3668
- for (const endpoint of resource.endpoints) {
3669
- lines.push(`- \`${endpoint.method} ${endpoint.path}\` - ${endpoint.description}`);
3670
- }
3671
- lines.push("");
3672
- lines.push("**Fields:**");
3673
- lines.push("");
3674
- for (const field of resource.fields) {
3675
- const required = field.required ? " *(required)*" : "";
3676
- const fk = field.foreignKey ? ` → ${field.foreignKey.table}` : "";
3677
- lines.push(`- \`${field.name}\` (${field.type})${required}${fk} - ${field.description}`);
3678
- }
3679
- lines.push("");
3680
- }
3681
- if (contract.relationships.length > 0) {
3682
- lines.push("## Relationships");
3683
- lines.push("");
3684
- for (const rel of contract.relationships) {
3685
- lines.push(`- **${rel.from}** → **${rel.to}** (${rel.type}): ${rel.description}`);
3686
- }
3687
- lines.push("");
3688
- }
3689
- return lines.join(`
3690
- `);
3691
- }
3692
- function emitApiContract(model, config) {
3693
- const contract = generateApiContract(model, config);
3694
- const contractJson = JSON.stringify(contract, null, 2);
3695
- return `/**
3696
- * API Contract
3697
- *
3698
- * This module exports the API contract that describes all available
3699
- * endpoints, resources, and their relationships.
3700
- */
3701
-
3702
- export const apiContract = ${contractJson};
3703
-
3704
- export const apiContractMarkdown = \`${generateApiContractMarkdown(contract).replace(/`/g, "\\`")}\`;
3705
-
3706
- /**
3707
- * Helper to get the contract in different formats
3708
- */
3709
- export function getApiContract(format: 'json' | 'markdown' = 'json') {
3710
- if (format === 'markdown') {
3711
- return apiContractMarkdown;
3712
- }
3713
- return apiContract;
3714
- }
3715
- `;
3716
- }
3717
-
3718
3434
  // src/index.ts
3719
3435
  init_emit_sdk_contract();
3720
3436
  init_utils();
@@ -3789,7 +3505,8 @@ async function generate(configPath) {
3789
3505
  join(serverDir, "zod"),
3790
3506
  join(serverDir, "routes"),
3791
3507
  clientDir,
3792
- join(clientDir, "types")
3508
+ join(clientDir, "types"),
3509
+ join(clientDir, "zod")
3793
3510
  ];
3794
3511
  if (generateTests) {
3795
3512
  dirs.push(testDir);
@@ -3823,10 +3540,9 @@ async function generate(configPath) {
3823
3540
  const typesSrc = emitTypes(table, { numericMode: "string" });
3824
3541
  files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
3825
3542
  files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
3826
- files.push({
3827
- path: join(serverDir, "zod", `${table.name}.ts`),
3828
- content: emitZod(table, { numericMode: "string" })
3829
- });
3543
+ const zodSrc = emitZod(table, { numericMode: "string" });
3544
+ files.push({ path: join(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
3545
+ files.push({ path: join(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
3830
3546
  let routeContent;
3831
3547
  if (serverFramework === "hono") {
3832
3548
  routeContent = emitHonoRoutes(table, graph, {
@@ -3864,10 +3580,6 @@ async function generate(configPath) {
3864
3580
  path: join(serverDir, "sdk-bundle.ts"),
3865
3581
  content: emitSdkBundle(clientFiles, clientDir)
3866
3582
  });
3867
- files.push({
3868
- path: join(serverDir, "api-contract.ts"),
3869
- content: emitApiContract(model, cfg)
3870
- });
3871
3583
  const contractCode = emitUnifiedContract(model, cfg);
3872
3584
  files.push({
3873
3585
  path: join(serverDir, "contract.ts"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.6.17",
3
+ "version": "0.7.1",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,6 +42,7 @@
42
42
  "zod": "^4.0.15"
43
43
  },
44
44
  "devDependencies": {
45
+ "@types/bun": "^1.2.20",
45
46
  "@types/node": "^20.0.0",
46
47
  "@types/pg": "^8.15.5",
47
48
  "drizzle-kit": "^0.31.4",