postgresdk 0.5.1-alpha.0 → 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
@@ -579,11 +579,29 @@ export default {
579
579
  // serverFramework: "hono",
580
580
 
581
581
  /**
582
- * Use .js extensions in imports (for Vercel Edge, Deno, etc.)
582
+ * Use .js extensions in server imports (for Vercel Edge, Deno, etc.)
583
583
  * @default false
584
584
  */
585
585
  // useJsExtensions: false,
586
586
 
587
+ /**
588
+ * Use .js extensions in client SDK imports (rarely needed)
589
+ * @default false
590
+ */
591
+ // useJsExtensionsClient: false,
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
+
587
605
  // ========== AUTHENTICATION ==========
588
606
 
589
607
  /**
@@ -1185,17 +1203,18 @@ ${hasAuth ? `
1185
1203
  }
1186
1204
 
1187
1205
  // src/emit-client.ts
1188
- function emitClient(table) {
1206
+ function emitClient(table, useJsExtensions) {
1189
1207
  const Type = pascal(table.name);
1208
+ const ext = useJsExtensions ? ".js" : "";
1190
1209
  const pkCols = Array.isArray(table.pk) ? table.pk : table.pk ? [table.pk] : [];
1191
1210
  const safePk = pkCols.length ? pkCols : ["id"];
1192
1211
  const hasCompositePk = safePk.length > 1;
1193
1212
  const pkType = hasCompositePk ? `{ ${safePk.map((c) => `${c}: string`).join("; ")} }` : `string`;
1194
1213
  const pkPathExpr = hasCompositePk ? safePk.map((c) => `pk.${c}`).join(` + "/" + `) : `pk`;
1195
1214
  return `/* Generated. Do not edit. */
1196
- import { BaseClient } from "./base-client";
1197
- import type { ${Type}IncludeSpec } from "./include-spec";
1198
- import type { Insert${Type}, Update${Type}, Select${Type} } from "./types/${table.name}";
1215
+ import { BaseClient } from "./base-client${ext}";
1216
+ import type { ${Type}IncludeSpec } from "./include-spec${ext}";
1217
+ import type { Insert${Type}, Update${Type}, Select${Type} } from "./types/${table.name}${ext}";
1199
1218
 
1200
1219
  /**
1201
1220
  * Client for ${table.name} table operations
@@ -1235,19 +1254,20 @@ export class ${Type}Client extends BaseClient {
1235
1254
  }
1236
1255
  `;
1237
1256
  }
1238
- function emitClientIndex(tables) {
1257
+ function emitClientIndex(tables, useJsExtensions) {
1258
+ const ext = useJsExtensions ? ".js" : "";
1239
1259
  let out = `/* Generated. Do not edit. */
1240
1260
  `;
1241
- out += `import { BaseClient, type AuthConfig } from "./base-client";
1261
+ out += `import { BaseClient, type AuthConfig } from "./base-client${ext}";
1242
1262
  `;
1243
1263
  for (const t of tables) {
1244
- out += `import { ${pascal(t.name)}Client } from "./${t.name}";
1264
+ out += `import { ${pascal(t.name)}Client } from "./${t.name}${ext}";
1245
1265
  `;
1246
1266
  }
1247
1267
  out += `
1248
1268
  // Re-export auth types for convenience
1249
1269
  `;
1250
- out += `export type { AuthConfig as SDKAuth, AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client";
1270
+ out += `export type { AuthConfig as SDKAuth, AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${ext}";
1251
1271
 
1252
1272
  `;
1253
1273
  out += `/**
@@ -1279,18 +1299,18 @@ function emitClientIndex(tables) {
1279
1299
  out += `// Export individual table clients
1280
1300
  `;
1281
1301
  for (const t of tables) {
1282
- out += `export { ${pascal(t.name)}Client } from "./${t.name}";
1302
+ out += `export { ${pascal(t.name)}Client } from "./${t.name}${ext}";
1283
1303
  `;
1284
1304
  }
1285
1305
  out += `
1286
1306
  // Export base client for custom extensions
1287
1307
  `;
1288
- out += `export { BaseClient } from "./base-client";
1308
+ out += `export { BaseClient } from "./base-client${ext}";
1289
1309
  `;
1290
1310
  out += `
1291
1311
  // Export include specifications
1292
1312
  `;
1293
- out += `export * from "./include-spec";
1313
+ out += `export * from "./include-spec${ext}";
1294
1314
  `;
1295
1315
  return out;
1296
1316
  }
@@ -2076,7 +2096,7 @@ ${registrations.replace(/router/g, "app")}
2076
2096
  ${reExports}
2077
2097
 
2078
2098
  // Re-export types and schemas for convenience
2079
- export * from "./include-spec";
2099
+ export * from "./include-spec${ext}";
2080
2100
  `;
2081
2101
  }
2082
2102
 
@@ -2319,6 +2339,301 @@ export async function deleteRecord(
2319
2339
  }`;
2320
2340
  }
