postgresdk 0.6.4 → 0.6.6

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/dist/index.js CHANGED
@@ -1736,6 +1736,7 @@ function emitHonoRouter(tables, hasAuth, useJsExtensions) {
1736
1736
  return `/* Generated. Do not edit. */
1737
1737
  import { Hono } from "hono";
1738
1738
  import { SDK_MANIFEST } from "./sdk-bundle${ext}";
1739
+ import { getApiContract } from "./api-contract${ext}";
1739
1740
  ${imports}
1740
1741
  ${hasAuth ? `export { authMiddleware } from "./auth${ext}";` : ""}
1741
1742
 
@@ -1797,6 +1798,29 @@ ${registrations}
1797
1798
  });
1798
1799
  });
1799
1800
 
1801
+ // API Contract endpoints - describes the entire API
1802
+ router.get("/api/contract", (c) => {
1803
+ const format = c.req.query("format") || "json";
1804
+
1805
+ if (format === "markdown") {
1806
+ return c.text(getApiContract("markdown") as string, 200, {
1807
+ "Content-Type": "text/markdown; charset=utf-8"
1808
+ });
1809
+ }
1810
+
1811
+ return c.json(getApiContract("json"));
1812
+ });
1813
+
1814
+ router.get("/api/contract.json", (c) => {
1815
+ return c.json(getApiContract("json"));
1816
+ });
1817
+
1818
+ router.get("/api/contract.md", (c) => {
1819
+ return c.text(getApiContract("markdown") as string, 200, {
1820
+ "Content-Type": "text/markdown; charset=utf-8"
1821
+ });
1822
+ });
1823
+
1800
1824
  return router;
1801
1825
  }
1802
1826
 
@@ -2074,30 +2098,44 @@ function emitTableTest(table, clientPath, framework = "vitest") {
2074
2098
  const Type = pascal(table.name);
2075
2099
  const tableName = table.name;
2076
2100
  const imports = getFrameworkImports(framework);
2077
- const sampleData = generateSampleData(table);
2101
+ const hasForeignKeys = table.fks.length > 0;
2102
+ const foreignKeySetup = hasForeignKeys ? generateForeignKeySetup(table, clientPath) : null;
2103
+ const sampleData = generateSampleData(table, hasForeignKeys);
2078
2104
  const updateData = generateUpdateData(table);
2079
2105
  return `${imports}
2080
2106
  import { SDK } from '${clientPath}';
2081
2107
  import type { Insert${Type}, Update${Type}, Select${Type} } from '${clientPath}/types/${tableName}';
2108
+ ${foreignKeySetup?.imports || ""}
2082
2109
 
2083
2110
  /**
2084
2111
  * Basic tests for ${tableName} table operations
2085
2112
  *
2086
2113
  * These tests demonstrate basic CRUD operations.
2087
- * Add your own business logic tests in separate files.
2114
+ * The test data is auto-generated and may need adjustment for your specific schema.
2115
+ *
2116
+ * If tests fail due to validation errors:
2117
+ * 1. Check which fields are required by your API
2118
+ * 2. Update the test data below to match your schema requirements
2119
+ * 3. Consider adding your own business logic tests in separate files
2088
2120
  */
2089
2121
  describe('${Type} SDK Operations', () => {
2090
2122
  let sdk: SDK;
2091
2123
  let createdId: string;
2124
+ ${foreignKeySetup?.variables || ""}
2092
2125
 
2093
- beforeAll(() => {
2126
+ beforeAll(async () => {
2094
2127
  sdk = new SDK({
2095
2128
  baseUrl: process.env.API_URL || 'http://localhost:3000',
2096
2129
  auth: process.env.API_KEY ? { apiKey: process.env.API_KEY } : undefined
2097
2130
  });
2131
+ ${foreignKeySetup?.setup || ""}
2098
2132
  });
2099
2133
 
2100
- ${generateTestCases(table, sampleData, updateData)}
2134
+ ${hasForeignKeys && foreignKeySetup?.cleanup ? `afterAll(async () => {
2135
+ ${foreignKeySetup.cleanup}
2136
+ });
2137
+
2138
+ ` : ""}${generateTestCases(table, sampleData, updateData, hasForeignKeys)}
2101
2139
  });
2102
2140
  `;
2103
2141
  }
@@ -2147,6 +2185,45 @@ export function randomDate(): Date {
2147
2185
  }
2148
2186
  `;
2149
2187
  }
2188
+ function emitVitestConfig() {
2189
+ return `import { defineConfig } from 'vitest/config';
2190
+
2191
+ export default defineConfig({
2192
+ test: {
2193
+ globals: true,
2194
+ environment: 'node',
2195
+ testTimeout: 30000,
2196
+ hookTimeout: 30000,
2197
+ // The reporters are configured via CLI in the test script
2198
+ // Force color output in terminal
2199
+ pool: 'forks',
2200
+ poolOptions: {
2201
+ forks: {
2202
+ singleFork: true,
2203
+ }
2204
+ }
2205
+ },
2206
+ });
2207
+ `;
2208
+ }
2209
+ function emitTestGitignore() {
2210
+ return `# Test results
2211
+ test-results/
2212
+ *.log
2213
+
2214
+ # Node modules (if tests have their own dependencies)
2215
+ node_modules/
2216
+
2217
+ # Environment files
2218
+ .env
2219
+ .env.local
2220
+ .env.test
2221
+
2222
+ # Coverage reports
2223
+ coverage/
2224
+ *.lcov
2225
+ `;
2226
+ }
2150
2227
  function emitDockerCompose() {
2151
2228
  return `# Docker Compose for Test Database
2152
2229
  #
@@ -2205,6 +2282,10 @@ docker-compose up -d --wait
2205
2282
  export TEST_DATABASE_URL="postgres://testuser:testpass@localhost:5432/testdb"
2206
2283
  export TEST_API_URL="http://localhost:3000"
2207
2284
 
2285
+ # Force color output for better terminal experience
2286
+ export FORCE_COLOR=1
2287
+ export CI=""
2288
+
2208
2289
  # Wait for database to be ready
2209
2290
  echo "⏳ Waiting for database..."
2210
2291
  sleep 3
@@ -2232,7 +2313,14 @@ echo ""
2232
2313
  # sleep 3
2233
2314
 
2234
2315
  echo "\uD83E\uDDEA Running tests..."
2235
- ${runCommand} "$@"
2316
+ TIMESTAMP=$(date +%Y%m%d_%H%M%S)
2317
+ TEST_RESULTS_DIR="$SCRIPT_DIR/test-results"
2318
+ mkdir -p "$TEST_RESULTS_DIR"
2319
+
2320
+ # Run tests with appropriate reporter based on framework
2321
+ ${getTestCommand(framework, runCommand)}
2322
+
2323
+ TEST_EXIT_CODE=$?
2236
2324
 
2237
2325
  # Cleanup
2238
2326
  # if [ ! -z "\${SERVER_PID}" ]; then
@@ -2240,12 +2328,85 @@ ${runCommand} "$@"
2240
2328
  # kill $SERVER_PID 2>/dev/null || true
2241
2329
  # fi
2242
2330
 
2243
- echo "✅ Tests completed!"
2331
+ if [ $TEST_EXIT_CODE -eq 0 ]; then
2332
+ echo "✅ Tests completed successfully!"
2333
+ else
2334
+ echo "❌ Tests failed with exit code $TEST_EXIT_CODE"
2335
+ fi
2336
+
2337
+ echo ""
2338
+ echo "\uD83D\uDCCA Test results saved to:"
2339
+ echo " $TEST_RESULTS_DIR/"
2244
2340
  echo ""
2245
2341
  echo "To stop the test database, run:"
2246
2342
  echo " cd $SCRIPT_DIR && docker-compose down"
2343
+
2344
+ exit $TEST_EXIT_CODE
2247
2345
  `;
2248
2346
  }
2347
+ function generateForeignKeySetup(table, clientPath) {
2348
+ const imports = [];
2349
+ const variables = [];
2350
+ const setupStatements = [];
2351
+ const cleanupStatements = [];
2352
+ const foreignTables = new Set;
2353
+ for (const fk of table.fks) {
2354
+ const foreignTable = fk.toTable;
2355
+ if (!foreignTables.has(foreignTable)) {
2356
+ foreignTables.add(foreignTable);
2357
+ const ForeignType = pascal(foreignTable);
2358
+ imports.push(`import type { Insert${ForeignType} } from '${clientPath}/types/${foreignTable}';`);
2359
+ variables.push(`let ${foreignTable}Id: string;`);
2360
+ setupStatements.push(`
2361
+ // Create parent ${foreignTable} record for foreign key reference
2362
+ const ${foreignTable}Data = ${generateMinimalSampleData(foreignTable)};
2363
+ const created${ForeignType} = await sdk.${foreignTable}.create(${foreignTable}Data);
2364
+ ${foreignTable}Id = created${ForeignType}.id;`);
2365
+ cleanupStatements.push(`
2366
+ // Clean up parent ${foreignTable} record
2367
+ if (${foreignTable}Id) {
2368
+ try {
2369
+ await sdk.${foreignTable}.delete(${foreignTable}Id);
2370
+ } catch (e) {
2371
+ // Parent might already be deleted due to cascading
2372
+ }
2373
+ }`);
2374
+ }
2375
+ }
2376
+ return {
2377
+ imports: imports.join(`
2378
+ `),
2379
+ variables: variables.join(`
2380
+ `),
2381
+ setup: setupStatements.join(""),
2382
+ cleanup: cleanupStatements.join("")
2383
+ };
2384
+ }
2385
+ function generateMinimalSampleData(tableName) {
2386
+ const commonPatterns = {
2387
+ contacts: `{ name: 'Test Contact', email: 'test@example.com', gender: 'M', date_of_birth: new Date('1990-01-01'), emergency_contact: 'Emergency Contact', clinic_location: 'Main Clinic', specialty: 'General', fmv_tiering: 'Standard', flight_preferences: 'Economy', dietary_restrictions: [], special_accommodations: 'None' }`,
2388
+ tags: `{ name: 'Test Tag' }`,
2389
+ authors: `{ name: 'Test Author' }`,
2390
+ books: `{ title: 'Test Book' }`,
2391
+ users: `{ name: 'Test User', email: 'test@example.com' }`,
2392
+ categories: `{ name: 'Test Category' }`,
2393
+ products: `{ name: 'Test Product', price: 10.99 }`,
2394
+ orders: `{ total: 100.00 }`
2395
+ };
2396
+ return commonPatterns[tableName] || `{ name: 'Test ${pascal(tableName)}' }`;
2397
+ }
2398
+ function getTestCommand(framework, baseCommand) {
2399
+ switch (framework) {
2400
+ case "vitest":
2401
+ return `FORCE_COLOR=1 ${baseCommand} --reporter=default --reporter=json --outputFile="$TEST_RESULTS_DIR/results-\${TIMESTAMP}.json" "$@"`;
2402
+ case "jest":
2403
+ return `FORCE_COLOR=1 ${baseCommand} --colors --json --outputFile="$TEST_RESULTS_DIR/results-\${TIMESTAMP}.json" "$@"`;
2404
+ case "bun":
2405
+ return `FORCE_COLOR=1 ${baseCommand} "$@" 2>&1 | tee "$TEST_RESULTS_DIR/results-\${TIMESTAMP}.txt"`;
2406
+ default:
2407
+ return `FORCE_COLOR=1 ${baseCommand} "$@"`;
2408
+ }
2409
+ }
2249
2410
  function getFrameworkImports(framework) {
2250
2411
  switch (framework) {
2251
2412
  case "vitest":
@@ -2258,17 +2419,38 @@ function getFrameworkImports(framework) {
2258
2419
  return "import { describe, it, expect, beforeAll, afterAll } from 'vitest';";
2259
2420
  }
2260
2421
  }
2261
- function generateSampleData(table) {
2422
+ function generateSampleData(table, hasForeignKeys = false) {
2262
2423
  const fields = [];
2424
+ const foreignKeyColumns = new Map;
2425
+ if (hasForeignKeys) {
2426
+ for (const fk of table.fks) {
2427
+ if (fk.from.length === 1 && fk.from[0]) {
2428
+ foreignKeyColumns.set(fk.from[0], fk.toTable);
2429
+ }
2430
+ }
2431
+ }
2263
2432
  for (const col of table.columns) {
2264
- if (col.hasDefault || col.name === "id" || col.name === "created_at" || col.name === "updated_at") {
2433
+ if (col.name === "id" && col.hasDefault) {
2434
+ continue;
2435
+ }
2436
+ if ((col.name === "created_at" || col.name === "updated_at") && col.hasDefault) {
2265
2437
  continue;
2266
2438
  }
2267
- if (col.nullable) {
2439
+ if (col.name === "deleted_at") {
2268
2440
  continue;
2269
2441
  }
2270
- const value = getSampleValue(col.pgType, col.name);
2271
- fields.push(` ${col.name}: ${value}`);
2442
+ if (!col.nullable) {
2443
+ const foreignTable = foreignKeyColumns.get(col.name);
2444
+ const value = foreignTable ? `${foreignTable}Id` : getSampleValue(col.pgType, col.name);
2445
+ fields.push(` ${col.name}: ${value}`);
2446
+ } else {
2447
+ const isImportant = col.name.endsWith("_id") || col.name.endsWith("_by") || col.name.includes("email") || col.name.includes("name") || col.name.includes("phone") || col.name.includes("address") || col.name.includes("description") || col.name.includes("color") || col.name.includes("type") || col.name.includes("status") || col.name.includes("subject");
2448
+ if (isImportant) {
2449
+ const foreignTable = foreignKeyColumns.get(col.name);
2450
+ const value = foreignTable ? `${foreignTable}Id` : getSampleValue(col.pgType, col.name);
2451
+ fields.push(` ${col.name}: ${value}`);
2452
+ }
2453
+ }
2272
2454
  }
2273
2455
  return fields.length > 0 ? `{
2274
2456
  ${fields.join(`,
@@ -2294,15 +2476,63 @@ ${fields.join(`,
2294
2476
  }
2295
2477
  function getSampleValue(type, name, isUpdate = false) {
2296
2478
  const suffix = isUpdate ? ' + " (updated)"' : "";
2479
+ if (name.endsWith("_id") || name.endsWith("_by")) {
2480
+ return `'550e8400-e29b-41d4-a716-446655440000'`;
2481
+ }
2297
2482
  if (name.includes("email")) {
2298
2483
  return `'test${isUpdate ? ".updated" : ""}@example.com'`;
2299
2484
  }
2485
+ if (name === "color") {
2486
+ return `'#${isUpdate ? "FF0000" : "0000FF"}'`;
2487
+ }
2488
+ if (name === "gender") {
2489
+ return `'${isUpdate ? "F" : "M"}'`;
2490
+ }
2491
+ if (name.includes("phone")) {
2492
+ return `'${isUpdate ? "555-0200" : "555-0100"}'`;
2493
+ }
2494
+ if (name.includes("address")) {
2495
+ return `'123 ${isUpdate ? "Updated" : "Test"} Street'`;
2496
+ }
2497
+ if (name === "type" || name === "status") {
2498
+ return `'${isUpdate ? "updated" : "active"}'`;
2499
+ }
2500
+ if (name === "subject") {
2501
+ return `'Test Subject${isUpdate ? " Updated" : ""}'`;
2502
+ }
2300
2503
  if (name.includes("name") || name.includes("title")) {
2301
2504
  return `'Test ${pascal(name)}'${suffix}`;
2302
2505
  }
2303
2506
  if (name.includes("description") || name.includes("bio") || name.includes("content")) {
2304
2507
  return `'Test description'${suffix}`;
2305
2508
  }
2509
+ if (name.includes("preferences") || name.includes("settings")) {
2510
+ return `'Test preferences'${suffix}`;
2511
+ }
2512
+ if (name.includes("restrictions") || name.includes("dietary")) {
2513
+ return `['vegetarian']`;
2514
+ }
2515
+ if (name.includes("location") || name.includes("clinic")) {
2516
+ return `'Test Location'${suffix}`;
2517
+ }
2518
+ if (name.includes("specialty")) {
2519
+ return `'General'`;
2520
+ }
2521
+ if (name.includes("tier")) {
2522
+ return `'Standard'`;
2523
+ }
2524
+ if (name.includes("emergency")) {
2525
+ return `'Emergency Contact ${isUpdate ? "Updated" : "Name"}'`;
2526
+ }
2527
+ if (name === "date_of_birth" || name.includes("birth")) {
2528
+ return `new Date('1990-01-01')`;
2529
+ }
2530
+ if (name.includes("accommodations")) {
2531
+ return `'No special accommodations'`;
2532
+ }
2533
+ if (name.includes("flight")) {
2534
+ return `'Economy'`;
2535
+ }
2306
2536
  switch (type) {
2307
2537
  case "text":
2308
2538
  case "varchar":
@@ -2326,17 +2556,20 @@ function getSampleValue(type, name, isUpdate = false) {
2326
2556
  return `'2024-01-01'`;
2327
2557
  case "timestamp":
2328
2558
  case "timestamptz":
2329
- return `new Date().toISOString()`;
2559
+ return `new Date()`;
2330
2560
  case "json":
2331
2561
  case "jsonb":
2332
2562
  return `{ key: 'value' }`;
2333
2563
  case "uuid":
2334
- return `'${isUpdate ? "b" : "a"}0e0e0e0-e0e0-e0e0-e0e0-e0e0e0e0e0e0'`;
2564
+ return `'${isUpdate ? "550e8400-e29b-41d4-a716-446655440001" : "550e8400-e29b-41d4-a716-446655440000"}'`;
2565
+ case "text[]":
2566
+ case "varchar[]":
2567
+ return `['item1', 'item2']`;
2335
2568
  default:
2336
2569
  return `'test'`;
2337
2570
  }
2338
2571
  }
2339
- function generateTestCases(table, sampleData, updateData) {
2572
+ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false) {
2340
2573
  const Type = pascal(table.name);
2341
2574
  const hasData = sampleData !== "{}";
2342
2575
  return `it('should create a ${table.name}', async () => {
@@ -2395,6 +2628,280 @@ function generateTestCases(table, sampleData, updateData) {
2395
2628
  });` : ""}`;
2396
2629
  }
2397
2630
 
2631
+ // src/emit-api-contract.ts
2632
+ function generateApiContract(model, config) {
2633
+ const resources = [];
2634
+ const relationships = [];
2635
+ for (const table of Object.values(model.tables)) {
2636
+ resources.push(generateResourceContract(table, model));
2637
+ for (const fk of table.fks) {
2638
+ relationships.push({
2639
+ from: table.name,
2640
+ to: fk.toTable,
2641
+ type: "many-to-one",
2642
+ description: `Each ${table.name} belongs to one ${fk.toTable}`
2643
+ });
2644
+ }
2645
+ }
2646
+ const contract = {
2647
+ version: "1.0.0",
2648
+ generatedAt: new Date().toISOString(),
2649
+ description: "Auto-generated API contract describing all available endpoints, resources, and their relationships",
2650
+ resources,
2651
+ relationships
2652
+ };
2653
+ if (config.auth?.strategy && config.auth.strategy !== "none") {
2654
+ contract.authentication = {
2655
+ type: config.auth.strategy,
2656
+ description: getAuthDescription(config.auth.strategy)
2657
+ };
2658
+ }
2659
+ return contract;
2660
+ }
2661
+ function generateResourceContract(table, model) {
2662
+ const Type = pascal(table.name);
2663
+ const basePath = `/v1/${table.name}`;
2664
+ const endpoints = [
2665
+ {
2666
+ method: "GET",
2667
+ path: basePath,
2668
+ description: `List all ${table.name} records with optional filtering, sorting, and pagination`,
2669
+ queryParameters: {
2670
+ limit: "number - Maximum number of records to return (default: 50)",
2671
+ offset: "number - Number of records to skip for pagination",
2672
+ order_by: "string - Field to sort by",
2673
+ order_dir: "string - Sort direction (asc or desc)",
2674
+ include: "string - Comma-separated list of related resources to include",
2675
+ ...generateFilterParams(table)
2676
+ },
2677
+ responseBody: `Array<${Type}>`
2678
+ },
2679
+ {
2680
+ method: "GET",
2681
+ path: `${basePath}/:id`,
2682
+ description: `Get a single ${table.name} record by ID`,
2683
+ queryParameters: {
2684
+ include: "string - Comma-separated list of related resources to include"
2685
+ },
2686
+ responseBody: `${Type}`
2687
+ },
2688
+ {
2689
+ method: "POST",
2690
+ path: basePath,
2691
+ description: `Create a new ${table.name} record`,
2692
+ requestBody: `Insert${Type}`,
2693
+ responseBody: `${Type}`
2694
+ },
2695
+ {
2696
+ method: "PATCH",
2697
+ path: `${basePath}/:id`,
2698
+ description: `Update an existing ${table.name} record`,
2699
+ requestBody: `Update${Type}`,
2700
+ responseBody: `${Type}`
2701
+ },
2702
+ {
2703
+ method: "DELETE",
2704
+ path: `${basePath}/:id`,
2705
+ description: `Delete a ${table.name} record`,
2706
+ responseBody: `${Type}`
2707
+ }
2708
+ ];
2709
+ const fields = table.columns.map((col) => generateFieldContract(col, table));
2710
+ return {
2711
+ name: Type,
2712
+ tableName: table.name,
2713
+ description: `Resource for managing ${table.name} records`,
2714
+ endpoints,
2715
+ fields
2716
+ };
2717
+ }
2718
+ function generateFieldContract(column, table) {
2719
+ const field = {
2720
+ name: column.name,
2721
+ type: postgresTypeToJsonType(column.pgType),
2722
+ required: !column.nullable && !column.hasDefault,
2723
+ description: generateFieldDescription(column, table)
2724
+ };
2725
+ const fk = table.fks.find((fk2) => fk2.from.length === 1 && fk2.from[0] === column.name);
2726
+ if (fk) {
2727
+ field.foreignKey = {
2728
+ table: fk.toTable,
2729
+ field: fk.to[0] || "id"
2730
+ };
2731
+ }
2732
+ return field;
2733
+ }
2734
+ function generateFieldDescription(column, table) {
2735
+ const descriptions = [];
2736
+ descriptions.push(`${postgresTypeToJsonType(column.pgType)} field`);
2737
+ if (column.name === "id") {
2738
+ descriptions.push("Unique identifier");
2739
+ } else if (column.name === "created_at") {
2740
+ descriptions.push("Timestamp when the record was created");
2741
+ } else if (column.name === "updated_at") {
2742
+ descriptions.push("Timestamp when the record was last updated");
2743
+ } else if (column.name === "deleted_at") {
2744
+ descriptions.push("Soft delete timestamp");
2745
+ } else if (column.name.endsWith("_id")) {
2746
+ const relatedTable = column.name.slice(0, -3);
2747
+ descriptions.push(`Reference to ${relatedTable}`);
2748
+ } else if (column.name.includes("email")) {
2749
+ descriptions.push("Email address");
2750
+ } else if (column.name.includes("phone")) {
2751
+ descriptions.push("Phone number");
2752
+ } else if (column.name.includes("name")) {
2753
+ descriptions.push("Name field");
2754
+ } else if (column.name.includes("description")) {
2755
+ descriptions.push("Description text");
2756
+ } else if (column.name.includes("status")) {
2757
+ descriptions.push("Status indicator");
2758
+ } else if (column.name.includes("price") || column.name.includes("amount") || column.name.includes("total")) {
2759
+ descriptions.push("Monetary value");
2760
+ }
2761
+ if (!column.nullable && !column.hasDefault) {
2762
+ descriptions.push("(required)");
2763
+ } else if (column.nullable) {
2764
+ descriptions.push("(optional)");
2765
+ }
2766
+ return descriptions.join(" - ");
2767
+ }
2768
+ function postgresTypeToJsonType(pgType) {
2769
+ switch (pgType) {
2770
+ case "int":
2771
+ case "integer":
2772
+ case "smallint":
2773
+ case "bigint":
2774
+ case "decimal":
2775
+ case "numeric":
2776
+ case "real":
2777
+ case "double precision":
2778
+ case "float":
2779
+ return "number";
2780
+ case "boolean":
2781
+ case "bool":
2782
+ return "boolean";
2783
+ case "date":
2784
+ case "timestamp":
2785
+ case "timestamptz":
2786
+ return "date/datetime";
2787
+ case "json":
2788
+ case "jsonb":
2789
+ return "object";
2790
+ case "uuid":
2791
+ return "uuid";
2792
+ case "text[]":
2793
+ case "varchar[]":
2794
+ return "array<string>";
2795
+ case "int[]":
2796
+ case "integer[]":
2797
+ return "array<number>";
2798
+ default:
2799
+ return "string";
2800
+ }
2801
+ }
2802
+ function generateFilterParams(table) {
2803
+ const filters = {};
2804
+ for (const col of table.columns) {
2805
+ const type = postgresTypeToJsonType(col.pgType);
2806
+ filters[col.name] = `${type} - Filter by exact ${col.name} value`;
2807
+ if (type === "number" || type === "date/datetime") {
2808
+ filters[`${col.name}_gt`] = `${type} - Filter where ${col.name} is greater than`;
2809
+ filters[`${col.name}_gte`] = `${type} - Filter where ${col.name} is greater than or equal`;
2810
+ filters[`${col.name}_lt`] = `${type} - Filter where ${col.name} is less than`;
2811
+ filters[`${col.name}_lte`] = `${type} - Filter where ${col.name} is less than or equal`;
2812
+ }
2813
+ if (type === "string") {
2814
+ filters[`${col.name}_like`] = `string - Filter where ${col.name} contains text (case-insensitive)`;
2815
+ }
2816
+ }
2817
+ return filters;
2818
+ }
2819
+ function getAuthDescription(strategy) {
2820
+ switch (strategy) {
2821
+ case "jwt":
2822
+ return "JWT Bearer token authentication. Include token in Authorization header: 'Bearer <token>'";
2823
+ case "apiKey":
2824
+ return "API Key authentication. Include key in the configured header (e.g., 'x-api-key')";
2825
+ default:
2826
+ return "Custom authentication strategy";
2827
+ }
2828
+ }
2829
+ function generateApiContractMarkdown(contract) {
2830
+ const lines = [];
2831
+ lines.push("# API Contract");
2832
+ lines.push("");
2833
+ lines.push(contract.description);
2834
+ lines.push("");
2835
+ lines.push(`**Version:** ${contract.version}`);
2836
+ lines.push(`**Generated:** ${new Date(contract.generatedAt).toLocaleString()}`);
2837
+ lines.push("");
2838
+ if (contract.authentication) {
2839
+ lines.push("## Authentication");
2840
+ lines.push("");
2841
+ lines.push(`**Type:** ${contract.authentication.type}`);
2842
+ lines.push("");
2843
+ lines.push(contract.authentication.description);
2844
+ lines.push("");
2845
+ }
2846
+ lines.push("## Resources");
2847
+ lines.push("");
2848
+ for (const resource of contract.resources) {
2849
+ lines.push(`### ${resource.name}`);
2850
+ lines.push("");
2851
+ lines.push(resource.description);
2852
+ lines.push("");
2853
+ lines.push("**Endpoints:**");
2854
+ lines.push("");
2855
+ for (const endpoint of resource.endpoints) {
2856
+ lines.push(`- \`${endpoint.method} ${endpoint.path}\` - ${endpoint.description}`);
2857
+ }
2858
+ lines.push("");
2859
+ lines.push("**Fields:**");
2860
+ lines.push("");
2861
+ for (const field of resource.fields) {
2862
+ const required = field.required ? " *(required)*" : "";
2863
+ const fk = field.foreignKey ? ` → ${field.foreignKey.table}` : "";
2864
+ lines.push(`- \`${field.name}\` (${field.type})${required}${fk} - ${field.description}`);
2865
+ }
2866
+ lines.push("");
2867
+ }
2868
+ if (contract.relationships.length > 0) {
2869
+ lines.push("## Relationships");
2870
+ lines.push("");
2871
+ for (const rel of contract.relationships) {
2872
+ lines.push(`- **${rel.from}** → **${rel.to}** (${rel.type}): ${rel.description}`);
2873
+ }
2874
+ lines.push("");
2875
+ }
2876
+ return lines.join(`
2877
+ `);
2878
+ }
2879
+ function emitApiContract(model, config) {
2880
+ const contract = generateApiContract(model, config);
2881
+ const contractJson = JSON.stringify(contract, null, 2);
2882
+ return `/**
2883
+ * API Contract
2884
+ *
2885
+ * This module exports the API contract that describes all available
2886
+ * endpoints, resources, and their relationships.
2887
+ */
2888
+
2889
+ export const apiContract = ${contractJson};
2890
+
2891
+ export const apiContractMarkdown = \`${generateApiContractMarkdown(contract).replace(/`/g, "\\`")}\`;
2892
+
2893
+ /**
2894
+ * Helper to get the contract in different formats
2895
+ */
2896
+ export function getApiContract(format: 'json' | 'markdown' = 'json') {
2897
+ if (format === 'markdown') {
2898
+ return apiContractMarkdown;
2899
+ }
2900
+ return apiContract;
2901
+ }
2902
+ `;
2903
+ }
2904
+
2398
2905
  // src/types.ts
2399
2906
  function normalizeAuthConfig(input) {
2400
2907
  if (!input)
@@ -2538,6 +3045,10 @@ async function generate(configPath) {
2538
3045
  path: join(serverDir, "sdk-bundle.ts"),
2539
3046
  content: emitSdkBundle(clientFiles, clientDir)
2540
3047
  });
3048
+ files.push({
3049
+ path: join(serverDir, "api-contract.ts"),
3050
+ content: emitApiContract(model, cfg)
3051
+ });
2541
3052
  if (generateTests) {
2542
3053
  console.log("\uD83E\uDDEA Generating tests...");
2543
3054
  const relativeClientPath = relative(testDir, clientDir);
@@ -2553,6 +3064,16 @@ async function generate(configPath) {
2553
3064
  path: join(testDir, "run-tests.sh"),
2554
3065
  content: emitTestScript(testFramework)
2555
3066
  });
3067
+ files.push({
3068
+ path: join(testDir, ".gitignore"),
3069
+ content: emitTestGitignore()
3070
+ });
3071
+ if (testFramework === "vitest") {
3072
+ files.push({
3073
+ path: join(testDir, "vitest.config.ts"),
3074
+ content: emitVitestConfig()
3075
+ });
3076
+ }
2556
3077
  for (const table of Object.values(model.tables)) {
2557
3078
  files.push({
2558
3079
  path: join(testDir, `${table.name}.test.ts`),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,6 +28,7 @@
28
28
  "test:gen-with-tests": "bun src/cli.ts generate -c test/test-with-tests.config.ts",
29
29
  "test:pull": "bun test/test-pull.ts",
30
30
  "test:typecheck": "bun test/test-typecheck.ts",
31
+ "typecheck": "tsc --noEmit",
31
32
  "prepublishOnly": "npm run build",
32
33
  "publish:patch": "./publish.sh",
33
34
  "publish:minor": "./publish.sh",