postgresdk 0.13.0 → 0.14.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 +160 -12
- package/dist/cli.js +242 -105
- package/dist/emit-routes-hono.d.ts +1 -0
- package/dist/emit-shared-types.d.ts +1 -0
- package/dist/index.js +138 -67
- package/dist/types.d.ts +9 -9
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -4,6 +4,50 @@
|
|
|
4
4
|
|
|
5
5
|
Generate a typed server/client SDK from your PostgreSQL database schema.
|
|
6
6
|
|
|
7
|
+
## See It In Action
|
|
8
|
+
|
|
9
|
+
**Your database:**
|
|
10
|
+
```sql
|
|
11
|
+
CREATE TABLE users (
|
|
12
|
+
id SERIAL PRIMARY KEY,
|
|
13
|
+
name TEXT NOT NULL,
|
|
14
|
+
email TEXT UNIQUE NOT NULL
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE TABLE posts (
|
|
18
|
+
id SERIAL PRIMARY KEY,
|
|
19
|
+
user_id INTEGER REFERENCES users(id),
|
|
20
|
+
title TEXT NOT NULL,
|
|
21
|
+
published_at TIMESTAMPTZ
|
|
22
|
+
);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**What you get:**
|
|
26
|
+
```typescript
|
|
27
|
+
// ✨ Fully typed client with autocomplete
|
|
28
|
+
const user = await sdk.users.create({
|
|
29
|
+
name: "Alice",
|
|
30
|
+
email: "alice@example.com"
|
|
31
|
+
});
|
|
32
|
+
// ^ TypeScript knows: user.id is number, user.name is string
|
|
33
|
+
|
|
34
|
+
// 🔗 Automatic relationship loading
|
|
35
|
+
const users = await sdk.users.list({
|
|
36
|
+
include: { posts: true }
|
|
37
|
+
});
|
|
38
|
+
// ^ users[0].posts is fully typed Post[]
|
|
39
|
+
|
|
40
|
+
// 🎯 Advanced filtering with type safety
|
|
41
|
+
const filtered = await sdk.users.list({
|
|
42
|
+
where: {
|
|
43
|
+
email: { $ilike: '%@company.com' },
|
|
44
|
+
posts: { published_at: { $isNot: null } }
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**All generated automatically. Zero boilerplate.**
|
|
50
|
+
|
|
7
51
|
## Features
|
|
8
52
|
|
|
9
53
|
- 🚀 **Instant SDK Generation** - Point at your PostgreSQL database and get a complete SDK
|
|
@@ -20,14 +64,23 @@ Generate a typed server/client SDK from your PostgreSQL database schema.
|
|
|
20
64
|
npm install -g postgresdk
|
|
21
65
|
# or
|
|
22
66
|
npx postgresdk generate
|
|
67
|
+
|
|
68
|
+
# With Bun
|
|
69
|
+
bun install -g postgresdk
|
|
70
|
+
# or
|
|
71
|
+
bunx postgresdk generate
|
|
23
72
|
```
|
|
24
73
|
|
|
74
|
+
> **Note:** Currently only generates **Hono** server code. See [Supported Frameworks](#supported-frameworks) for details.
|
|
75
|
+
|
|
25
76
|
## Quick Start
|
|
26
77
|
|
|
27
78
|
1. Initialize your project:
|
|
28
79
|
|
|
29
80
|
```bash
|
|
30
81
|
npx postgresdk init
|
|
82
|
+
# or with Bun
|
|
83
|
+
bunx postgresdk init
|
|
31
84
|
```
|
|
32
85
|
|
|
33
86
|
This creates a `postgresdk.config.ts` file with all available options documented.
|
|
@@ -45,12 +98,13 @@ export default {
|
|
|
45
98
|
|
|
46
99
|
```bash
|
|
47
100
|
postgresdk generate
|
|
101
|
+
# or with Bun
|
|
102
|
+
bunx postgresdk generate
|
|
48
103
|
```
|
|
49
104
|
|
|
50
|
-
4.
|
|
105
|
+
4. Set up your server:
|
|
51
106
|
|
|
52
107
|
```typescript
|
|
53
|
-
// Server (Hono)
|
|
54
108
|
import { Hono } from "hono";
|
|
55
109
|
import { Client } from "pg";
|
|
56
110
|
import { createRouter } from "./api/server/router";
|
|
@@ -61,8 +115,11 @@ await pg.connect();
|
|
|
61
115
|
|
|
62
116
|
const api = createRouter({ pg });
|
|
63
117
|
app.route("/", api);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
5. Use the client SDK:
|
|
64
121
|
|
|
65
|
-
|
|
122
|
+
```typescript
|
|
66
123
|
import { SDK } from "./api/client";
|
|
67
124
|
|
|
68
125
|
const sdk = new SDK({ baseUrl: "http://localhost:3000" });
|
|
@@ -82,13 +139,12 @@ Create a `postgresdk.config.ts` file in your project root:
|
|
|
82
139
|
export default {
|
|
83
140
|
// Required
|
|
84
141
|
connectionString: process.env.DATABASE_URL || "postgres://user:pass@localhost:5432/dbname",
|
|
85
|
-
|
|
142
|
+
|
|
86
143
|
// Optional (with defaults)
|
|
87
144
|
schema: "public", // Database schema to introspect
|
|
88
|
-
|
|
89
|
-
outClient: "./api/client", // Client SDK output directory
|
|
145
|
+
outDir: "./api", // Output directory (or { client: "./sdk", server: "./api" })
|
|
90
146
|
softDeleteColumn: null, // Column name for soft deletes (e.g., "deleted_at")
|
|
91
|
-
includeMethodsDepth: 2,
|
|
147
|
+
includeMethodsDepth: 2, // Max depth for nested includes
|
|
92
148
|
dateType: "date", // "date" | "string" - How to handle timestamps
|
|
93
149
|
serverFramework: "hono", // Currently only hono is supported
|
|
94
150
|
useJsExtensions: false, // Add .js to imports (for Vercel Edge, Deno)
|
|
@@ -97,7 +153,11 @@ export default {
|
|
|
97
153
|
auth: {
|
|
98
154
|
apiKey: process.env.API_KEY, // Simple API key auth
|
|
99
155
|
// OR
|
|
100
|
-
jwt:
|
|
156
|
+
jwt: { // JWT with multi-service support
|
|
157
|
+
services: [
|
|
158
|
+
{ issuer: "my-app", secret: process.env.JWT_SECRET }
|
|
159
|
+
]
|
|
160
|
+
}
|
|
101
161
|
},
|
|
102
162
|
|
|
103
163
|
// Test generation (optional)
|
|
@@ -282,14 +342,35 @@ const sdk = new SDK({
|
|
|
282
342
|
export default {
|
|
283
343
|
connectionString: "...",
|
|
284
344
|
auth: {
|
|
285
|
-
|
|
345
|
+
strategy: "jwt-hs256",
|
|
346
|
+
jwt: {
|
|
347
|
+
services: [
|
|
348
|
+
{ issuer: "my-app", secret: process.env.JWT_SECRET }
|
|
349
|
+
],
|
|
350
|
+
audience: "my-api" // Optional
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// Multi-service example (each service has its own secret)
|
|
356
|
+
export default {
|
|
357
|
+
connectionString: "...",
|
|
358
|
+
auth: {
|
|
359
|
+
strategy: "jwt-hs256",
|
|
360
|
+
jwt: {
|
|
361
|
+
services: [
|
|
362
|
+
{ issuer: "web-app", secret: process.env.WEB_APP_SECRET },
|
|
363
|
+
{ issuer: "mobile-app", secret: process.env.MOBILE_SECRET },
|
|
364
|
+
],
|
|
365
|
+
audience: "my-api"
|
|
366
|
+
}
|
|
286
367
|
}
|
|
287
368
|
};
|
|
288
369
|
|
|
289
370
|
// Client SDK usage
|
|
290
371
|
const sdk = new SDK({
|
|
291
372
|
baseUrl: "http://localhost:3000",
|
|
292
|
-
auth: { jwt: "eyJhbGciOiJIUzI1NiIs..." }
|
|
373
|
+
auth: { jwt: "eyJhbGciOiJIUzI1NiIs..." } // JWT must include 'iss' claim
|
|
293
374
|
});
|
|
294
375
|
```
|
|
295
376
|
|
|
@@ -378,6 +459,42 @@ app.route("/", apiRouter);
|
|
|
378
459
|
serve({ fetch: app.fetch, port: 3000 });
|
|
379
460
|
```
|
|
380
461
|
|
|
462
|
+
## Deployment
|
|
463
|
+
|
|
464
|
+
### Serverless (Vercel, Netlify, Cloudflare Workers)
|
|
465
|
+
|
|
466
|
+
Use `max: 1` - each serverless instance should hold one connection:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
import { Pool } from "@neondatabase/serverless";
|
|
470
|
+
|
|
471
|
+
const pool = new Pool({
|
|
472
|
+
connectionString: process.env.DATABASE_URL,
|
|
473
|
+
max: 1 // One connection per serverless instance
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const apiRouter = createRouter({ pg: pool });
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
**Why `max: 1`?** Serverless functions are ephemeral and isolated. Each instance handles one request at a time, so connection pooling provides no benefit and wastes database connections.
|
|
480
|
+
|
|
481
|
+
### Traditional Servers (Railway, Render, VPS)
|
|
482
|
+
|
|
483
|
+
Use connection pooling to reuse connections across requests:
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
import { Pool } from "@neondatabase/serverless";
|
|
487
|
+
|
|
488
|
+
const pool = new Pool({
|
|
489
|
+
connectionString: process.env.DATABASE_URL,
|
|
490
|
+
max: 10 // Reuse connections across requests
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const apiRouter = createRouter({ pg: pool });
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Why `max: 10`?** Long-running servers handle many concurrent requests. Pooling prevents opening/closing connections for every request, significantly improving performance.
|
|
497
|
+
|
|
381
498
|
## SDK Distribution
|
|
382
499
|
|
|
383
500
|
Your generated SDK can be pulled by client applications:
|
|
@@ -420,6 +537,9 @@ Run tests with the included Docker setup:
|
|
|
420
537
|
```bash
|
|
421
538
|
chmod +x api/tests/run-tests.sh
|
|
422
539
|
./api/tests/run-tests.sh
|
|
540
|
+
|
|
541
|
+
# Or with Bun's built-in test runner (if framework: "bun")
|
|
542
|
+
bun test
|
|
423
543
|
```
|
|
424
544
|
|
|
425
545
|
## CLI Commands
|
|
@@ -437,8 +557,14 @@ Commands:
|
|
|
437
557
|
Options:
|
|
438
558
|
-c, --config <path> Path to config file (default: postgresdk.config.ts)
|
|
439
559
|
|
|
560
|
+
Init flags:
|
|
561
|
+
--api Generate API-side config (for database introspection)
|
|
562
|
+
--sdk Generate SDK-side config (for consuming remote SDK)
|
|
563
|
+
|
|
440
564
|
Examples:
|
|
441
|
-
postgresdk init
|
|
565
|
+
postgresdk init # Interactive prompt
|
|
566
|
+
postgresdk init --api # API-side config
|
|
567
|
+
postgresdk init --sdk # SDK-side config
|
|
442
568
|
postgresdk generate
|
|
443
569
|
postgresdk generate -c custom.config.ts
|
|
444
570
|
postgresdk pull --from=https://api.com --output=./src/sdk
|
|
@@ -446,10 +572,32 @@ Examples:
|
|
|
446
572
|
|
|
447
573
|
## Requirements
|
|
448
574
|
|
|
449
|
-
- Node.js 18+
|
|
575
|
+
- Node.js 18+
|
|
450
576
|
- PostgreSQL 12+
|
|
451
577
|
- TypeScript project (for using generated code)
|
|
452
578
|
|
|
579
|
+
## Supported Frameworks
|
|
580
|
+
|
|
581
|
+
**Currently, postgresdk only generates server code for Hono.**
|
|
582
|
+
|
|
583
|
+
While the configuration accepts `serverFramework: "hono" | "express" | "fastify"`, only Hono is implemented at this time. Attempting to generate code with `express` or `fastify` will result in an error.
|
|
584
|
+
|
|
585
|
+
### Why Hono?
|
|
586
|
+
|
|
587
|
+
Hono was chosen as the initial framework because:
|
|
588
|
+
- **Edge-first design** - Works seamlessly in serverless and edge environments (Cloudflare Workers, Vercel Edge, Deno Deploy)
|
|
589
|
+
- **Minimal dependencies** - Lightweight with excellent performance
|
|
590
|
+
- **Modern patterns** - Web Standard APIs (Request/Response), TypeScript-first
|
|
591
|
+
- **Framework compatibility** - Works across Node.js, Bun, Deno, and edge runtimes
|
|
592
|
+
|
|
593
|
+
### Future Framework Support
|
|
594
|
+
|
|
595
|
+
The codebase architecture is designed to support multiple frameworks. Adding Express or Fastify support would require:
|
|
596
|
+
- Implementing framework-specific route emitters (`emit-routes-express.ts`, etc.)
|
|
597
|
+
- Implementing framework-specific router creators (`emit-router-express.ts`, etc.)
|
|
598
|
+
|
|
599
|
+
Contributions to add additional framework support are welcome.
|
|
600
|
+
|
|
453
601
|
## License
|
|
454
602
|
|
|
455
603
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -578,7 +578,7 @@ function generateIncludeMethods(table, graph, opts, allTables) {
|
|
|
578
578
|
path: newPath,
|
|
579
579
|
isMany: newIsMany,
|
|
580
580
|
targets: newTargets,
|
|
581
|
-
returnType: `{
|
|
581
|
+
returnType: `PaginatedResponse<${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)}>`,
|
|
582
582
|
includeSpec: buildIncludeSpec(newPath)
|
|
583
583
|
});
|
|
584
584
|
methods.push({
|
|
@@ -618,7 +618,7 @@ function generateIncludeMethods(table, graph, opts, allTables) {
|
|
|
618
618
|
path: combinedPath,
|
|
619
619
|
isMany: [edge1.kind === "many", edge2.kind === "many"],
|
|
620
620
|
targets: [edge1.target, edge2.target],
|
|
621
|
-
returnType: `
|
|
621
|
+
returnType: `PaginatedResponse<Select${pascal(baseTableName)} & { ${type1}; ${type2} }>`,
|
|
622
622
|
includeSpec: { [key1]: true, [key2]: true }
|
|
623
623
|
});
|
|
624
624
|
methods.push({
|
|
@@ -785,7 +785,7 @@ function generateResourceWithSDK(table, model, graph, config) {
|
|
|
785
785
|
const endpoints = [];
|
|
786
786
|
sdkMethods.push({
|
|
787
787
|
name: "list",
|
|
788
|
-
signature: `list(params?: ListParams): Promise<{
|
|
788
|
+
signature: `list(params?: ListParams): Promise<PaginatedResponse<${Type}>>`,
|
|
789
789
|
description: `List ${tableName} with filtering, sorting, and pagination. Returns paginated results with metadata.`,
|
|
790
790
|
example: `// Get all ${tableName}
|
|
791
791
|
const result = await sdk.${tableName}.list();
|
|
@@ -812,7 +812,7 @@ const currentPage = Math.floor(filtered.offset / filtered.limit) + 1;`,
|
|
|
812
812
|
path: basePath,
|
|
813
813
|
description: `List all ${tableName} records with pagination metadata`,
|
|
814
814
|
queryParameters: generateQueryParams(table, enums),
|
|
815
|
-
responseBody: `{
|
|
815
|
+
responseBody: `PaginatedResponse<${Type}>`
|
|
816
816
|
});
|
|
817
817
|
if (hasSinglePK) {
|
|
818
818
|
sdkMethods.push({
|
|
@@ -1951,12 +1951,14 @@ function getDefaultComplexBlock(key) {
|
|
|
1951
1951
|
// process.env.API_KEY_1,
|
|
1952
1952
|
// process.env.API_KEY_2,
|
|
1953
1953
|
// ],
|
|
1954
|
-
//
|
|
1954
|
+
//
|
|
1955
1955
|
// // For JWT (HS256) authentication
|
|
1956
1956
|
// jwt: {
|
|
1957
|
-
//
|
|
1958
|
-
//
|
|
1959
|
-
//
|
|
1957
|
+
// services: [ // Array of services that can authenticate
|
|
1958
|
+
// { issuer: "web-app", secret: process.env.WEB_APP_SECRET },
|
|
1959
|
+
// { issuer: "mobile-app", secret: process.env.MOBILE_SECRET },
|
|
1960
|
+
// ],
|
|
1961
|
+
// audience: "my-api", // Optional: validate 'aud' claim
|
|
1960
1962
|
// }
|
|
1961
1963
|
// },`;
|
|
1962
1964
|
case "pull":
|
|
@@ -1983,6 +1985,8 @@ async function initCommand(args) {
|
|
|
1983
1985
|
console.log(`\uD83D\uDE80 Initializing postgresdk configuration...
|
|
1984
1986
|
`);
|
|
1985
1987
|
const forceError = args.includes("--force-error");
|
|
1988
|
+
const isApiSide = args.includes("--api");
|
|
1989
|
+
const isSdkSide = args.includes("--sdk");
|
|
1986
1990
|
const configPath = resolve(process.cwd(), "postgresdk.config.ts");
|
|
1987
1991
|
if (existsSync(configPath)) {
|
|
1988
1992
|
if (forceError) {
|
|
@@ -2102,21 +2106,65 @@ async function initCommand(args) {
|
|
|
2102
2106
|
}
|
|
2103
2107
|
return;
|
|
2104
2108
|
}
|
|
2109
|
+
let projectType;
|
|
2110
|
+
if (isApiSide && isSdkSide) {
|
|
2111
|
+
console.error("❌ Error: Cannot use both --api and --sdk flags");
|
|
2112
|
+
process.exit(1);
|
|
2113
|
+
} else if (isApiSide) {
|
|
2114
|
+
projectType = "api";
|
|
2115
|
+
} else if (isSdkSide) {
|
|
2116
|
+
projectType = "sdk";
|
|
2117
|
+
} else {
|
|
2118
|
+
const response = await prompts({
|
|
2119
|
+
type: "select",
|
|
2120
|
+
name: "projectType",
|
|
2121
|
+
message: "What type of project is this?",
|
|
2122
|
+
choices: [
|
|
2123
|
+
{
|
|
2124
|
+
title: "API-side (generating SDK from database)",
|
|
2125
|
+
value: "api",
|
|
2126
|
+
description: "You have a PostgreSQL database and want to generate an SDK"
|
|
2127
|
+
},
|
|
2128
|
+
{
|
|
2129
|
+
title: "SDK-side (consuming a remote SDK)",
|
|
2130
|
+
value: "sdk",
|
|
2131
|
+
description: "You want to pull and use an SDK from a remote API"
|
|
2132
|
+
}
|
|
2133
|
+
],
|
|
2134
|
+
initial: 0
|
|
2135
|
+
});
|
|
2136
|
+
projectType = response.projectType;
|
|
2137
|
+
if (!projectType) {
|
|
2138
|
+
console.log(`
|
|
2139
|
+
✅ Cancelled. No changes made.`);
|
|
2140
|
+
process.exit(0);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
const template = projectType === "api" ? CONFIG_TEMPLATE_API : CONFIG_TEMPLATE_SDK;
|
|
2105
2144
|
const envPath = resolve(process.cwd(), ".env");
|
|
2106
2145
|
const hasEnv = existsSync(envPath);
|
|
2107
2146
|
try {
|
|
2108
|
-
writeFileSync(configPath,
|
|
2147
|
+
writeFileSync(configPath, template, "utf-8");
|
|
2109
2148
|
console.log("✅ Created postgresdk.config.ts");
|
|
2110
2149
|
console.log(`
|
|
2111
2150
|
\uD83D\uDCDD Next steps:`);
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2151
|
+
if (projectType === "api") {
|
|
2152
|
+
console.log(" 1. Edit postgresdk.config.ts with your database connection");
|
|
2153
|
+
if (!hasEnv) {
|
|
2154
|
+
console.log(" 2. Consider creating a .env file for sensitive values:");
|
|
2155
|
+
console.log(" DATABASE_URL=postgres://user:pass@localhost:5432/mydb");
|
|
2156
|
+
console.log(" API_KEY=your-secret-key");
|
|
2157
|
+
console.log(" JWT_SECRET=your-jwt-secret");
|
|
2158
|
+
}
|
|
2159
|
+
console.log(" 3. Run 'postgresdk generate' to create your SDK");
|
|
2160
|
+
} else {
|
|
2161
|
+
console.log(" 1. Edit postgresdk.config.ts with your API URL in pull.from");
|
|
2162
|
+
if (!hasEnv) {
|
|
2163
|
+
console.log(" 2. Consider creating a .env file if you need authentication:");
|
|
2164
|
+
console.log(" API_TOKEN=your-api-token");
|
|
2165
|
+
}
|
|
2166
|
+
console.log(" 3. Run 'postgresdk pull' to fetch your SDK");
|
|
2167
|
+
}
|
|
2120
2168
|
console.log(`
|
|
2121
2169
|
\uD83D\uDCA1 Tip: The config file has detailed comments for all options.`);
|
|
2122
2170
|
console.log(" Uncomment the options you want to customize.");
|
|
@@ -2125,8 +2173,8 @@ async function initCommand(args) {
|
|
|
2125
2173
|
process.exit(1);
|
|
2126
2174
|
}
|
|
2127
2175
|
}
|
|
2128
|
-
var
|
|
2129
|
-
* PostgreSDK Configuration
|
|
2176
|
+
var CONFIG_TEMPLATE_API = `/**
|
|
2177
|
+
* PostgreSDK Configuration (API-Side)
|
|
2130
2178
|
*
|
|
2131
2179
|
* This file configures how postgresdk generates your type-safe API and SDK
|
|
2132
2180
|
* from your PostgreSQL database schema.
|
|
@@ -2137,9 +2185,7 @@ var CONFIG_TEMPLATE = `/**
|
|
|
2137
2185
|
* 3. Start using your generated SDK!
|
|
2138
2186
|
*
|
|
2139
2187
|
* CLI COMMANDS:
|
|
2140
|
-
* postgresdk init Initialize this config file
|
|
2141
2188
|
* postgresdk generate Generate API and SDK from your database
|
|
2142
|
-
* postgresdk pull Pull SDK from a remote API
|
|
2143
2189
|
* postgresdk help Show help and examples
|
|
2144
2190
|
*
|
|
2145
2191
|
* Environment variables are automatically loaded from .env files.
|
|
@@ -2165,16 +2211,17 @@ export default {
|
|
|
2165
2211
|
// schema: "public",
|
|
2166
2212
|
|
|
2167
2213
|
/**
|
|
2168
|
-
* Output directory for
|
|
2169
|
-
*
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
*
|
|
2175
|
-
*
|
|
2214
|
+
* Output directory for generated code
|
|
2215
|
+
*
|
|
2216
|
+
* Simple usage (same directory for both):
|
|
2217
|
+
* outDir: "./api"
|
|
2218
|
+
*
|
|
2219
|
+
* Separate directories for client and server:
|
|
2220
|
+
* outDir: { client: "./sdk", server: "./api" }
|
|
2221
|
+
*
|
|
2222
|
+
* Default: { client: "./api/client", server: "./api/server" }
|
|
2176
2223
|
*/
|
|
2177
|
-
//
|
|
2224
|
+
// outDir: "./api",
|
|
2178
2225
|
|
|
2179
2226
|
// ========== ADVANCED OPTIONS ==========
|
|
2180
2227
|
|
|
@@ -2254,23 +2301,42 @@ export default {
|
|
|
2254
2301
|
//
|
|
2255
2302
|
// // For JWT (HS256) authentication
|
|
2256
2303
|
// jwt: {
|
|
2257
|
-
//
|
|
2258
|
-
//
|
|
2259
|
-
//
|
|
2304
|
+
// services: [ // Array of services that can authenticate
|
|
2305
|
+
// { issuer: "web-app", secret: process.env.WEB_APP_SECRET },
|
|
2306
|
+
// { issuer: "mobile-app", secret: process.env.MOBILE_SECRET },
|
|
2307
|
+
// ],
|
|
2308
|
+
// audience: "my-api", // Optional: validate 'aud' claim
|
|
2260
2309
|
// }
|
|
2261
2310
|
// },
|
|
2311
|
+
};
|
|
2312
|
+
`, CONFIG_TEMPLATE_SDK = `/**
|
|
2313
|
+
* PostgreSDK Configuration (SDK-Side)
|
|
2314
|
+
*
|
|
2315
|
+
* This file configures how postgresdk pulls a generated SDK from a remote API.
|
|
2316
|
+
*
|
|
2317
|
+
* QUICK START:
|
|
2318
|
+
* 1. Update the 'pull.from' URL below
|
|
2319
|
+
* 2. Run: postgresdk pull
|
|
2320
|
+
* 3. Import and use the SDK!
|
|
2321
|
+
*
|
|
2322
|
+
* CLI COMMANDS:
|
|
2323
|
+
* postgresdk pull Pull SDK from a remote API
|
|
2324
|
+
* postgresdk help Show help and examples
|
|
2325
|
+
*
|
|
2326
|
+
* Environment variables are automatically loaded from .env files.
|
|
2327
|
+
*/
|
|
2262
2328
|
|
|
2263
|
-
|
|
2329
|
+
export default {
|
|
2330
|
+
// ========== SDK PULL CONFIGURATION ==========
|
|
2264
2331
|
|
|
2265
2332
|
/**
|
|
2266
2333
|
* Configuration for pulling SDK from a remote API
|
|
2267
|
-
* Used when running: postgresdk pull
|
|
2268
2334
|
*/
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2335
|
+
pull: {
|
|
2336
|
+
from: "https://api.myapp.com", // API URL to pull SDK from
|
|
2337
|
+
output: "./src/sdk", // Local directory for pulled SDK
|
|
2338
|
+
// token: process.env.API_TOKEN, // Optional authentication token
|
|
2339
|
+
},
|
|
2274
2340
|
};
|
|
2275
2341
|
`;
|
|
2276
2342
|
var init_cli_init = __esm(() => {
|
|
@@ -2333,7 +2399,7 @@ Example config file:`);
|
|
|
2333
2399
|
console.log(`\uD83D\uDCC1 Output directory: ${config.output}`);
|
|
2334
2400
|
try {
|
|
2335
2401
|
const headers = config.token ? { Authorization: `Bearer ${config.token}` } : {};
|
|
2336
|
-
const manifestRes = await fetch(`${config.from}/sdk/manifest`, { headers });
|
|
2402
|
+
const manifestRes = await fetch(`${config.from}/_psdk/sdk/manifest`, { headers });
|
|
2337
2403
|
if (!manifestRes.ok) {
|
|
2338
2404
|
throw new Error(`Failed to fetch SDK manifest: ${manifestRes.status} ${manifestRes.statusText}`);
|
|
2339
2405
|
}
|
|
@@ -2341,7 +2407,7 @@ Example config file:`);
|
|
|
2341
2407
|
console.log(`\uD83D\uDCE6 SDK version: ${manifest.version}`);
|
|
2342
2408
|
console.log(`\uD83D\uDCC5 Generated: ${manifest.generated}`);
|
|
2343
2409
|
console.log(`\uD83D\uDCC4 Files: ${manifest.files.length}`);
|
|
2344
|
-
const sdkRes = await fetch(`${config.from}/sdk/download`, { headers });
|
|
2410
|
+
const sdkRes = await fetch(`${config.from}/_psdk/sdk/download`, { headers });
|
|
2345
2411
|
if (!sdkRes.ok) {
|
|
2346
2412
|
throw new Error(`Failed to download SDK: ${sdkRes.status} ${sdkRes.statusText}`);
|
|
2347
2413
|
}
|
|
@@ -2735,6 +2801,31 @@ export type PaginationParams = z.infer<typeof PaginationParamsSchema>;
|
|
|
2735
2801
|
`;
|
|
2736
2802
|
}
|
|
2737
2803
|
|
|
2804
|
+
// src/emit-shared-types.ts
|
|
2805
|
+
function emitSharedTypes() {
|
|
2806
|
+
return `/**
|
|
2807
|
+
* Shared types used across all SDK operations
|
|
2808
|
+
*/
|
|
2809
|
+
|
|
2810
|
+
/**
|
|
2811
|
+
* Paginated response structure returned by list operations
|
|
2812
|
+
* @template T - The type of records in the data array
|
|
2813
|
+
*/
|
|
2814
|
+
export interface PaginatedResponse<T> {
|
|
2815
|
+
/** Array of records for the current page */
|
|
2816
|
+
data: T[];
|
|
2817
|
+
/** Total number of records matching the query (across all pages) */
|
|
2818
|
+
total: number;
|
|
2819
|
+
/** Maximum number of records per page */
|
|
2820
|
+
limit: number;
|
|
2821
|
+
/** Number of records skipped (for pagination) */
|
|
2822
|
+
offset: number;
|
|
2823
|
+
/** Whether there are more records available after this page */
|
|
2824
|
+
hasMore: boolean;
|
|
2825
|
+
}
|
|
2826
|
+
`;
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2738
2829
|
// src/emit-routes-hono.ts
|
|
2739
2830
|
init_utils();
|
|
2740
2831
|
function emitHonoRoutes(table, _graph, opts) {
|
|
@@ -2786,7 +2877,7 @@ const listSchema = z.object({
|
|
|
2786
2877
|
* @param deps.onRequest - Optional hook that runs before each request (for audit logging, RLS, etc.)
|
|
2787
2878
|
*/
|
|
2788
2879
|
export function register${Type}Routes(app: Hono, deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> }, onRequest?: (c: Context, pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> }) => Promise<void> }) {
|
|
2789
|
-
const base = "
|
|
2880
|
+
const base = "${opts.apiPathPrefix}/${fileTableName}";
|
|
2790
2881
|
|
|
2791
2882
|
// Create operation context
|
|
2792
2883
|
const ctx: coreOps.OperationContext = {
|
|
@@ -3003,7 +3094,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
3003
3094
|
* @returns The record with nested ${method.path.join(" and ")} if found, null otherwise
|
|
3004
3095
|
*/
|
|
3005
3096
|
async ${method.name}(pk: ${pkType}): Promise<${method.returnType}> {
|
|
3006
|
-
const results = await this.post<{
|
|
3097
|
+
const results = await this.post<PaginatedResponse<${baseReturnType}>>(\`\${this.resource}/list\`, {
|
|
3007
3098
|
where: ${pkWhere},
|
|
3008
3099
|
include: ${JSON.stringify(method.includeSpec)},
|
|
3009
3100
|
limit: 1
|
|
@@ -3034,6 +3125,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
3034
3125
|
*/
|
|
3035
3126
|
import { BaseClient } from "./base-client${ext}";
|
|
3036
3127
|
import type { Where } from "./where-types${ext}";
|
|
3128
|
+
import type { PaginatedResponse } from "./types/shared${ext}";
|
|
3037
3129
|
${typeImports}
|
|
3038
3130
|
${otherTableImports.join(`
|
|
3039
3131
|
`)}
|
|
@@ -3081,20 +3173,8 @@ export class ${Type}Client extends BaseClient {
|
|
|
3081
3173
|
where?: Where<Select${Type}>;
|
|
3082
3174
|
orderBy?: string | string[];
|
|
3083
3175
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3084
|
-
}): Promise<{
|
|
3085
|
-
|
|
3086
|
-
total: number;
|
|
3087
|
-
limit: number;
|
|
3088
|
-
offset: number;
|
|
3089
|
-
hasMore: boolean;
|
|
3090
|
-
}> {
|
|
3091
|
-
return this.post<{
|
|
3092
|
-
data: Select${Type}[];
|
|
3093
|
-
total: number;
|
|
3094
|
-
limit: number;
|
|
3095
|
-
offset: number;
|
|
3096
|
-
hasMore: boolean;
|
|
3097
|
-
}>(\`\${this.resource}/list\`, params ?? {});
|
|
3176
|
+
}): Promise<PaginatedResponse<Select${Type}>> {
|
|
3177
|
+
return this.post<PaginatedResponse<Select${Type}>>(\`\${this.resource}/list\`, params ?? {});
|
|
3098
3178
|
}
|
|
3099
3179
|
|
|
3100
3180
|
/**
|
|
@@ -3205,6 +3285,11 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
|
|
|
3205
3285
|
out += `export type { Insert${Type}, Update${Type}, Select${Type} } from "./types/${t.name}${ext}";
|
|
3206
3286
|
`;
|
|
3207
3287
|
}
|
|
3288
|
+
out += `
|
|
3289
|
+
// Shared types
|
|
3290
|
+
`;
|
|
3291
|
+
out += `export type { PaginatedResponse } from "./types/shared${ext}";
|
|
3292
|
+
`;
|
|
3208
3293
|
return out;
|
|
3209
3294
|
}
|
|
3210
3295
|
|
|
@@ -3845,14 +3930,12 @@ function emitAuth(cfgAuth) {
|
|
|
3845
3930
|
const strategy = cfgAuth?.strategy ?? "none";
|
|
3846
3931
|
const apiKeyHeader = cfgAuth?.apiKeyHeader ?? "x-api-key";
|
|
3847
3932
|
const apiKeys = cfgAuth?.apiKeys ?? [];
|
|
3848
|
-
const
|
|
3849
|
-
const jwtIssuer = cfgAuth?.jwt?.issuer ?? undefined;
|
|
3933
|
+
const jwtServices = cfgAuth?.jwt?.services ?? [];
|
|
3850
3934
|
const jwtAudience = cfgAuth?.jwt?.audience ?? undefined;
|
|
3851
3935
|
const STRATEGY = JSON.stringify(strategy);
|
|
3852
3936
|
const API_KEY_HEADER = JSON.stringify(apiKeyHeader);
|
|
3853
3937
|
const RAW_API_KEYS = JSON.stringify(apiKeys);
|
|
3854
|
-
const
|
|
3855
|
-
const JWT_ISSUER = jwtIssuer === undefined ? "undefined" : JSON.stringify(jwtIssuer);
|
|
3938
|
+
const JWT_SERVICES = JSON.stringify(jwtServices);
|
|
3856
3939
|
const JWT_AUDIENCE = jwtAudience === undefined ? "undefined" : JSON.stringify(jwtAudience);
|
|
3857
3940
|
return `/**
|
|
3858
3941
|
* AUTO-GENERATED FILE - DO NOT EDIT
|
|
@@ -3870,8 +3953,7 @@ const STRATEGY = ${STRATEGY} as "none" | "api-key" | "jwt-hs256";
|
|
|
3870
3953
|
const API_KEY_HEADER = ${API_KEY_HEADER} as string;
|
|
3871
3954
|
const RAW_API_KEYS = ${RAW_API_KEYS} as readonly string[];
|
|
3872
3955
|
|
|
3873
|
-
const
|
|
3874
|
-
const JWT_ISSUER = ${JWT_ISSUER} as string | undefined;
|
|
3956
|
+
const JWT_SERVICES = ${JWT_SERVICES} as ReadonlyArray<{ issuer: string; secret: string }>;
|
|
3875
3957
|
const JWT_AUDIENCE = ${JWT_AUDIENCE} as string | undefined;
|
|
3876
3958
|
// -------------------------------------
|
|
3877
3959
|
|
|
@@ -3912,7 +3994,12 @@ function resolveKeys(keys: readonly string[]): string[] {
|
|
|
3912
3994
|
}
|
|
3913
3995
|
|
|
3914
3996
|
const API_KEYS = resolveKeys(RAW_API_KEYS);
|
|
3915
|
-
|
|
3997
|
+
|
|
3998
|
+
// Resolve JWT service secrets from env vars if needed
|
|
3999
|
+
const RESOLVED_JWT_SERVICES = JWT_SERVICES.map(svc => ({
|
|
4000
|
+
issuer: svc.issuer,
|
|
4001
|
+
secret: resolveValue(svc.secret)
|
|
4002
|
+
}));
|
|
3916
4003
|
|
|
3917
4004
|
// Augment Hono context for DX
|
|
3918
4005
|
declare module "hono" {
|
|
@@ -3956,21 +4043,43 @@ export async function authMiddleware(c: Context, next: Next) {
|
|
|
3956
4043
|
}
|
|
3957
4044
|
const token = m[1];
|
|
3958
4045
|
|
|
3959
|
-
if (!
|
|
3960
|
-
log.error("JWT strategy configured but
|
|
4046
|
+
if (!RESOLVED_JWT_SERVICES.length) {
|
|
4047
|
+
log.error("JWT strategy configured but no services defined");
|
|
3961
4048
|
return c.json({ error: "Unauthorized" }, 401);
|
|
3962
4049
|
}
|
|
3963
4050
|
|
|
3964
4051
|
// Lazy import 'jose' so projects not using JWT don't need it at runtime.
|
|
3965
4052
|
try {
|
|
3966
|
-
const { jwtVerify } = await import("jose");
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
4053
|
+
const { decodeJwt, jwtVerify } = await import("jose");
|
|
4054
|
+
|
|
4055
|
+
// Decode without verification to extract issuer claim
|
|
4056
|
+
const unverifiedPayload = decodeJwt(token);
|
|
4057
|
+
const issuer = unverifiedPayload.iss;
|
|
4058
|
+
|
|
4059
|
+
if (!issuer) {
|
|
4060
|
+
log.error("JWT missing required 'iss' (issuer) claim");
|
|
4061
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
4062
|
+
}
|
|
4063
|
+
|
|
4064
|
+
// Find matching service by issuer
|
|
4065
|
+
const service = RESOLVED_JWT_SERVICES.find(s => s.issuer === issuer);
|
|
4066
|
+
if (!service) {
|
|
4067
|
+
log.error("Unknown JWT issuer:", issuer);
|
|
4068
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
if (!service.secret) {
|
|
4072
|
+
log.error("JWT service configured but secret is empty for issuer:", issuer);
|
|
4073
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
4074
|
+
}
|
|
4075
|
+
|
|
4076
|
+
// Verify JWT using the service's secret
|
|
4077
|
+
const key = new TextEncoder().encode(service.secret);
|
|
4078
|
+
log.debug("Verifying JWT from issuer:", issuer);
|
|
3970
4079
|
log.debug("Expected audience:", JWT_AUDIENCE);
|
|
3971
|
-
|
|
4080
|
+
|
|
3972
4081
|
const { payload } = await jwtVerify(token, key, {
|
|
3973
|
-
issuer:
|
|
4082
|
+
issuer: issuer,
|
|
3974
4083
|
audience: JWT_AUDIENCE || undefined,
|
|
3975
4084
|
});
|
|
3976
4085
|
|
|
@@ -3980,7 +4089,7 @@ export async function authMiddleware(c: Context, next: Next) {
|
|
|
3980
4089
|
sub: typeof payload.sub === "string" ? payload.sub : undefined,
|
|
3981
4090
|
claims: payload as any,
|
|
3982
4091
|
});
|
|
3983
|
-
log.debug("JWT verified successfully");
|
|
4092
|
+
log.debug("JWT verified successfully for issuer:", issuer);
|
|
3984
4093
|
return next();
|
|
3985
4094
|
} catch (verifyError: any) {
|
|
3986
4095
|
log.error("JWT verification failed:", verifyError?.message || verifyError);
|
|
@@ -4045,10 +4154,22 @@ ${hasAuth ? `export { authMiddleware } from "./auth${ext}";` : ""}
|
|
|
4045
4154
|
* const pg = new Client({ connectionString: process.env.DATABASE_URL });
|
|
4046
4155
|
* await pg.connect();
|
|
4047
4156
|
*
|
|
4048
|
-
* // OR using Neon driver
|
|
4157
|
+
* // OR using Neon driver
|
|
4049
4158
|
* import { Pool } from "@neondatabase/serverless";
|
|
4050
|
-
*
|
|
4051
|
-
*
|
|
4159
|
+
*
|
|
4160
|
+
* // For serverless (Vercel/Netlify) - one connection per instance
|
|
4161
|
+
* const pool = new Pool({
|
|
4162
|
+
* connectionString: process.env.DATABASE_URL!,
|
|
4163
|
+
* max: 1
|
|
4164
|
+
* });
|
|
4165
|
+
*
|
|
4166
|
+
* // For traditional servers - connection pooling
|
|
4167
|
+
* const pool = new Pool({
|
|
4168
|
+
* connectionString: process.env.DATABASE_URL!,
|
|
4169
|
+
* max: 10
|
|
4170
|
+
* });
|
|
4171
|
+
*
|
|
4172
|
+
* const pg = pool;
|
|
4052
4173
|
*
|
|
4053
4174
|
* // Mount all generated routes
|
|
4054
4175
|
* const app = new Hono();
|
|
@@ -4080,21 +4201,21 @@ export function createRouter(
|
|
|
4080
4201
|
|
|
4081
4202
|
// Register table routes
|
|
4082
4203
|
${registrations}
|
|
4083
|
-
|
|
4204
|
+
|
|
4084
4205
|
// SDK distribution endpoints
|
|
4085
|
-
router.get("/sdk/manifest", (c) => {
|
|
4206
|
+
router.get("/_psdk/sdk/manifest", (c) => {
|
|
4086
4207
|
return c.json({
|
|
4087
4208
|
version: SDK_MANIFEST.version,
|
|
4088
4209
|
generated: SDK_MANIFEST.generated,
|
|
4089
4210
|
files: Object.keys(SDK_MANIFEST.files)
|
|
4090
4211
|
});
|
|
4091
4212
|
});
|
|
4092
|
-
|
|
4093
|
-
router.get("/sdk/download", (c) => {
|
|
4213
|
+
|
|
4214
|
+
router.get("/_psdk/sdk/download", (c) => {
|
|
4094
4215
|
return c.json(SDK_MANIFEST);
|
|
4095
4216
|
});
|
|
4096
|
-
|
|
4097
|
-
router.get("/sdk/files/:path{.*}", (c) => {
|
|
4217
|
+
|
|
4218
|
+
router.get("/_psdk/sdk/files/:path{.*}", (c) => {
|
|
4098
4219
|
const path = c.req.param("path");
|
|
4099
4220
|
const content = SDK_MANIFEST.files[path as keyof typeof SDK_MANIFEST.files];
|
|
4100
4221
|
if (!content) {
|
|
@@ -4104,25 +4225,25 @@ ${registrations}
|
|
|
4104
4225
|
"Content-Type": "text/plain; charset=utf-8"
|
|
4105
4226
|
});
|
|
4106
4227
|
});
|
|
4107
|
-
|
|
4228
|
+
|
|
4108
4229
|
// API Contract endpoints - describes the entire API
|
|
4109
|
-
router.get("/
|
|
4230
|
+
router.get("/_psdk/contract", (c) => {
|
|
4110
4231
|
const format = c.req.query("format") || "json";
|
|
4111
|
-
|
|
4232
|
+
|
|
4112
4233
|
if (format === "markdown") {
|
|
4113
4234
|
return c.text(getContract("markdown") as string, 200, {
|
|
4114
4235
|
"Content-Type": "text/markdown; charset=utf-8"
|
|
4115
4236
|
});
|
|
4116
4237
|
}
|
|
4117
|
-
|
|
4238
|
+
|
|
4118
4239
|
return c.json(getContract("json"));
|
|
4119
4240
|
});
|
|
4120
|
-
|
|
4121
|
-
router.get("/
|
|
4241
|
+
|
|
4242
|
+
router.get("/_psdk/contract.json", (c) => {
|
|
4122
4243
|
return c.json(getContract("json"));
|
|
4123
4244
|
});
|
|
4124
|
-
|
|
4125
|
-
router.get("/
|
|
4245
|
+
|
|
4246
|
+
router.get("/_psdk/contract.md", (c) => {
|
|
4126
4247
|
return c.text(getContract("markdown") as string, 200, {
|
|
4127
4248
|
"Content-Type": "text/markdown; charset=utf-8"
|
|
4128
4249
|
});
|
|
@@ -5378,17 +5499,10 @@ function normalizeAuthConfig(input) {
|
|
|
5378
5499
|
};
|
|
5379
5500
|
}
|
|
5380
5501
|
if ("jwt" in input && input.jwt) {
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
};
|
|
5386
|
-
} else {
|
|
5387
|
-
return {
|
|
5388
|
-
strategy: "jwt-hs256",
|
|
5389
|
-
jwt: input.jwt
|
|
5390
|
-
};
|
|
5391
|
-
}
|
|
5502
|
+
return {
|
|
5503
|
+
strategy: "jwt-hs256",
|
|
5504
|
+
jwt: input.jwt
|
|
5505
|
+
};
|
|
5392
5506
|
}
|
|
5393
5507
|
return { strategy: "none" };
|
|
5394
5508
|
}
|
|
@@ -5404,8 +5518,18 @@ async function generate(configPath) {
|
|
|
5404
5518
|
const model = await introspect(cfg.connectionString, cfg.schema || "public");
|
|
5405
5519
|
console.log("\uD83D\uDD17 Building relationship graph...");
|
|
5406
5520
|
const graph = buildGraph(model);
|
|
5407
|
-
|
|
5408
|
-
|
|
5521
|
+
let serverDir;
|
|
5522
|
+
let originalClientDir;
|
|
5523
|
+
if (typeof cfg.outDir === "string") {
|
|
5524
|
+
serverDir = cfg.outDir;
|
|
5525
|
+
originalClientDir = cfg.outDir;
|
|
5526
|
+
} else if (cfg.outDir && typeof cfg.outDir === "object") {
|
|
5527
|
+
serverDir = cfg.outDir.server;
|
|
5528
|
+
originalClientDir = cfg.outDir.client;
|
|
5529
|
+
} else {
|
|
5530
|
+
serverDir = "./api/server";
|
|
5531
|
+
originalClientDir = "./api/client";
|
|
5532
|
+
}
|
|
5409
5533
|
const sameDirectory = serverDir === originalClientDir;
|
|
5410
5534
|
let clientDir = originalClientDir;
|
|
5411
5535
|
if (sameDirectory) {
|
|
@@ -5439,6 +5563,7 @@ async function generate(configPath) {
|
|
|
5439
5563
|
files.push({ path: join(serverDir, "include-spec.ts"), content: includeSpec });
|
|
5440
5564
|
files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
|
|
5441
5565
|
files.push({ path: join(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
|
|
5566
|
+
files.push({ path: join(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
|
|
5442
5567
|
files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
|
|
5443
5568
|
files.push({ path: join(clientDir, "where-types.ts"), content: emitWhereTypes() });
|
|
5444
5569
|
files.push({
|
|
@@ -5475,7 +5600,8 @@ async function generate(configPath) {
|
|
|
5475
5600
|
softDeleteColumn: cfg.softDeleteColumn || null,
|
|
5476
5601
|
includeMethodsDepth: cfg.includeMethodsDepth || 2,
|
|
5477
5602
|
authStrategy: normalizedAuth?.strategy,
|
|
5478
|
-
useJsExtensions: cfg.useJsExtensions
|
|
5603
|
+
useJsExtensions: cfg.useJsExtensions,
|
|
5604
|
+
apiPathPrefix: cfg.apiPathPrefix || "/v1"
|
|
5479
5605
|
});
|
|
5480
5606
|
} else {
|
|
5481
5607
|
throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
|
|
@@ -5573,6 +5699,17 @@ async function generate(configPath) {
|
|
|
5573
5699
|
console.log(` 2. Edit the script to configure your API server startup`);
|
|
5574
5700
|
console.log(` 3. Run tests: ${testDir}/run-tests.sh`);
|
|
5575
5701
|
}
|
|
5702
|
+
console.log(`
|
|
5703
|
+
\uD83D\uDCDA Usage:`);
|
|
5704
|
+
console.log(` Server (${serverFramework}):`);
|
|
5705
|
+
console.log(` import { createRouter } from "./${relative(process.cwd(), serverDir)}/router";`);
|
|
5706
|
+
console.log(` const api = createRouter({ pg });`);
|
|
5707
|
+
console.log(` app.route("/", api);`);
|
|
5708
|
+
console.log(`
|
|
5709
|
+
Client:`);
|
|
5710
|
+
console.log(` import { SDK } from "./${relative(process.cwd(), clientDir)}";`);
|
|
5711
|
+
console.log(` const sdk = new SDK({ baseUrl: "http://localhost:3000" });`);
|
|
5712
|
+
console.log(` const users = await sdk.users.list();`);
|
|
5576
5713
|
}
|
|
5577
5714
|
|
|
5578
5715
|
// src/cli.ts
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function emitSharedTypes(): string;
|
package/dist/index.js
CHANGED
|
@@ -577,7 +577,7 @@ function generateIncludeMethods(table, graph, opts, allTables) {
|
|
|
577
577
|
path: newPath,
|
|
578
578
|
isMany: newIsMany,
|
|
579
579
|
targets: newTargets,
|
|
580
|
-
returnType: `{
|
|
580
|
+
returnType: `PaginatedResponse<${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)}>`,
|
|
581
581
|
includeSpec: buildIncludeSpec(newPath)
|
|
582
582
|
});
|
|
583
583
|
methods.push({
|
|
@@ -617,7 +617,7 @@ function generateIncludeMethods(table, graph, opts, allTables) {
|
|
|
617
617
|
path: combinedPath,
|
|
618
618
|
isMany: [edge1.kind === "many", edge2.kind === "many"],
|
|
619
619
|
targets: [edge1.target, edge2.target],
|
|
620
|
-
returnType: `
|
|
620
|
+
returnType: `PaginatedResponse<Select${pascal(baseTableName)} & { ${type1}; ${type2} }>`,
|
|
621
621
|
includeSpec: { [key1]: true, [key2]: true }
|
|
622
622
|
});
|
|
623
623
|
methods.push({
|
|
@@ -784,7 +784,7 @@ function generateResourceWithSDK(table, model, graph, config) {
|
|
|
784
784
|
const endpoints = [];
|
|
785
785
|
sdkMethods.push({
|
|
786
786
|
name: "list",
|
|
787
|
-
signature: `list(params?: ListParams): Promise<{
|
|
787
|
+
signature: `list(params?: ListParams): Promise<PaginatedResponse<${Type}>>`,
|
|
788
788
|
description: `List ${tableName} with filtering, sorting, and pagination. Returns paginated results with metadata.`,
|
|
789
789
|
example: `// Get all ${tableName}
|
|
790
790
|
const result = await sdk.${tableName}.list();
|
|
@@ -811,7 +811,7 @@ const currentPage = Math.floor(filtered.offset / filtered.limit) + 1;`,
|
|
|
811
811
|
path: basePath,
|
|
812
812
|
description: `List all ${tableName} records with pagination metadata`,
|
|
813
813
|
queryParameters: generateQueryParams(table, enums),
|
|
814
|
-
responseBody: `{
|
|
814
|
+
responseBody: `PaginatedResponse<${Type}>`
|
|
815
815
|
});
|
|
816
816
|
if (hasSinglePK) {
|
|
817
817
|
sdkMethods.push({
|
|
@@ -1975,6 +1975,31 @@ export type PaginationParams = z.infer<typeof PaginationParamsSchema>;
|
|
|
1975
1975
|
`;
|
|
1976
1976
|
}
|
|
1977
1977
|
|
|
1978
|
+
// src/emit-shared-types.ts
|
|
1979
|
+
function emitSharedTypes() {
|
|
1980
|
+
return `/**
|
|
1981
|
+
* Shared types used across all SDK operations
|
|
1982
|
+
*/
|
|
1983
|
+
|
|
1984
|
+
/**
|
|
1985
|
+
* Paginated response structure returned by list operations
|
|
1986
|
+
* @template T - The type of records in the data array
|
|
1987
|
+
*/
|
|
1988
|
+
export interface PaginatedResponse<T> {
|
|
1989
|
+
/** Array of records for the current page */
|
|
1990
|
+
data: T[];
|
|
1991
|
+
/** Total number of records matching the query (across all pages) */
|
|
1992
|
+
total: number;
|
|
1993
|
+
/** Maximum number of records per page */
|
|
1994
|
+
limit: number;
|
|
1995
|
+
/** Number of records skipped (for pagination) */
|
|
1996
|
+
offset: number;
|
|
1997
|
+
/** Whether there are more records available after this page */
|
|
1998
|
+
hasMore: boolean;
|
|
1999
|
+
}
|
|
2000
|
+
`;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1978
2003
|
// src/emit-routes-hono.ts
|
|
1979
2004
|
init_utils();
|
|
1980
2005
|
function emitHonoRoutes(table, _graph, opts) {
|
|
@@ -2026,7 +2051,7 @@ const listSchema = z.object({
|
|
|
2026
2051
|
* @param deps.onRequest - Optional hook that runs before each request (for audit logging, RLS, etc.)
|
|
2027
2052
|
*/
|
|
2028
2053
|
export function register${Type}Routes(app: Hono, deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> }, onRequest?: (c: Context, pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> }) => Promise<void> }) {
|
|
2029
|
-
const base = "
|
|
2054
|
+
const base = "${opts.apiPathPrefix}/${fileTableName}";
|
|
2030
2055
|
|
|
2031
2056
|
// Create operation context
|
|
2032
2057
|
const ctx: coreOps.OperationContext = {
|
|
@@ -2243,7 +2268,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
2243
2268
|
* @returns The record with nested ${method.path.join(" and ")} if found, null otherwise
|
|
2244
2269
|
*/
|
|
2245
2270
|
async ${method.name}(pk: ${pkType}): Promise<${method.returnType}> {
|
|
2246
|
-
const results = await this.post<{
|
|
2271
|
+
const results = await this.post<PaginatedResponse<${baseReturnType}>>(\`\${this.resource}/list\`, {
|
|
2247
2272
|
where: ${pkWhere},
|
|
2248
2273
|
include: ${JSON.stringify(method.includeSpec)},
|
|
2249
2274
|
limit: 1
|
|
@@ -2274,6 +2299,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
2274
2299
|
*/
|
|
2275
2300
|
import { BaseClient } from "./base-client${ext}";
|
|
2276
2301
|
import type { Where } from "./where-types${ext}";
|
|
2302
|
+
import type { PaginatedResponse } from "./types/shared${ext}";
|
|
2277
2303
|
${typeImports}
|
|
2278
2304
|
${otherTableImports.join(`
|
|
2279
2305
|
`)}
|
|
@@ -2321,20 +2347,8 @@ export class ${Type}Client extends BaseClient {
|
|
|
2321
2347
|
where?: Where<Select${Type}>;
|
|
2322
2348
|
orderBy?: string | string[];
|
|
2323
2349
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
2324
|
-
}): Promise<{
|
|
2325
|
-
|
|
2326
|
-
total: number;
|
|
2327
|
-
limit: number;
|
|
2328
|
-
offset: number;
|
|
2329
|
-
hasMore: boolean;
|
|
2330
|
-
}> {
|
|
2331
|
-
return this.post<{
|
|
2332
|
-
data: Select${Type}[];
|
|
2333
|
-
total: number;
|
|
2334
|
-
limit: number;
|
|
2335
|
-
offset: number;
|
|
2336
|
-
hasMore: boolean;
|
|
2337
|
-
}>(\`\${this.resource}/list\`, params ?? {});
|
|
2350
|
+
}): Promise<PaginatedResponse<Select${Type}>> {
|
|
2351
|
+
return this.post<PaginatedResponse<Select${Type}>>(\`\${this.resource}/list\`, params ?? {});
|
|
2338
2352
|
}
|
|
2339
2353
|
|
|
2340
2354
|
/**
|
|
@@ -2445,6 +2459,11 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
|
|
|
2445
2459
|
out += `export type { Insert${Type}, Update${Type}, Select${Type} } from "./types/${t.name}${ext}";
|
|
2446
2460
|
`;
|
|
2447
2461
|
}
|
|
2462
|
+
out += `
|
|
2463
|
+
// Shared types
|
|
2464
|
+
`;
|
|
2465
|
+
out += `export type { PaginatedResponse } from "./types/shared${ext}";
|
|
2466
|
+
`;
|
|
2448
2467
|
return out;
|
|
2449
2468
|
}
|
|
2450
2469
|
|
|
@@ -3085,14 +3104,12 @@ function emitAuth(cfgAuth) {
|
|
|
3085
3104
|
const strategy = cfgAuth?.strategy ?? "none";
|
|
3086
3105
|
const apiKeyHeader = cfgAuth?.apiKeyHeader ?? "x-api-key";
|
|
3087
3106
|
const apiKeys = cfgAuth?.apiKeys ?? [];
|
|
3088
|
-
const
|
|
3089
|
-
const jwtIssuer = cfgAuth?.jwt?.issuer ?? undefined;
|
|
3107
|
+
const jwtServices = cfgAuth?.jwt?.services ?? [];
|
|
3090
3108
|
const jwtAudience = cfgAuth?.jwt?.audience ?? undefined;
|
|
3091
3109
|
const STRATEGY = JSON.stringify(strategy);
|
|
3092
3110
|
const API_KEY_HEADER = JSON.stringify(apiKeyHeader);
|
|
3093
3111
|
const RAW_API_KEYS = JSON.stringify(apiKeys);
|
|
3094
|
-
const
|
|
3095
|
-
const JWT_ISSUER = jwtIssuer === undefined ? "undefined" : JSON.stringify(jwtIssuer);
|
|
3112
|
+
const JWT_SERVICES = JSON.stringify(jwtServices);
|
|
3096
3113
|
const JWT_AUDIENCE = jwtAudience === undefined ? "undefined" : JSON.stringify(jwtAudience);
|
|
3097
3114
|
return `/**
|
|
3098
3115
|
* AUTO-GENERATED FILE - DO NOT EDIT
|
|
@@ -3110,8 +3127,7 @@ const STRATEGY = ${STRATEGY} as "none" | "api-key" | "jwt-hs256";
|
|
|
3110
3127
|
const API_KEY_HEADER = ${API_KEY_HEADER} as string;
|
|
3111
3128
|
const RAW_API_KEYS = ${RAW_API_KEYS} as readonly string[];
|
|
3112
3129
|
|
|
3113
|
-
const
|
|
3114
|
-
const JWT_ISSUER = ${JWT_ISSUER} as string | undefined;
|
|
3130
|
+
const JWT_SERVICES = ${JWT_SERVICES} as ReadonlyArray<{ issuer: string; secret: string }>;
|
|
3115
3131
|
const JWT_AUDIENCE = ${JWT_AUDIENCE} as string | undefined;
|
|
3116
3132
|
// -------------------------------------
|
|
3117
3133
|
|
|
@@ -3152,7 +3168,12 @@ function resolveKeys(keys: readonly string[]): string[] {
|
|
|
3152
3168
|
}
|
|
3153
3169
|
|
|
3154
3170
|
const API_KEYS = resolveKeys(RAW_API_KEYS);
|
|
3155
|
-
|
|
3171
|
+
|
|
3172
|
+
// Resolve JWT service secrets from env vars if needed
|
|
3173
|
+
const RESOLVED_JWT_SERVICES = JWT_SERVICES.map(svc => ({
|
|
3174
|
+
issuer: svc.issuer,
|
|
3175
|
+
secret: resolveValue(svc.secret)
|
|
3176
|
+
}));
|
|
3156
3177
|
|
|
3157
3178
|
// Augment Hono context for DX
|
|
3158
3179
|
declare module "hono" {
|
|
@@ -3196,21 +3217,43 @@ export async function authMiddleware(c: Context, next: Next) {
|
|
|
3196
3217
|
}
|
|
3197
3218
|
const token = m[1];
|
|
3198
3219
|
|
|
3199
|
-
if (!
|
|
3200
|
-
log.error("JWT strategy configured but
|
|
3220
|
+
if (!RESOLVED_JWT_SERVICES.length) {
|
|
3221
|
+
log.error("JWT strategy configured but no services defined");
|
|
3201
3222
|
return c.json({ error: "Unauthorized" }, 401);
|
|
3202
3223
|
}
|
|
3203
3224
|
|
|
3204
3225
|
// Lazy import 'jose' so projects not using JWT don't need it at runtime.
|
|
3205
3226
|
try {
|
|
3206
|
-
const { jwtVerify } = await import("jose");
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3227
|
+
const { decodeJwt, jwtVerify } = await import("jose");
|
|
3228
|
+
|
|
3229
|
+
// Decode without verification to extract issuer claim
|
|
3230
|
+
const unverifiedPayload = decodeJwt(token);
|
|
3231
|
+
const issuer = unverifiedPayload.iss;
|
|
3232
|
+
|
|
3233
|
+
if (!issuer) {
|
|
3234
|
+
log.error("JWT missing required 'iss' (issuer) claim");
|
|
3235
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
// Find matching service by issuer
|
|
3239
|
+
const service = RESOLVED_JWT_SERVICES.find(s => s.issuer === issuer);
|
|
3240
|
+
if (!service) {
|
|
3241
|
+
log.error("Unknown JWT issuer:", issuer);
|
|
3242
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
if (!service.secret) {
|
|
3246
|
+
log.error("JWT service configured but secret is empty for issuer:", issuer);
|
|
3247
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
// Verify JWT using the service's secret
|
|
3251
|
+
const key = new TextEncoder().encode(service.secret);
|
|
3252
|
+
log.debug("Verifying JWT from issuer:", issuer);
|
|
3210
3253
|
log.debug("Expected audience:", JWT_AUDIENCE);
|
|
3211
|
-
|
|
3254
|
+
|
|
3212
3255
|
const { payload } = await jwtVerify(token, key, {
|
|
3213
|
-
issuer:
|
|
3256
|
+
issuer: issuer,
|
|
3214
3257
|
audience: JWT_AUDIENCE || undefined,
|
|
3215
3258
|
});
|
|
3216
3259
|
|
|
@@ -3220,7 +3263,7 @@ export async function authMiddleware(c: Context, next: Next) {
|
|
|
3220
3263
|
sub: typeof payload.sub === "string" ? payload.sub : undefined,
|
|
3221
3264
|
claims: payload as any,
|
|
3222
3265
|
});
|
|
3223
|
-
log.debug("JWT verified successfully");
|
|
3266
|
+
log.debug("JWT verified successfully for issuer:", issuer);
|
|
3224
3267
|
return next();
|
|
3225
3268
|
} catch (verifyError: any) {
|
|
3226
3269
|
log.error("JWT verification failed:", verifyError?.message || verifyError);
|
|
@@ -3285,10 +3328,22 @@ ${hasAuth ? `export { authMiddleware } from "./auth${ext}";` : ""}
|
|
|
3285
3328
|
* const pg = new Client({ connectionString: process.env.DATABASE_URL });
|
|
3286
3329
|
* await pg.connect();
|
|
3287
3330
|
*
|
|
3288
|
-
* // OR using Neon driver
|
|
3331
|
+
* // OR using Neon driver
|
|
3289
3332
|
* import { Pool } from "@neondatabase/serverless";
|
|
3290
|
-
*
|
|
3291
|
-
*
|
|
3333
|
+
*
|
|
3334
|
+
* // For serverless (Vercel/Netlify) - one connection per instance
|
|
3335
|
+
* const pool = new Pool({
|
|
3336
|
+
* connectionString: process.env.DATABASE_URL!,
|
|
3337
|
+
* max: 1
|
|
3338
|
+
* });
|
|
3339
|
+
*
|
|
3340
|
+
* // For traditional servers - connection pooling
|
|
3341
|
+
* const pool = new Pool({
|
|
3342
|
+
* connectionString: process.env.DATABASE_URL!,
|
|
3343
|
+
* max: 10
|
|
3344
|
+
* });
|
|
3345
|
+
*
|
|
3346
|
+
* const pg = pool;
|
|
3292
3347
|
*
|
|
3293
3348
|
* // Mount all generated routes
|
|
3294
3349
|
* const app = new Hono();
|
|
@@ -3320,21 +3375,21 @@ export function createRouter(
|
|
|
3320
3375
|
|
|
3321
3376
|
// Register table routes
|
|
3322
3377
|
${registrations}
|
|
3323
|
-
|
|
3378
|
+
|
|
3324
3379
|
// SDK distribution endpoints
|
|
3325
|
-
router.get("/sdk/manifest", (c) => {
|
|
3380
|
+
router.get("/_psdk/sdk/manifest", (c) => {
|
|
3326
3381
|
return c.json({
|
|
3327
3382
|
version: SDK_MANIFEST.version,
|
|
3328
3383
|
generated: SDK_MANIFEST.generated,
|
|
3329
3384
|
files: Object.keys(SDK_MANIFEST.files)
|
|
3330
3385
|
});
|
|
3331
3386
|
});
|
|
3332
|
-
|
|
3333
|
-
router.get("/sdk/download", (c) => {
|
|
3387
|
+
|
|
3388
|
+
router.get("/_psdk/sdk/download", (c) => {
|
|
3334
3389
|
return c.json(SDK_MANIFEST);
|
|
3335
3390
|
});
|
|
3336
|
-
|
|
3337
|
-
router.get("/sdk/files/:path{.*}", (c) => {
|
|
3391
|
+
|
|
3392
|
+
router.get("/_psdk/sdk/files/:path{.*}", (c) => {
|
|
3338
3393
|
const path = c.req.param("path");
|
|
3339
3394
|
const content = SDK_MANIFEST.files[path as keyof typeof SDK_MANIFEST.files];
|
|
3340
3395
|
if (!content) {
|
|
@@ -3344,25 +3399,25 @@ ${registrations}
|
|
|
3344
3399
|
"Content-Type": "text/plain; charset=utf-8"
|
|
3345
3400
|
});
|
|
3346
3401
|
});
|
|
3347
|
-
|
|
3402
|
+
|
|
3348
3403
|
// API Contract endpoints - describes the entire API
|
|
3349
|
-
router.get("/
|
|
3404
|
+
router.get("/_psdk/contract", (c) => {
|
|
3350
3405
|
const format = c.req.query("format") || "json";
|
|
3351
|
-
|
|
3406
|
+
|
|
3352
3407
|
if (format === "markdown") {
|
|
3353
3408
|
return c.text(getContract("markdown") as string, 200, {
|
|
3354
3409
|
"Content-Type": "text/markdown; charset=utf-8"
|
|
3355
3410
|
});
|
|
3356
3411
|
}
|
|
3357
|
-
|
|
3412
|
+
|
|
3358
3413
|
return c.json(getContract("json"));
|
|
3359
3414
|
});
|
|
3360
|
-
|
|
3361
|
-
router.get("/
|
|
3415
|
+
|
|
3416
|
+
router.get("/_psdk/contract.json", (c) => {
|
|
3362
3417
|
return c.json(getContract("json"));
|
|
3363
3418
|
});
|
|
3364
|
-
|
|
3365
|
-
router.get("/
|
|
3419
|
+
|
|
3420
|
+
router.get("/_psdk/contract.md", (c) => {
|
|
3366
3421
|
return c.text(getContract("markdown") as string, 200, {
|
|
3367
3422
|
"Content-Type": "text/markdown; charset=utf-8"
|
|
3368
3423
|
});
|
|
@@ -4618,17 +4673,10 @@ function normalizeAuthConfig(input) {
|
|
|
4618
4673
|
};
|
|
4619
4674
|
}
|
|
4620
4675
|
if ("jwt" in input && input.jwt) {
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
};
|
|
4626
|
-
} else {
|
|
4627
|
-
return {
|
|
4628
|
-
strategy: "jwt-hs256",
|
|
4629
|
-
jwt: input.jwt
|
|
4630
|
-
};
|
|
4631
|
-
}
|
|
4676
|
+
return {
|
|
4677
|
+
strategy: "jwt-hs256",
|
|
4678
|
+
jwt: input.jwt
|
|
4679
|
+
};
|
|
4632
4680
|
}
|
|
4633
4681
|
return { strategy: "none" };
|
|
4634
4682
|
}
|
|
@@ -4644,8 +4692,18 @@ async function generate(configPath) {
|
|
|
4644
4692
|
const model = await introspect(cfg.connectionString, cfg.schema || "public");
|
|
4645
4693
|
console.log("\uD83D\uDD17 Building relationship graph...");
|
|
4646
4694
|
const graph = buildGraph(model);
|
|
4647
|
-
|
|
4648
|
-
|
|
4695
|
+
let serverDir;
|
|
4696
|
+
let originalClientDir;
|
|
4697
|
+
if (typeof cfg.outDir === "string") {
|
|
4698
|
+
serverDir = cfg.outDir;
|
|
4699
|
+
originalClientDir = cfg.outDir;
|
|
4700
|
+
} else if (cfg.outDir && typeof cfg.outDir === "object") {
|
|
4701
|
+
serverDir = cfg.outDir.server;
|
|
4702
|
+
originalClientDir = cfg.outDir.client;
|
|
4703
|
+
} else {
|
|
4704
|
+
serverDir = "./api/server";
|
|
4705
|
+
originalClientDir = "./api/client";
|
|
4706
|
+
}
|
|
4649
4707
|
const sameDirectory = serverDir === originalClientDir;
|
|
4650
4708
|
let clientDir = originalClientDir;
|
|
4651
4709
|
if (sameDirectory) {
|
|
@@ -4679,6 +4737,7 @@ async function generate(configPath) {
|
|
|
4679
4737
|
files.push({ path: join(serverDir, "include-spec.ts"), content: includeSpec });
|
|
4680
4738
|
files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
|
|
4681
4739
|
files.push({ path: join(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
|
|
4740
|
+
files.push({ path: join(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
|
|
4682
4741
|
files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
|
|
4683
4742
|
files.push({ path: join(clientDir, "where-types.ts"), content: emitWhereTypes() });
|
|
4684
4743
|
files.push({
|
|
@@ -4715,7 +4774,8 @@ async function generate(configPath) {
|
|
|
4715
4774
|
softDeleteColumn: cfg.softDeleteColumn || null,
|
|
4716
4775
|
includeMethodsDepth: cfg.includeMethodsDepth || 2,
|
|
4717
4776
|
authStrategy: normalizedAuth?.strategy,
|
|
4718
|
-
useJsExtensions: cfg.useJsExtensions
|
|
4777
|
+
useJsExtensions: cfg.useJsExtensions,
|
|
4778
|
+
apiPathPrefix: cfg.apiPathPrefix || "/v1"
|
|
4719
4779
|
});
|
|
4720
4780
|
} else {
|
|
4721
4781
|
throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
|
|
@@ -4813,6 +4873,17 @@ async function generate(configPath) {
|
|
|
4813
4873
|
console.log(` 2. Edit the script to configure your API server startup`);
|
|
4814
4874
|
console.log(` 3. Run tests: ${testDir}/run-tests.sh`);
|
|
4815
4875
|
}
|
|
4876
|
+
console.log(`
|
|
4877
|
+
\uD83D\uDCDA Usage:`);
|
|
4878
|
+
console.log(` Server (${serverFramework}):`);
|
|
4879
|
+
console.log(` import { createRouter } from "./${relative(process.cwd(), serverDir)}/router";`);
|
|
4880
|
+
console.log(` const api = createRouter({ pg });`);
|
|
4881
|
+
console.log(` app.route("/", api);`);
|
|
4882
|
+
console.log(`
|
|
4883
|
+
Client:`);
|
|
4884
|
+
console.log(` import { SDK } from "./${relative(process.cwd(), clientDir)}";`);
|
|
4885
|
+
console.log(` const sdk = new SDK({ baseUrl: "http://localhost:3000" });`);
|
|
4886
|
+
console.log(` const users = await sdk.users.list();`);
|
|
4816
4887
|
}
|
|
4817
4888
|
export {
|
|
4818
4889
|
generate
|
package/dist/types.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ export interface AuthConfig {
|
|
|
3
3
|
apiKeyHeader?: string;
|
|
4
4
|
apiKeys?: string[];
|
|
5
5
|
jwt?: {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
services: Array<{
|
|
7
|
+
issuer: string;
|
|
8
|
+
secret: string;
|
|
9
|
+
}>;
|
|
8
10
|
audience?: string;
|
|
9
11
|
};
|
|
10
12
|
}
|
|
@@ -12,22 +14,20 @@ export type AuthConfigInput = AuthConfig | {
|
|
|
12
14
|
apiKey?: string;
|
|
13
15
|
apiKeys?: string[];
|
|
14
16
|
apiKeyHeader?: string;
|
|
15
|
-
jwt?: string | {
|
|
16
|
-
sharedSecret?: string;
|
|
17
|
-
issuer?: string;
|
|
18
|
-
audience?: string;
|
|
19
|
-
};
|
|
20
17
|
};
|
|
21
18
|
export interface Config {
|
|
22
19
|
connectionString: string;
|
|
23
20
|
schema?: string;
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
outDir?: string | {
|
|
22
|
+
client: string;
|
|
23
|
+
server: string;
|
|
24
|
+
};
|
|
26
25
|
softDeleteColumn?: string | null;
|
|
27
26
|
dateType?: "date" | "string";
|
|
28
27
|
includeMethodsDepth?: number;
|
|
29
28
|
skipJunctionTables?: boolean;
|
|
30
29
|
serverFramework?: "hono" | "express" | "fastify";
|
|
30
|
+
apiPathPrefix?: string;
|
|
31
31
|
auth?: AuthConfigInput;
|
|
32
32
|
pull?: PullConfig;
|
|
33
33
|
useJsExtensions?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postgresdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
"zod": "^4.0.15"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
+
"@hono/node-server": "^1.19.7",
|
|
48
49
|
"@types/bun": "^1.2.20",
|
|
49
50
|
"@types/node": "^20.0.0",
|
|
50
51
|
"@types/pg": "^8.15.5",
|
|
51
52
|
"drizzle-kit": "^0.31.4",
|
|
52
53
|
"drizzle-orm": "^0.44.4",
|
|
53
54
|
"jose": "^6.0.12",
|
|
54
|
-
"typescript": "^5.5.0"
|
|
55
|
-
"vitest": "^3.2.4"
|
|
55
|
+
"typescript": "^5.5.0"
|
|
56
56
|
},
|
|
57
57
|
"author": "Ben Honda <ben@theadpharm.com>",
|
|
58
58
|
"license": "MIT",
|