2321
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
+
2322
2637
  // src/types.ts
2323
2638
  function normalizeAuthConfig(input) {
2324
2639
  if (!input)
@@ -2376,15 +2691,22 @@ async function generate(configPath) {
2376
2691
  }
2377
2692
  const normDateType = cfg.dateType === "string" ? "string" : "date";
2378
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";
2379
2697
  console.log("\uD83D\uDCC1 Creating directories...");
2380
- await ensureDirs([
2698
+ const dirs = [
2381
2699
  serverDir,
2382
2700
  join(serverDir, "types"),
2383
2701
  join(serverDir, "zod"),
2384
2702
  join(serverDir, "routes"),
2385
2703
  clientDir,
2386
2704
  join(clientDir, "types")
2387
- ]);
2705
+ ];
2706
+ if (generateTests) {
2707
+ dirs.push(testDir);
2708
+ }
2709
+ await ensureDirs(dirs);
2388
2710
  const files = [];
2389
2711
  const includeSpec = emitIncludeSpec(graph);
2390
2712
  files.push({ path: join(serverDir, "include-spec.ts"), content: includeSpec });
@@ -2431,12 +2753,12 @@ async function generate(configPath) {
2431
2753
  });
2432
2754
  files.push({
2433
2755
  path: join(clientDir, `${table.name}.ts`),
2434
- content: emitClient(table)
2756
+ content: emitClient(table, cfg.useJsExtensionsClient)
2435
2757
  });
2436
2758
  }
2437
2759
  files.push({
2438
2760
  path: join(clientDir, "index.ts"),
2439
- content: emitClientIndex(Object.values(model.tables))
2761
+ content: emitClientIndex(Object.values(model.tables), cfg.useJsExtensionsClient)
2440
2762
  });
