postgresdk 0.5.1-alpha.1 → 0.6.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.
package/README.md CHANGED
@@ -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: "./generated/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 generated/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 '../generated/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
@@ -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: "./generated/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)
@@ -2384,15 +2691,22 @@ 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 testDir = cfg.tests?.output || "./generated/tests";
2696
+ const testFramework = cfg.tests?.framework || "vitest";
2387
2697
  console.log("\uD83D\uDCC1 Creating directories...");
2388
- await ensureDirs([
2698
+ const dirs = [
2389
2699
  serverDir,
2390
2700
  join(serverDir, "types"),
2391
2701
  join(serverDir, "zod"),
2392
2702
  join(serverDir, "routes"),
2393
2703
  clientDir,
2394
2704
  join(clientDir, "types")
2395
- ]);
2705
+ ];
2706
+ if (generateTests) {
2707
+ dirs.push(testDir);
2708
+ }
2709
+ await ensureDirs(dirs);
2396
2710
  const files = [];
2397
2711
  const includeSpec = emitIncludeSpec(graph);
2398
2712
  files.push({ path: join(serverDir, "include-spec.ts"), content: includeSpec });
@@ -2459,11 +2773,37 @@ async function generate(configPath) {
2459
2773
  path: join(serverDir, "sdk-bundle.ts"),
2460
2774
  content: emitSdkBundle(clientFiles, clientDir)
2461
2775
  });
2776
+ if (generateTests) {
2777
+ console.log("\uD83E\uDDEA Generating tests...");
2778
+ files.push({
2779
+ path: join(testDir, "setup.ts"),
2780
+ content: emitTestSetup(testFramework)
2781
+ });
2782
+ files.push({
2783
+ path: join(testDir, "docker-compose.yml"),
2784
+ content: emitDockerCompose()
2785
+ });
2786
+ files.push({
2787
+ path: join(testDir, "run-tests.sh"),
2788
+ content: emitTestScript(testFramework)
2789
+ });
2790
+ for (const table of Object.values(model.tables)) {
2791
+ files.push({
2792
+ path: join(testDir, `${table.name}.test.ts`),
2793
+ content: emitTableTest(table, testFramework)
2794
+ });
2795
+ }
2796
+ }
2462
2797
  console.log("✍️ Writing files...");
2463
2798
  await writeFiles(files);
2464
2799
  console.log(`✅ Generated ${files.length} files`);
2465
2800
  console.log(` Server: ${serverDir}`);
2466
2801
  console.log(` Client: ${sameDirectory ? clientDir + " (in sdk subdir due to same output dir)" : clientDir}`);
2802
+ if (generateTests) {
2803
+ console.log(` Tests: ${testDir}`);
2804
+ console.log(` \uD83D\uDC33 Run 'cd ${testDir} && docker-compose up -d' to start test database`);
2805
+ console.log(` \uD83E\uDDEA Run 'bash ${testDir}/run-tests.sh' to execute tests`);
2806
+ }
2467
2807
  }
2468
2808
 
2469
2809
  // 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)
@@ -2126,15 +2421,22 @@ 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 testDir = cfg.tests?.output || "./generated/tests";
2426
+ const testFramework = cfg.tests?.framework || "vitest";
2129
2427
  console.log("\uD83D\uDCC1 Creating directories...");
2130
- await ensureDirs([
2428
+ const dirs = [
2131
2429
  serverDir,
2132
2430
  join(serverDir, "types"),
2133
2431
  join(serverDir, "zod"),
2134
2432
  join(serverDir, "routes"),
2135
2433
  clientDir,
2136
2434
  join(clientDir, "types")
2137
- ]);
2435
+ ];
2436
+ if (generateTests) {
2437
+ dirs.push(testDir);
2438
+ }
2439
+ await ensureDirs(dirs);
2138
2440
  const files = [];
2139
2441
  const includeSpec = emitIncludeSpec(graph);
2140
2442
  files.push({ path: join(serverDir, "include-spec.ts"), content: includeSpec });
@@ -2201,11 +2503,37 @@ async function generate(configPath) {
2201
2503
  path: join(serverDir, "sdk-bundle.ts"),
2202
2504
  content: emitSdkBundle(clientFiles, clientDir)
2203
2505
  });
2506
+ if (generateTests) {
2507
+ console.log("\uD83E\uDDEA Generating tests...");
2508
+ files.push({
2509
+ path: join(testDir, "setup.ts"),
2510
+ content: emitTestSetup(testFramework)
2511
+ });
2512
+ files.push({
2513
+ path: join(testDir, "docker-compose.yml"),
2514
+ content: emitDockerCompose()
2515
+ });
2516
+ files.push({
2517
+ path: join(testDir, "run-tests.sh"),
2518
+ content: emitTestScript(testFramework)
2519
+ });
2520
+ for (const table of Object.values(model.tables)) {
2521
+ files.push({
2522
+ path: join(testDir, `${table.name}.test.ts`),
2523
+ content: emitTableTest(table, testFramework)
2524
+ });
2525
+ }
2526
+ }
2204
2527
  console.log("✍️ Writing files...");
2205
2528
  await writeFiles(files);
2206
2529
  console.log(`✅ Generated ${files.length} files`);
2207
2530
  console.log(` Server: ${serverDir}`);
2208
2531
  console.log(` Client: ${sameDirectory ? clientDir + " (in sdk subdir due to same output dir)" : clientDir}`);
2532
+ if (generateTests) {
2533
+ console.log(` Tests: ${testDir}`);
2534
+ console.log(` \uD83D\uDC33 Run 'cd ${testDir} && docker-compose up -d' to start test database`);
2535
+ console.log(` \uD83E\uDDEA Run 'bash ${testDir}/run-tests.sh' to execute tests`);
2536
+ }
2209
2537
  }
2210
2538
  export {
2211
2539
  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.1",
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",