postgresdk 0.5.1-alpha.1 → 0.6.2

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/README.md CHANGED
@@ -43,7 +43,7 @@ Get a complete, type-safe SDK with:
43
43
  ### 🎯 Client SDK with Full TypeScript Support
44
44
 
45
45
  ```typescript
46
- import { SDK } from "./generated/client";
46
+ import { SDK } from "./api/client";
47
47
 
48
48
  const sdk = new SDK({
49
49
  baseUrl: "http://localhost:3000",
@@ -89,7 +89,7 @@ const recentBooks = await sdk.books.list({
89
89
  ```typescript
90
90
  import { Hono } from "hono";
91
91
  import { Client } from "pg";
92
- import { createRouter } from "./generated/server/router";
92
+ import { createRouter } from "./api/server/router";
93
93
 
94
94
  const app = new Hono();
95
95
  const pg = new Client({ connectionString: process.env.DATABASE_URL });
@@ -124,7 +124,7 @@ const book = await sdk.books.create({
124
124
  });
125
125
 
126
126
  // Generated Zod schemas for runtime validation
127
- import { InsertBooksSchema } from "./generated/server/zod/books";
127
+ import { InsertBooksSchema } from "./api/server/zod/books";
128
128
 
129
129
  const validated = InsertBooksSchema.parse(requestBody);
130
130
  ```
@@ -182,7 +182,7 @@ postgresdk generate
182
182
  // Server (Hono)
183
183
  import { Hono } from "hono";
184
184
  import { Client } from "pg";
185
- import { registerUsersRoutes } from "./generated/server/routes/users";
185
+ import { registerUsersRoutes } from "./api/server/routes/users";
186
186
 
187
187
  const app = new Hono();
188
188
  const pg = new Client({ connectionString: "..." });
@@ -191,7 +191,7 @@ await pg.connect();
191
191
  registerUsersRoutes(app, { pg });
192
192
 
193
193
  // Client
194
- import { SDK } from "./generated/client";
194
+ import { SDK } from "./api/client";
195
195
 
196
196
  const sdk = new SDK({ baseUrl: "http://localhost:3000" });
197
197
 
@@ -213,8 +213,8 @@ export default {
213
213
 
214
214
  // Optional (with defaults)
215
215
  schema: "public", // Database schema to introspect
216
- outServer: "./generated/server", // Server code output directory
217
- outClient: "./generated/client", // Client SDK output directory
216
+ outServer: "./api/server", // Server code output directory
217
+ outClient: "./api/client", // Client SDK output directory
218
218
  softDeleteColumn: null, // Column name for soft deletes (e.g., "deleted_at")
219
219
  includeDepthLimit: 3, // Max depth for nested includes
220
220
  dateType: "date", // "date" | "string" - How to handle timestamps
@@ -507,7 +507,7 @@ Server setup:
507
507
  ```typescript
508
508
  import { Hono } from "hono";
509
509
  import { Client } from "pg";
510
- import { createRouter } from "./generated/server/router";
510
+ import { createRouter } from "./api/server/router";
511
511
 
512
512
  const app = new Hono();
513
513
 
@@ -540,7 +540,7 @@ Server setup:
540
540
  ```typescript
541
541
  import { Hono } from "hono";
542
542
  import { Pool } from "@neondatabase/serverless";
543
- import { createRouter } from "./generated/server/router";
543
+ import { createRouter } from "./api/server/router";
544
544
 
545
545
  const app = new Hono();
546
546
 
@@ -577,7 +577,7 @@ For production Node.js deployments, use connection pooling:
577
577
 
578
578
  ```typescript
579
579
  import { Pool } from "pg";
580
- import { createRouter } from "./generated/server/router";
580
+ import { createRouter } from "./api/server/router";
581
581
 
