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