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 +98 -0
- package/dist/cli.js +342 -2
- package/dist/emit-tests.d.ts +17 -0
- package/dist/index.js +330 -2
- package/dist/types.d.ts +5 -0
- package/package.json +3 -2
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
|
-
|
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
|
-
|
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.
|
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",
|