582
582
  const pool = new Pool({
583
583
  connectionString: process.env.DATABASE_URL,
@@ -627,7 +627,7 @@ postgresdk generates a `createRouter` function that returns a Hono router with a
627
627
  import { Hono } from "hono";
628
628
  import { serve } from "@hono/node-server";
629
629
  import { Client } from "pg";
630
- import { createRouter } from "./generated/server/router";
630
+ import { createRouter } from "./api/server/router";
631
631
 
632
632
  const app = new Hono();
633
633
 
@@ -650,7 +650,7 @@ The `createRouter` function returns a Hono router that can be mounted anywhere:
650
650
 
651
651
  ```typescript
652
652
  import { Hono } from "hono";
653
- import { createRouter } from "./generated/server/router";
653
+ import { createRouter } from "./api/server/router";
654
654
 
655
655
  const app = new Hono();
656
656
 
@@ -671,7 +671,7 @@ app.route("/v2", apiRouter); // Routes will be at /v2/v1/users, /v2/v1/posts,
671
671
  If you prefer to register routes directly on your app without a sub-router:
672
672
 
673
673
  ```typescript
674
- import { registerAllRoutes } from "./generated/server/router";
674
+ import { registerAllRoutes } from "./api/server/router";
675
675
 
676
676
  const app = new Hono();
677
677
  const pg = new Client({ connectionString: process.env.DATABASE_URL });
@@ -686,7 +686,7 @@ registerAllRoutes(app, { pg });
686
686
  You can also import and register individual routes:
687
687
 
688
688
  ```typescript
689
- import { registerUsersRoutes, registerPostsRoutes } from "./generated/server/router";
689
+ import { registerUsersRoutes, registerPostsRoutes } from "./api/server/router";
690
690
 
691
691
  const app = new Hono();
692
692
 
@@ -717,8 +717,8 @@ const pg = new Client({ connectionString: process.env.DATABASE_URL });
717
717
  await pg.connect();
718
718
 
719
719
  // All generated routes are prefixed with /v1 by default
720
- import { registerUsersRoutes } from "./generated/server/routes/users";
721
- import { registerPostsRoutes } from "./generated/server/routes/posts";
720
+ import { registerUsersRoutes } from "./api/server/routes/users";
721
+ import { registerPostsRoutes } from "./api/server/routes/posts";
722
722
 
723
723
  registerUsersRoutes(app, { pg }); // Adds /v1/users/*
724
724
  registerPostsRoutes(app, { pg }); // Adds /v1/posts/*
@@ -756,7 +756,7 @@ app.use("*", async (c, next) => {
756
756
  const pg = new Client({ connectionString: process.env.DATABASE_URL });
757
757
  await pg.connect();
758
758
 
759
- import { registerUsersRoutes } from "./generated/server/routes/users";
759
+ import { registerUsersRoutes } from "./api/server/routes/users";
760
760
  registerUsersRoutes(app, { pg });
761
761
  ```
762
762
 
@@ -779,8 +779,8 @@ const pool = new Pool({
779
779
  const app = new Hono();
780
780
 
781
781
  // The generated routes work with both Client and Pool
782
- import { registerUsersRoutes } from "./generated/server/routes/users";
783
- import { registerPostsRoutes } from "./generated/server/routes/posts";
782
+ import { registerUsersRoutes } from "./api/server/routes/users";
783
+ import { registerPostsRoutes } from "./api/server/routes/posts";
784
784
 
785
785
  registerUsersRoutes(app, { pg: pool });
786
786
  registerPostsRoutes(app, { pg: pool });
@@ -831,9 +831,9 @@ import { serve } from "@hono/node-server";
831
831
  import { Pool } from "pg";
832
832
 
833
833
  // Import all generated route registrations
834
- import { registerUsersRoutes } from "./generated/server/routes/users";
835
- import { registerPostsRoutes } from "./generated/server/routes/posts";
836
- import { registerCommentsRoutes } from "./generated/server/routes/comments";
834
+ import { registerUsersRoutes } from "./api/server/routes/users";
835
+ import { registerPostsRoutes } from "./api/server/routes/posts";
836
+ import { registerCommentsRoutes } from "./api/server/routes/comments";
837
837
 
838
838
  // Create app with type safety
839
839
  const app = new Hono();
@@ -974,6 +974,104 @@ The pulled SDK includes metadata about when it was generated and from where:
974
974
  }
975
975
  ```
976
976
 
977
+ ## Generated Tests
978
+
979
+ postgresdk can generate basic SDK tests to help you get started quickly. These tests demonstrate CRUD operations for each table and include Docker setup for easy testing.
980
+
981
+ ### Enabling Test Generation
982
+
983
+ Add test configuration to your `postgresdk.config.ts`:
984
+
985
+ ```typescript
986
+ export default {
987
+ connectionString: process.env.DATABASE_URL,
988
+
989
+ tests: {
990
+ generate: true, // Enable test generation
991
+ output: "./api/tests", // Output directory
992
+ framework: "vitest" // Test framework (vitest, jest, or bun)
993
+ }
994
+ };
995
+ ```
996
+
997
+ ### What Gets Generated
998
+
999
+ When tests are enabled, postgresdk generates:
1000
+
1001
+ 1. **Test files for each table** - Basic CRUD operation tests
1002
+ 2. **setup.ts** - Common test utilities and helpers
1003
+ 3. **docker-compose.yml** - PostgreSQL test database configuration
1004
+ 4. **run-tests.sh** - Script to run tests with Docker
1005
+
1006
+ ### Running Tests with Docker
1007
+
1008
+ The generated Docker setup makes it easy to run tests in isolation:
1009
+
1010
+ ```bash
1011
+ # Navigate to test directory
1012
+ cd api/tests
1013
+
1014
+ # Start test database
1015
+ docker-compose up -d
1016
+
1017
+ # Wait for database to be ready
1018
+ sleep 3
1019
+
1020
+ # Set environment variables
1021
+ export TEST_DATABASE_URL="postgres://testuser:testpass@localhost:5432/testdb"
1022
+ export TEST_API_URL="http://localhost:3000"
1023
+
1024
+ # Run your migrations on test database
1025
+ # (your migration command here)
1026
+
1027
+ # Start your API server
1028
+ npm run dev &
1029
+
1030
+ # Run tests
1031
+ npm test
1032
+
1033
+ # Stop database when done
1034
+ docker-compose down
1035
+
1036
+ # Or use the generated script
1037
+ bash run-tests.sh
1038
+ ```
1039
+
1040
+ ### Customizing Tests
1041
+
1042
+ The generated tests are basic and meant as a starting point. Create your own test files for:
1043
+
1044
+ - Business logic validation
1045
+ - Complex query scenarios
1046
+ - Edge cases and error handling
1047
+ - Performance testing
1048
+ - Integration workflows
1049
+
1050
+ Example custom test:
1051
+
1052
+ ```typescript
1053
+ // tests/custom/user-workflow.test.ts
1054
+ import { describe, it, expect } from 'vitest';
1055
+ import { createTestSDK, randomEmail } from '../api/tests/setup';
1056
+
1057
+ describe('User Registration Workflow', () => {
1058
+ const sdk = createTestSDK();
1059
+
1060
+ it('should handle complete registration flow', async () => {
1061
+ // Your custom business logic tests
1062
+ const user = await sdk.users.create({
1063
+ email: randomEmail(),
1064
+ name: 'Test User'
1065
+ });
1066
+
1067
+ // Verify welcome email was sent
1068
+ // Check audit logs
1069
+ // Validate permissions
1070
+ // etc.
1071
+ });
1072
+ });
1073
+ ```
1074
+
977
1075
  ## CLI Commands
978
1076
 
979
1077
  ```bash
package/dist/cli.js CHANGED
@@ -535,15 +535,15 @@ export default {
535
535
 
536
536
  /**
537
537
  * Output directory for server-side code (routes, validators, etc.)
538
- * @default "./generated/server"
538
+ * @default "./api/server"
539
539
  */
540
- // outServer: "./generated/server",
540
+ // outServer: "./api/server",
541
541
 
542
542
  /**
543
543
  * Output directory for client SDK
544
- * @default "./generated/client"
544
+ * @default "./api/client"
545
545
  */
546
- // outClient: "./generated/client",
546
+ // outClient: "./api/client",
547
547
 
548
548
  // ========== ADVANCED OPTIONS ==========
549
549
 
@@ -590,6 +590,18 @@ export default {
590
590
  */
591
591
  // useJsExtensionsClient: false,
592
592
 
593
+ // ========== TEST GENERATION ==========
594
+
595
+ /**
596
+ * Generate basic SDK tests
597
+ * Uncomment to enable test generation with Docker setup
598
+ */
599
+ // tests: {
600
+ // generate: true,
601
+ // output: "./api/tests",
602
+ // framework: "vitest" // or "jest" or "bun"
603
+ // },
604
+
593
605
  // ========== AUTHENTICATION ==========
594
606
 
595
607
  /**
@@ -2327,6 +2339,301 @@ export async function deleteRecord(
2327
2339
  }`;
2328
2340
  }
2329
2341
 
2342
+ // src/emit-tests.ts
2343
+ function emitTableTest(table, framework = "vitest") {
2344
+ const Type = pascal(table.name);
2345
+ const tableName = table.name;
2346
+ const imports = getFrameworkImports(framework);
2347
+ const sampleData = generateSampleData(table);
2348
+ const updateData = generateUpdateData(table);
2349
+ return `${imports}
2350
+ import { SDK } from '../client';
2351
+ import type { Insert${Type}, Update${Type}, Select${Type} } from '../client/types/${tableName}';
2352
+
2353
+ /**
2354
+ * Basic tests for ${tableName} table operations
2355
+ *
2356
+ * These tests demonstrate basic CRUD operations.
2357
+ * Add your own business logic tests in separate files.
2358
+ */
2359
+ describe('${Type} SDK Operations', () => {
2360
+ let sdk: SDK;
2361
+ let createdId: string;
2362
+
2363
+ beforeAll(() => {
2364
+ sdk = new SDK({
2365
+ baseUrl: process.env.API_URL || 'http://localhost:3000',
2366
+ auth: process.env.API_KEY ? { apiKey: process.env.API_KEY } : undefined
2367
+ });
2368
+ });
2369
+
2370
+ ${generateTestCases(table, sampleData, updateData)}
2371
+ });
2372
+ `;
2373
+ }
2374
+ function emitTestSetup(framework = "vitest") {
2375
+ return `/**
2376
+ * Test Setup and Utilities
2377
+ *
2378
+ * This file provides common test utilities and configuration.
2379
+ * Extend this with your own helpers as needed.
2380
+ */
2381
+
2382
+ ${getFrameworkImports(framework)}
2383
+
2384
+ // Test database connection
2385
+ export const TEST_DATABASE_URL = process.env.TEST_DATABASE_URL || process.env.DATABASE_URL;
2386
+
2387
+ // API configuration
2388
+ export const TEST_API_URL = process.env.TEST_API_URL || 'http://localhost:3000';
2389
+ export const TEST_API_KEY = process.env.TEST_API_KEY;
2390
+
2391
+ // Utility to create SDK instance
2392
+ export function createTestSDK() {
2393
+ const { SDK } = require('../client');
2394
+ return new SDK({
2395
+ baseUrl: TEST_API_URL,
2396
+ auth: TEST_API_KEY ? { apiKey: TEST_API_KEY } : undefined
2397
+ });
2398
+ }
2399
+
2400
+ // Utility to generate random test data
2401
+ export function randomString(prefix = 'test'): string {
2402
+ return \`\${prefix}_\${Date.now()}_\${Math.random().toString(36).substr(2, 9)}\`;
2403
+ }
2404
+
2405
+ export function randomEmail(): string {
2406
+ return \`\${randomString('user')}@example.com\`;
2407
+ }
2408
+
2409
+ export function randomInt(min = 1, max = 1000): number {
2410
+ return Math.floor(Math.random() * (max - min + 1)) + min;
2411
+ }
2412
+
2413
+ export function randomDate(): Date {
2414
+ const start = new Date(2020, 0, 1);
2415
+ const end = new Date();
2416
+ return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
2417
+ }
2418
+ `;
2419
+ }
2420
+ function emitDockerCompose() {
2421
+ return `# Docker Compose for Test Database
2422
+ #
2423
+ # Start: docker-compose up -d
2424
+ # Stop: docker-compose down
2425
+ # Reset: docker-compose down -v && docker-compose up -d
2426
+
2427
+ version: '3.8'
2428
+
2429
+ services:
2430
+ postgres:
2431
+ image: postgres:16-alpine
2432
+ environment:
2433
+ POSTGRES_USER: testuser
2434
+ POSTGRES_PASSWORD: testpass
2435
+ POSTGRES_DB: testdb
2436
+ ports:
2437
+ - "5432:5432"
2438
+ volumes:
2439
+ - postgres_data:/var/lib/postgresql/data
2440
+ healthcheck:
2441
+ test: ["CMD-SHELL", "pg_isready -U testuser"]
2442
+ interval: 5s
2443
+ timeout: 5s
2444
+ retries: 5
2445
+
2446
+ volumes:
2447
+ postgres_data:
2448
+ `;
2449
+ }
2450
+ function emitTestScript(framework = "vitest") {
2451
+ const runCommand = framework === "bun" ? "bun test" : framework;
2452
+ return `#!/bin/bash
2453
+ # Test Runner Script
2454
+ #
2455
+ # This script sets up and runs tests with a Docker PostgreSQL database
2456
+
2457
+ set -e
2458
+
2459
+ echo "\uD83D\uDC33 Starting test database..."
2460
+ docker-compose up -d --wait
2461
+
2462
+ # Export test database URL
2463
+ export TEST_DATABASE_URL="postgres://testuser:testpass@localhost:5432/testdb"
2464
+ export TEST_API_URL="http://localhost:3000"
2465
+
2466
+ # Wait for database to be ready
2467
+ echo "⏳ Waiting for database..."
2468
+ sleep 2
2469
+
2470
+ # Run migrations if needed (customize this)
2471
+ # npm run migrate
2472
+
2473
+ echo "\uD83D\uDE80 Starting API server..."
2474
+ # Start your API server in the background
2475
+ # npm run dev &
2476
+ # SERVER_PID=$!
2477
+ # sleep 3
2478
+
2479
+ echo "\uD83E\uDDEA Running tests..."
2480
+ ${runCommand} $@
2481
+
2482
+ # Cleanup
2483
+ # kill $SERVER_PID 2>/dev/null || true
2484
+
2485
+ echo "✅ Tests completed!"
2486
+ `;
2487
+ }
2488
+ function getFrameworkImports(framework) {
2489
+ switch (framework) {
2490
+ case "vitest":
2491
+ return "import { describe, it, expect, beforeAll, afterAll } from 'vitest';";
2492
+ case "jest":
2493
+ return "// Jest is configured globally, no imports needed";
2494
+ case "bun":
2495
+ return "import { describe, it, expect, beforeAll, afterAll } from 'bun:test';";
2496
+ default:
2497
+ return "import { describe, it, expect, beforeAll, afterAll } from 'vitest';";
2498
+ }
2499
+ }
2500
+ function generateSampleData(table) {
2501
+ const fields = [];
2502
+ for (const col of table.columns) {
2503
+ if (col.hasDefault || col.name === "id" || col.name === "created_at" || col.name === "updated_at") {
2504
+ continue;
2505
+ }
2506
+ if (col.nullable) {
2507
+ continue;
2508
+ }
2509
+ const value = getSampleValue(col.pgType, col.name);
2510
+ fields.push(` ${col.name}: ${value}`);
2511
+ }
2512
+ return fields.length > 0 ? `{
2513
+ ${fields.join(`,
2514
+ `)}
2515
+ }` : "{}";
2516
+ }
2517
+ function generateUpdateData(table) {
2518
+ const fields = [];
2519
+ for (const col of table.columns) {
2520
+ if (col.hasDefault || col.name === "id" || col.name === "created_at" || col.name === "updated_at") {
2521
+ continue;
2522
+ }
2523
+ if (!col.nullable && fields.length === 0) {
2524
+ const value = getSampleValue(col.pgType, col.name, true);
2525
+ fields.push(` ${col.name}: ${value}`);
2526
+ break;
2527
+ }
2528
+ }
2529
+ return fields.length > 0 ? `{
2530
+ ${fields.join(`,
2531
+ `)}
2532
+ }` : "{}";
2533
+ }
2534
+ function getSampleValue(type, name, isUpdate = false) {
2535
+ const suffix = isUpdate ? ' + " (updated)"' : "";
2536
+ if (name.includes("email")) {
2537
+ return `'test${isUpdate ? ".updated" : ""}@example.com'`;
2538
+ }
2539
+ if (name.includes("name") || name.includes("title")) {
2540
+ return `'Test ${pascal(name)}'${suffix}`;
2541
+ }
2542
+ if (name.includes("description") || name.includes("bio") || name.includes("content")) {
2543
+ return `'Test description'${suffix}`;
2544
+ }
2545
+ switch (type) {
2546
+ case "text":
2547
+ case "varchar":
2548
+ case "char":
2549
+ return `'test_value'${suffix}`;
2550
+ case "int":
2551
+ case "integer":
2552
+ case "smallint":
2553
+ case "bigint":
2554
+ return isUpdate ? "42" : "1";
2555
+ case "decimal":
2556
+ case "numeric":
2557
+ case "real":
2558
+ case "double precision":
2559
+ case "float":
2560
+ return isUpdate ? "99.99" : "10.50";
2561
+ case "boolean":
2562
+ case "bool":
2563
+ return isUpdate ? "false" : "true";
2564
+ case "date":
2565
+ return `'2024-01-01'`;
2566
+ case "timestamp":
2567
+ case "timestamptz":
2568
+ return `new Date().toISOString()`;
2569
+ case "json":
2570
+ case "jsonb":
2571
+ return `{ key: 'value' }`;
2572
+ case "uuid":
2573
+ return `'${isUpdate ? "b" : "a"}0e0e0e0-e0e0-e0e0-e0e0-e0e0e0e0e0e0'`;
2574
+ default:
2575
+ return `'test'`;
2576
+ }
2577
+ }
2578
+ function generateTestCases(table, sampleData, updateData) {
2579
+ const Type = pascal(table.name);
2580
+ const hasData = sampleData !== "{}";
2581
+ return `it('should create a ${table.name}', async () => {
2582
+ const data: Insert${Type} = ${sampleData};
2583
+ ${hasData ? `
2584
+ const created = await sdk.${table.name}.create(data);
2585
+ expect(created).toBeDefined();
2586
+ expect(created.id).toBeDefined();
2587
+ createdId = created.id;
2588
+ ` : `
2589
+ // Table has only auto-generated columns
2590
+ // Skip create test or add your own test data
2591
+ expect(true).toBe(true);
2592
+ `}
2593
+ });
2594
+
2595
+ it('should list ${table.name}', async () => {
2596
+ const list = await sdk.${table.name}.list({ limit: 10 });
2597
+ expect(Array.isArray(list)).toBe(true);
2598
+ });
2599
+
2600
+ ${hasData ? `it('should get ${table.name} by id', async () => {
2601
+ if (!createdId) {
2602
+ console.warn('No ID from create test, skipping get test');
2603
+ return;
2604
+ }
2605
+
2606
+ const item = await sdk.${table.name}.getByPk(createdId);
2607
+ expect(item).toBeDefined();
2608
+ expect(item?.id).toBe(createdId);
2609
+ });
2610
+
2611
+ ${updateData !== "{}" ? `it('should update ${table.name}', async () => {
2612
+ if (!createdId) {
2613
+ console.warn('No ID from create test, skipping update test');
2614
+ return;
2615
+ }
2616
+
2617
+ const updateData: Update${Type} = ${updateData};
2618
+ const updated = await sdk.${table.name}.update(createdId, updateData);
2619
+ expect(updated).toBeDefined();
2620
+ });` : ""}
2621
+
2622
+ it('should delete ${table.name}', async () => {
2623
+ if (!createdId) {
2624
+ console.warn('No ID from create test, skipping delete test');
2625
+ return;
2626
+ }
2627
+
2628
+ const deleted = await sdk.${table.name}.delete(createdId);
2629
+ expect(deleted).toBeDefined();
2630
+
2631
+ // Verify deletion
2632
+ const item = await sdk.${table.name}.getByPk(createdId);
2633
+ expect(item).toBeNull();
2634
+ });` : ""}`;
2635
+ }
2636
+
2330
2637
  // src/types.ts
2331
2638
  function normalizeAuthConfig(input) {
2332
2639
  if (!input)
@@ -2375,8 +2682,8 @@ async function generate(configPath) {
2375
2682
  const model = await introspect(cfg.connectionString, cfg.schema || "public");
2376
2683
  console.log("\uD83D\uDD17 Building relationship graph...");
2377
2684
  const graph = buildGraph(model);
2378
- const serverDir = cfg.outServer || "./generated/server";
2379
- const originalClientDir = cfg.outClient || "./generated/client";
2685
+ const serverDir = cfg.outServer || "./api/server";
2686
+ const originalClientDir = cfg.outClient || "./api/client";
2380
2687
  const sameDirectory = serverDir === originalClientDir;
2381
2688
  let clientDir = originalClientDir;
2382
2689
  if (sameDirectory) {
@@ -2384,15 +2691,26 @@ async function generate(configPath) {
2384
2691
  }
2385
2692
  const normDateType = cfg.dateType === "string" ? "string" : "date";
2386
2693
  const serverFramework = cfg.serverFramework || "hono";
2694
+ const generateTests = cfg.tests?.generate ?? false;
2695
+ const originalTestDir = cfg.tests?.output || "./api/tests";
2696
+ let testDir = originalTestDir;
2697
+ if (generateTests && (originalTestDir === serverDir || originalTestDir === originalClientDir)) {
2698
+ testDir = join(originalTestDir, "tests");
2699
+ }
2700
+ const testFramework = cfg.tests?.framework || "vitest";
2387
2701
  console.log("\uD83D\uDCC1 Creating directories...");
2388
- await ensureDirs([
2702
+ const dirs = [
2389
2703
  serverDir,
2390
2704
  join(serverDir, "types"),
2391
2705
  join(serverDir, "zod"),
2392
2706
  join(serverDir, "routes"),
2393
2707
  clientDir,
2394
2708
  join(clientDir, "types")
2395
- ]);
2709
+ ];
2710
+ if (generateTests) {
2711
+ dirs.push(testDir);
2712
+ }
2713
+ await ensureDirs(dirs);
2396
2714
  const files = [];
2397
2715
  const includeSpec = emitIncludeSpec(graph);
2398
2716
  files.push({ path: join(serverDir, "include-spec.ts"), content: includeSpec });
@@ -2459,11 +2777,38 @@ async function generate(configPath) {
2459
2777
  path: join(serverDir, "sdk-bundle.ts"),
2460
2778
  content: emitSdkBundle(clientFiles, clientDir)
2461
2779
  });
2780
+ if (generateTests) {
2781
+ console.log("\uD83E\uDDEA Generating tests...");
2782
+ files.push({
2783
+ path: join(testDir, "setup.ts"),
2784
+ content: emitTestSetup(testFramework)
2785
+ });
2786
+ files.push({
2787
+ path: join(testDir, "docker-compose.yml"),
2788
+ content: emitDockerCompose()
2789
+ });
2790
+ files.push({
2791
+ path: join(testDir, "run-tests.sh"),
2792
+ content: emitTestScript(testFramework)
2793
+ });
2794
+ for (const table of Object.values(model.tables)) {
2795
+ files.push({
2796
+ path: join(testDir, `${table.name}.test.ts`),
2797
+ content: emitTableTest(table, testFramework)
2798
+ });
2799
+ }
2800
+ }
2462
2801
  console.log("✍️ Writing files...");
2463
2802
  await writeFiles(files);
2464
2803
  console.log(`✅ Generated ${files.length} files`);
2465
2804
  console.log(` Server: ${serverDir}`);
2466
2805
  console.log(` Client: ${sameDirectory ? clientDir + " (in sdk subdir due to same output dir)" : clientDir}`);
2806
+ if (generateTests) {
2807
+ const testsInSubdir = originalTestDir === serverDir || originalTestDir === originalClientDir;
2808
+ console.log(` Tests: ${testsInSubdir ? testDir + " (in tests subdir due to same output dir)" : testDir}`);
2809
+ console.log(` \uD83D\uDC33 Run 'cd ${testDir} && docker-compose up -d' to start test database`);
2810
+ console.log(` \uD83E\uDDEA Run 'bash ${testDir}/run-tests.sh' to execute tests`);
2811
+ }
2467
2812
  }
2468
2813
 
2469
2814
  // src/cli.ts
@@ -0,0 +1,17 @@
1
+ import type { Table } from "./introspect";
2
+ /**
3
+ * Generate basic SDK tests for a table
4
+ */
5
+ export declare function emitTableTest(table: Table, framework?: "vitest" | "jest" | "bun"): string;
6
+ /**
7
+ * Generate a test setup file
8
+ */
9
+ export declare function emitTestSetup(framework?: "vitest" | "jest" | "bun"): string;
10
+ /**
11
+ * Generate docker-compose.yml for test database
12
+ */
13
+ export declare function emitDockerCompose(): string;
14
+ /**
15
+ * Generate test runner script
16
+ */
17
+ export declare function emitTestScript(framework?: "vitest" | "jest" | "bun"): string;
package/dist/index.js CHANGED
@@ -2069,6 +2069,301 @@ export async function deleteRecord(
2069
2069
  }`;
2070
2070
  }
2071
2071
 
2072
+ // src/emit-tests.ts
2073
+ function emitTableTest(table, framework = "vitest") {
2074
+ const Type = pascal(table.name);
2075
+ const tableName = table.name;
2076
+ const imports = getFrameworkImports(framework);
2077
+ const sampleData = generateSampleData(table);
2078
+ const updateData = generateUpdateData(table);
2079
+ return `${imports}
2080
+ import { SDK } from '../client';
2081
+ import type { Insert${Type}, Update${Type}, Select${Type} } from '../client/types/${tableName}';
2082
+
2083
+ /**
2084
+ * Basic tests for ${tableName} table operations
2085
+ *
2086
+ * These tests demonstrate basic CRUD operations.
2087
+ * Add your own business logic tests in separate files.
2088
+ */
2089
+ describe('${Type} SDK Operations', () => {
2090
+ let sdk: SDK;
2091
+ let createdId: string;
2092
+
2093
+ beforeAll(() => {
2094
+ sdk = new SDK({
2095
+ baseUrl: process.env.API_URL || 'http://localhost:3000',
2096
+ auth: process.env.API_KEY ? { apiKey: process.env.API_KEY } : undefined
2097
+ });
2098
+ });
2099
+
2100
+ ${generateTestCases(table, sampleData, updateData)}
2101
+ });
2102
+ `;
2103
+ }
2104
+ function emitTestSetup(framework = "vitest") {
2105
+ return `/**
2106
+ * Test Setup and Utilities
2107
+ *
2108
+ * This file provides common test utilities and configuration.
2109
+ * Extend this with your own helpers as needed.
2110
+ */
2111
+
2112
+ ${getFrameworkImports(framework)}
2113
+
2114
+ // Test database connection
2115
+ export const TEST_DATABASE_URL = process.env.TEST_DATABASE_URL || process.env.DATABASE_URL;
2116
+
2117
+ // API configuration
2118
+ export const TEST_API_URL = process.env.TEST_API_URL || 'http://localhost:3000';
2119
+ export const TEST_API_KEY = process.env.TEST_API_KEY;
2120
+
2121
+ // Utility to create SDK instance
2122
+ export function createTestSDK() {
2123
+ const { SDK } = require('../client');
2124
+ return new SDK({
2125
+ baseUrl: TEST_API_URL,
2126
+ auth: TEST_API_KEY ? { apiKey: TEST_API_KEY } : undefined
2127
+ });
2128
+ }
2129
+
2130
+ // Utility to generate random test data
2131
+ export function randomString(prefix = 'test'): string {
2132
+ return \`\${prefix}_\${Date.now()}_\${Math.random().toString(36).substr(2, 9)}\`;
2133
+ }
2134
+
2135
+ export function randomEmail(): string {
2136
+ return \`\${randomString('user')}@example.com\`;
2137
+ }
2138
+
2139
+ export function randomInt(min = 1, max = 1000): number {
2140
+ return Math.floor(Math.random() * (max - min + 1)) + min;
2141
+ }
2142
+
2143
+ export function randomDate(): Date {
2144
+ const start = new Date(2020, 0, 1);
2145
+ const end = new Date();
2146
+ return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
2147
+ }
2148
+ `;
2149
+ }
2150
+ function emitDockerCompose() {
2151
+ return `# Docker Compose for Test Database
2152
+ #
2153
+ # Start: docker-compose up -d
2154
+ # Stop: docker-compose down
2155
+ # Reset: docker-compose down -v && docker-compose up -d
2156
+
2157
+ version: '3.8'
2158
+
2159
+ services:
2160
+ postgres:
2161
+ image: postgres:16-alpine
2162
+ environment:
2163
+ POSTGRES_USER: testuser
2164
+ POSTGRES_PASSWORD: testpass
2165
+ POSTGRES_DB: testdb
2166
+ ports:
2167
+ - "5432:5432"
2168
+ volumes:
2169
+ - postgres_data:/var/lib/postgresql/data
2170
+ healthcheck:
2171
+ test: ["CMD-SHELL", "pg_isready -U testuser"]
2172
+ interval: 5s
2173
+ timeout: 5s
2174
+ retries: 5
2175
+
2176
+ volumes:
2177
+ postgres_data:
2178
+ `;
2179
+ }
2180
+ function emitTestScript(framework = "vitest") {
2181
+ const runCommand = framework === "bun" ? "bun test" : framework;
2182
+ return `#!/bin/bash
2183
+ # Test Runner Script
2184
+ #
2185
+ # This script sets up and runs tests with a Docker PostgreSQL database
2186
+
2187
+ set -e
2188
+
2189
+ echo "\uD83D\uDC33 Starting test database..."
2190
+ docker-compose up -d --wait
2191
+
2192
+ # Export test database URL
2193
+ export TEST_DATABASE_URL="postgres://testuser:testpass@localhost:5432/testdb"
2194
+ export TEST_API_URL="http://localhost:3000"
2195
+
2196
+ # Wait for database to be ready
2197
+ echo "⏳ Waiting for database..."
2198
+ sleep 2
2199
+
2200
+ # Run migrations if needed (customize this)
2201
+ # npm run migrate
2202
+
2203
+ echo "\uD83D\uDE80 Starting API server..."
2204
+ # Start your API server in the background
2205
+ # npm run dev &
2206
+ # SERVER_PID=$!
2207
+ # sleep 3
2208
+
2209
+ echo "\uD83E\uDDEA Running tests..."
2210
+ ${runCommand} $@
2211
+
2212
+ # Cleanup
2213
+ # kill $SERVER_PID 2>/dev/null || true
2214
+
2215
+ echo "✅ Tests completed!"
2216
+ `;
2217
+ }
2218
+ function getFrameworkImports(framework) {
2219
+ switch (framework) {
2220
+ case "vitest":
2221
+ return "import { describe, it, expect, beforeAll, afterAll } from 'vitest';";
2222
+ case "jest":
2223
+ return "// Jest is configured globally, no imports needed";
2224
+ case "bun":
2225
+ return "import { describe, it, expect, beforeAll, afterAll } from 'bun:test';";
2226
+ default:
2227
+ return "import { describe, it, expect, beforeAll, afterAll } from 'vitest';";
2228
+ }
2229
+ }
2230
+ function generateSampleData(table) {
2231
+ const fields = [];
2232
+ for (const col of table.columns) {
2233
+ if (col.hasDefault || col.name === "id" || col.name === "created_at" || col.name === "updated_at") {
2234
+ continue;
2235
+ }
2236
+ if (col.nullable) {
2237
+ continue;
2238
+ }
2239
+ const value = getSampleValue(col.pgType, col.name);
2240
+ fields.push(` ${col.name}: ${value}`);
2241
+ }
2242
+ return fields.length > 0 ? `{
2243
+ ${fields.join(`,
2244
+ `)}
2245
+ }` : "{}";
2246
+ }
2247
+ function generateUpdateData(table) {
2248
+ const fields = [];
2249
+ for (const col of table.columns) {
2250
+ if (col.hasDefault || col.name === "id" || col.name === "created_at" || col.name === "updated_at") {
2251
+ continue;
2252
+ }
2253
+ if (!col.nullable && fields.length === 0) {
2254
+ const value = getSampleValue(col.pgType, col.name, true);
2255
+ fields.push(` ${col.name}: ${value}`);
2256
+ break;
2257
+ }
2258
+ }
2259
+ return fields.length > 0 ? `{
2260
+ ${fields.join(`,
2261
+ `)}
2262
+ }` : "{}";
2263
+ }
2264
+ function getSampleValue(type, name, isUpdate = false) {
2265
+ const suffix = isUpdate ? ' + " (updated)"' : "";
2266
+ if (name.includes("email")) {
2267
+ return `'test${isUpdate ? ".updated" : ""}@example.com'`;
2268
+ }
2269
+ if (name.includes("name") || name.includes("title")) {
2270
+ return `'Test ${pascal(name)}'${suffix}`;
2271
+ }
2272
+ if (name.includes("description") || name.includes("bio") || name.includes("content")) {
2273
+ return `'Test description'${suffix}`;
2274
+ }
2275
+ switch (type) {
2276
+ case "text":
2277
+ case "varchar":
2278
+ case "char":
2279
+ return `'test_value'${suffix}`;
2280
+ case "int":
2281
+ case "integer":
2282
+ case "smallint":
2283
+ case "bigint":
2284
+ return isUpdate ? "42" : "1";
2285
+ case "decimal":
2286
+ case "numeric":
2287
+ case "real":
2288
+ case "double precision":
2289
+ case "float":
2290
+ return isUpdate ? "99.99" : "10.50";
2291
+ case "boolean":
2292
+ case "bool":
2293
+ return isUpdate ? "false" : "true";
2294
+ case "date":
2295
+ return `'2024-01-01'`;
2296
+ case "timestamp":
2297
+ case "timestamptz":
2298
+ return `new Date().toISOString()`;
2299
+ case "json":
2300
+ case "jsonb":
2301
+ return `{ key: 'value' }`;
2302
+ case "uuid":
2303
+ return `'${isUpdate ? "b" : "a"}0e0e0e0-e0e0-e0e0-e0e0-e0e0e0e0e0e0'`;
2304
+ default:
2305
+ return `'test'`;
2306
+ }
2307
+ }
2308
+ function generateTestCases(table, sampleData, updateData) {
2309
+ const Type = pascal(table.name);
2310
+ const hasData = sampleData !== "{}";
2311
+ return `it('should create a ${table.name}', async () => {
2312
+ const data: Insert${Type} = ${sampleData};
2313
+ ${hasData ? `
2314
+ const created = await sdk.${table.name}.create(data);
2315
+ expect(created).toBeDefined();
2316
+ expect(created.id).toBeDefined();
2317
+ createdId = created.id;
2318
+ ` : `
2319
+ // Table has only auto-generated columns
2320
+ // Skip create test or add your own test data
2321
+ expect(true).toBe(true);
2322
+ `}
2323
+ });
2324
+
2325
+ it('should list ${table.name}', async () => {
2326
+ const list = await sdk.${table.name}.list({ limit: 10 });
2327
+ expect(Array.isArray(list)).toBe(true);
2328
+ });
2329
+
2330
+ ${hasData ? `it('should get ${table.name} by id', async () => {
2331
+ if (!createdId) {
2332
+ console.warn('No ID from create test, skipping get test');
2333
+ return;
2334
+ }
2335
+
2336
+ const item = await sdk.${table.name}.getByPk(createdId);
2337
+ expect(item).toBeDefined();
2338
+ expect(item?.id).toBe(createdId);
2339
+ });
2340
+
2341
+ ${updateData !== "{}" ? `it('should update ${table.name}', async () => {
2342
+ if (!createdId) {
2343
+ console.warn('No ID from create test, skipping update test');
2344
+ return;
2345
+ }
2346
+
2347
+ const updateData: Update${Type} = ${updateData};
2348
+ const updated = await sdk.${table.name}.update(createdId, updateData);
2349
+ expect(updated).toBeDefined();
2350
+ });` : ""}
2351
+
2352
+ it('should delete ${table.name}', async () => {
2353
+ if (!createdId) {
2354
+ console.warn('No ID from create test, skipping delete test');
2355
+ return;
2356
+ }
2357
+
2358
+ const deleted = await sdk.${table.name}.delete(createdId);
2359
+ expect(deleted).toBeDefined();
2360
+
2361
+ // Verify deletion
2362
+ const item = await sdk.${table.name}.getByPk(createdId);
2363
+ expect(item).toBeNull();
2364
+ });` : ""}`;
2365
+ }
2366
+
2072
2367
  // src/types.ts
2073
2368
  function normalizeAuthConfig(input) {
2074
2369
  if (!input)
@@ -2117,8 +2412,8 @@ async function generate(configPath) {
2117
2412
  const model = await introspect(cfg.connectionString, cfg.schema || "public");
2118
2413
  console.log("\uD83D\uDD17 Building relationship graph...");
2119
2414
  const graph = buildGraph(model);
2120
- const serverDir = cfg.outServer || "./generated/server";
2121
- const originalClientDir = cfg.outClient || "./generated/client";
2415
+ const serverDir = cfg.outServer || "./api/server";
2416
+ const originalClientDir = cfg.outClient || "./api/client";
2122
2417
  const sameDirectory = serverDir === originalClientDir;
2123
2418
  let clientDir = originalClientDir;
2124
2419
  if (sameDirectory) {
@@ -2126,15 +2421,26 @@ async function generate(configPath) {
2126
2421
  }
2127
2422
  const normDateType = cfg.dateType === "string" ? "string" : "date";
2128
2423
  const serverFramework = cfg.serverFramework || "hono";
2424
+ const generateTests = cfg.tests?.generate ?? false;
2425
+ const originalTestDir = cfg.tests?.output || "./api/tests";
2426
+ let testDir = originalTestDir;
2427
+ if (generateTests && (originalTestDir === serverDir || originalTestDir === originalClientDir)) {
2428
+ testDir = join(originalTestDir, "tests");
2429
+ }
2430
+ const testFramework = cfg.tests?.framework || "vitest";
2129
2431
  console.log("\uD83D\uDCC1 Creating directories...");
2130
- await ensureDirs([
2432
+ const dirs = [
2131
2433
  serverDir,
2132
2434
  join(serverDir, "types"),
2133
2435
  join(serverDir, "zod"),
2134
2436
  join(serverDir, "routes"),
2135
2437
  clientDir,
2136
2438
  join(clientDir, "types")
2137
- ]);
2439
+ ];
2440
+ if (generateTests) {
2441
+ dirs.push(testDir);
2442
+ }
2443
+ await ensureDirs(dirs);
2138
2444
  const files = [];
2139
2445
  const includeSpec = emitIncludeSpec(graph);
2140
2446
  files.push({ path: join(serverDir, "include-spec.ts"), content: includeSpec });
@@ -2201,11 +2507,38 @@ async function generate(configPath) {
2201
2507
  path: join(serverDir, "sdk-bundle.ts"),
2202
2508
  content: emitSdkBundle(clientFiles, clientDir)
2203
2509
  });
2510
+ if (generateTests) {
2511
+ console.log("\uD83E\uDDEA Generating tests...");
2512
+ files.push({
2513
+ path: join(testDir, "setup.ts"),
2514
+ content: emitTestSetup(testFramework)
2515
+ });
2516
+ files.push({
2517
+ path: join(testDir, "docker-compose.yml"),
2518
+ content: emitDockerCompose()
2519
+ });
2520
+ files.push({
2521
+ path: join(testDir, "run-tests.sh"),
2522
+ content: emitTestScript(testFramework)
2523
+ });
2524
+ for (const table of Object.values(model.tables)) {
2525
+ files.push({
2526
+ path: join(testDir, `${table.name}.test.ts`),
2527
+ content: emitTableTest(table, testFramework)
2528
+ });
2529
+ }
2530
+ }
2204
2531
  console.log("✍️ Writing files...");
2205
2532
  await writeFiles(files);
2206
2533
  console.log(`✅ Generated ${files.length} files`);
2207
2534
  console.log(` Server: ${serverDir}`);
2208
2535
  console.log(` Client: ${sameDirectory ? clientDir + " (in sdk subdir due to same output dir)" : clientDir}`);
2536
+ if (generateTests) {
2537
+ const testsInSubdir = originalTestDir === serverDir || originalTestDir === originalClientDir;
2538
+ console.log(` Tests: ${testsInSubdir ? testDir + " (in tests subdir due to same output dir)" : testDir}`);
2539
+ console.log(` \uD83D\uDC33 Run 'cd ${testDir} && docker-compose up -d' to start test database`);
2540
+ console.log(` \uD83E\uDDEA Run 'bash ${testDir}/run-tests.sh' to execute tests`);
2541
+ }
2209
2542
  }
2210
2543
  export {
2211
2544
  generate
package/dist/types.d.ts CHANGED
@@ -31,6 +31,11 @@ export interface Config {
31
31
  pull?: PullConfig;
32
32
  useJsExtensions?: boolean;
33
33
  useJsExtensionsClient?: boolean;
34
+ tests?: {
35
+ generate?: boolean;
36
+ output?: string;
37
+ framework?: "vitest" | "jest" | "bun";
38
+ };
34
39
  }
35
40
  export interface PullConfig {
36
41
  from: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.5.1-alpha.1",
3
+ "version": "0.6.2",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,9 +22,10 @@
22
22
  },
23
23
  "scripts": {
24
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",
25
- "test": "bun test:init && bun test:gen && bun test:pull && bun test:typecheck",
25
+ "test": "bun test:init && bun test:gen && bun test:gen-with-tests && bun test:pull && bun test:typecheck",
26
26
  "test:init": "bun test/test-init.ts",
27
27
  "test:gen": "bun test/test-gen.ts",
28
+ "test:gen-with-tests": "bun src/cli.ts generate -c test/test-with-tests.config.ts",
28
29
  "test:pull": "bun test/test-pull.ts",
29
30
  "test:typecheck": "bun test/test-typecheck.ts",
30
31
  "prepublishOnly": "npm run build",