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 +98 -0
- package/dist/cli.js +365 -17
- package/dist/emit-client.d.ts +2 -2
- package/dist/emit-tests.d.ts +17 -0
- package/dist/index.js +346 -16
- package/dist/types.d.ts +6 -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
@@ -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
|
-
|
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
|
package/dist/emit-client.d.ts
CHANGED
@@ -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
|
-
|
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.
|
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",
|