2441
2763
  if (serverFramework === "hono") {
2442
2764
  files.push({
@@ -2451,11 +2773,37 @@ async function generate(configPath) {
2451
2773
  path: join(serverDir, "sdk-bundle.ts"),
2452
2774
  content: emitSdkBundle(clientFiles, clientDir)
2453
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
+ }
2454
2797
  console.log("✍️ Writing files...");
2455
2798
  await writeFiles(files);
2456
2799
  console.log(`✅ Generated ${files.length} files`);
2457
2800
  console.log(` Server: ${serverDir}`);
2458
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
+ }
2459
2807
  }
2460
2808
 
2461
2809
  // src/cli.ts
@@ -1,3 +1,3 @@
1
1
  import type { Table } from "./introspect";
2
- export declare function emitClient(table: Table): string;
3
- export declare function emitClientIndex(tables: Table[]): string;
2
+ export declare function emitClient(table: Table, useJsExtensions?: boolean): string;
3
+ export declare function emitClientIndex(tables: Table[], useJsExtensions?: boolean): string;
@@ -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
@@ -933,17 +933,18 @@ ${hasAuth ? `
933
933
  }
934
934
 
935
935
  // src/emit-client.ts
936
- function emitClient(table) {
936
+ function emitClient(table, useJsExtensions) {
937
937
  const Type = pascal(table.name);
938
+ const ext = useJsExtensions ? ".js" : "";
938
939
  const pkCols = Array.isArray(table.pk) ? table.pk : table.pk ? [table.pk] : [];
939
940
  const safePk = pkCols.length ? pkCols : ["id"];
940
941
  const hasCompositePk = safePk.length > 1;
941
942
  const pkType = hasCompositePk ? `{ ${safePk.map((c) => `${c}: string`).join("; ")} }` : `string`;
942
943
  const pkPathExpr = hasCompositePk ? safePk.map((c) => `pk.${c}`).join(` + "/" + `) : `pk`;
943
944
  return `/* Generated. Do not edit. */
944
- import { BaseClient } from "./base-client";
945
- import type { ${Type}IncludeSpec } from "./include-spec";
946
- import type { Insert${Type}, Update${Type}, Select${Type} } from "./types/${table.name}";
945
+ import { BaseClient } from "./base-client${ext}";
946
+ import type { ${Type}IncludeSpec } from "./include-spec${ext}";
947
+ import type { Insert${Type}, Update${Type}, Select${Type} } from "./types/${table.name}${ext}";
947
948
 
948
949
  /**
949
950
  * Client for ${table.name} table operations
@@ -983,19 +984,20 @@ export class ${Type}Client extends BaseClient {
983
984
  }
984
985
  `;
985
986
  }
986
- function emitClientIndex(tables) {
987
+ function emitClientIndex(tables, useJsExtensions) {
988
+ const ext = useJsExtensions ? ".js" : "";
987
989
  let out = `/* Generated. Do not edit. */
988
990
  `;
989
- out += `import { BaseClient, type AuthConfig } from "./base-client";
991
+ out += `import { BaseClient, type AuthConfig } from "./base-client${ext}";
990
992
  `;
991
993
  for (const t of tables) {
992
- out += `import { ${pascal(t.name)}Client } from "./${t.name}";
994
+ out += `import { ${pascal(t.name)}Client } from "./${t.name}${ext}";
993
995
  `;
994
996
  }
995
997
  out += `
996
998
  // Re-export auth types for convenience
997
999
  `;
998
- out += `export type { AuthConfig as SDKAuth, AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client";
1000
+ out += `export type { AuthConfig as SDKAuth, AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${ext}";
999
1001
 
1000
1002
  `;
1001
1003
  out += `/**
@@ -1027,18 +1029,18 @@ function emitClientIndex(tables) {
1027
1029
  out += `// Export individual table clients
1028
1030
  `;
1029
1031
  for (const t of tables) {
1030
- out += `export { ${pascal(t.name)}Client } from "./${t.name}";
1032
+ out += `export { ${pascal(t.name)}Client } from "./${t.name}${ext}";
1031
1033
  `;
1032
1034
  }
1033
1035
  out += `
1034
1036
  // Export base client for custom extensions
1035
1037
  `;
1036
- out += `export { BaseClient } from "./base-client";
1038
+ out += `export { BaseClient } from "./base-client${ext}";
1037
1039
  `;
1038
1040
  out += `
1039
1041
  // Export include specifications
1040
1042
  `;
1041
- out += `export * from "./include-spec";
1043
+ out += `export * from "./include-spec${ext}";
1042
1044
  `;
1043
1045
  return out;
1044
1046
  }
@@ -1824,7 +1826,7 @@ ${registrations.replace(/router/g, "app")}
1824
1826
  ${reExports}
1825
1827
 
1826
1828
  // Re-export types and schemas for convenience
1827
- export * from "./include-spec";
1829
+ export * from "./include-spec${ext}";
1828
1830
  `;
1829
1831
  }
1830
1832
 
@@ -2067,6 +2069,301 @@ export async function deleteRecord(
2067
2069
  }`;
2068
2070
  }
2069
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
+
2070
2367
  // src/types.ts
2071
2368
  function normalizeAuthConfig(input) {
2072
2369
  if (!input)
@@ -2124,15 +2421,22 @@ async function generate(configPath) {
2124
2421
  }
2125
2422
  const normDateType = cfg.dateType === "string" ? "string" : "date";
2126
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";
2127
2427
  console.log("\uD83D\uDCC1 Creating directories...");
2128
- await ensureDirs([
2428
+ const dirs = [
2129
2429
  serverDir,
2130
2430
  join(serverDir, "types"),
2131
2431
  join(serverDir, "zod"),
2132
2432
  join(serverDir, "routes"),
2133
2433
  clientDir,
2134
2434
  join(clientDir, "types")
2135
- ]);
2435
+ ];
2436
+ if (generateTests) {
2437
+ dirs.push(testDir);
2438
+ }
2439
+ await ensureDirs(dirs);
2136
2440
  const files = [];
2137
2441
  const includeSpec = emitIncludeSpec(graph);
2138
2442
  files.push({ path: join(serverDir, "include-spec.ts"), content: includeSpec });
@@ -2179,12 +2483,12 @@ async function generate(configPath) {
2179
2483
  });
2180
2484
  files.push({
2181
2485
  path: join(clientDir, `${table.name}.ts`),
2182
- content: emitClient(table)
2486
+ content: emitClient(table, cfg.useJsExtensionsClient)
2183
2487
  });
2184
2488
  }
2185
2489
  files.push({
2186
2490
  path: join(clientDir, "index.ts"),
2187
- content: emitClientIndex(Object.values(model.tables))
2491
+ content: emitClientIndex(Object.values(model.tables), cfg.useJsExtensionsClient)
2188
2492
  });
2189
2493
  if (serverFramework === "hono") {
2190
2494
  files.push({
@@ -2199,11 +2503,37 @@ async function generate(configPath) {
2199
2503
  path: join(serverDir, "sdk-bundle.ts"),
2200
2504
  content: emitSdkBundle(clientFiles, clientDir)
2201
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
+ }
2202
2527
  console.log("✍️ Writing files...");
2203
2528
  await writeFiles(files);
2204
2529
  console.log(`✅ Generated ${files.length} files`);
2205
2530
  console.log(` Server: ${serverDir}`);
2206
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
+ }
2207
2537
  }
2208
2538
  export {
2209
2539
  generate
package/dist/types.d.ts CHANGED
@@ -30,6 +30,12 @@ export interface Config {
30
30
  auth?: AuthConfigInput;
31
31
  pull?: PullConfig;
32
32
  useJsExtensions?: boolean;
33
+ useJsExtensionsClient?: boolean;
34
+ tests?: {
35
+ generate?: boolean;
36
+ output?: string;
37
+ framework?: "vitest" | "jest" | "bun";
38
+ };
33
39
  }
34
40
  export interface PullConfig {
35
41
  from: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.5.1-alpha.0",
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",