postgresdk 0.13.1 → 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 +203 -86
- package/dist/emit-routes-hono.d.ts +1 -0
- package/dist/index.js +99 -48
- 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
|
@@ -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
|
}
|
|
@@ -2811,7 +2877,7 @@ const listSchema = z.object({
|
|
|
2811
2877
|
* @param deps.onRequest - Optional hook that runs before each request (for audit logging, RLS, etc.)
|
|
2812
2878
|
*/
|
|
2813
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> }) {
|
|
2814
|
-
const base = "
|
|
2880
|
+
const base = "${opts.apiPathPrefix}/${fileTableName}";
|
|
2815
2881
|
|
|
2816
2882
|
// Create operation context
|
|
2817
2883
|
const ctx: coreOps.OperationContext = {
|
|
@@ -3864,14 +3930,12 @@ function emitAuth(cfgAuth) {
|
|
|
3864
3930
|
const strategy = cfgAuth?.strategy ?? "none";
|
|
3865
3931
|
const apiKeyHeader = cfgAuth?.apiKeyHeader ?? "x-api-key";
|
|
3866
3932
|
const apiKeys = cfgAuth?.apiKeys ?? [];
|
|
3867
|
-
const
|
|
3868
|
-
const jwtIssuer = cfgAuth?.jwt?.issuer ?? undefined;
|
|
3933
|
+
const jwtServices = cfgAuth?.jwt?.services ?? [];
|
|
3869
3934
|
const jwtAudience = cfgAuth?.jwt?.audience ?? undefined;
|
|
3870
3935
|
const STRATEGY = JSON.stringify(strategy);
|
|
3871
3936
|
const API_KEY_HEADER = JSON.stringify(apiKeyHeader);
|
|
3872
3937
|
const RAW_API_KEYS = JSON.stringify(apiKeys);
|
|
3873
|
-
const
|
|
3874
|
-
const JWT_ISSUER = jwtIssuer === undefined ? "undefined" : JSON.stringify(jwtIssuer);
|
|
3938
|
+
const JWT_SERVICES = JSON.stringify(jwtServices);
|
|
3875
3939
|
const JWT_AUDIENCE = jwtAudience === undefined ? "undefined" : JSON.stringify(jwtAudience);
|
|
3876
3940
|
return `/**
|
|
3877
3941
|
* AUTO-GENERATED FILE - DO NOT EDIT
|
|
@@ -3889,8 +3953,7 @@ const STRATEGY = ${STRATEGY} as "none" | "api-key" | "jwt-hs256";
|
|
|
3889
3953
|
const API_KEY_HEADER = ${API_KEY_HEADER} as string;
|
|
3890
3954
|
const RAW_API_KEYS = ${RAW_API_KEYS} as readonly string[];
|
|
3891
3955
|
|
|
3892
|
-
const
|
|
3893
|
-
const JWT_ISSUER = ${JWT_ISSUER} as string | undefined;
|
|
3956
|
+
const JWT_SERVICES = ${JWT_SERVICES} as ReadonlyArray<{ issuer: string; secret: string }>;
|
|
3894
3957
|
const JWT_AUDIENCE = ${JWT_AUDIENCE} as string | undefined;
|
|
3895
3958
|
// -------------------------------------
|
|
3896
3959
|
|
|
@@ -3931,7 +3994,12 @@ function resolveKeys(keys: readonly string[]): string[] {
|
|
|
3931
3994
|
}
|
|
3932
3995
|
|
|
3933
3996
|
const API_KEYS = resolveKeys(RAW_API_KEYS);
|
|
3934
|
-
|
|
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
|
+
}));
|
|
3935
4003
|
|
|
3936
4004
|
// Augment Hono context for DX
|
|
3937
4005
|
declare module "hono" {
|
|
@@ -3975,21 +4043,43 @@ export async function authMiddleware(c: Context, next: Next) {
|
|
|
3975
4043
|
}
|
|
3976
4044
|
const token = m[1];
|
|
3977
4045
|
|
|
3978
|
-
if (!
|
|
3979
|
-
log.error("JWT strategy configured but
|
|
4046
|
+
if (!RESOLVED_JWT_SERVICES.length) {
|
|
4047
|
+
log.error("JWT strategy configured but no services defined");
|
|
3980
4048
|
return c.json({ error: "Unauthorized" }, 401);
|
|
3981
4049
|
}
|
|
3982
4050
|
|
|
3983
4051
|
// Lazy import 'jose' so projects not using JWT don't need it at runtime.
|
|
3984
4052
|
try {
|
|
3985
|
-
const { jwtVerify } = await import("jose");
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
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);
|
|
3989
4079
|
log.debug("Expected audience:", JWT_AUDIENCE);
|
|
3990
|
-
|
|
4080
|
+
|
|
3991
4081
|
const { payload } = await jwtVerify(token, key, {
|
|
3992
|
-
issuer:
|
|
4082
|
+
issuer: issuer,
|
|
3993
4083
|
audience: JWT_AUDIENCE || undefined,
|
|
3994
4084
|
});
|
|
3995
4085
|
|
|
@@ -3999,7 +4089,7 @@ export async function authMiddleware(c: Context, next: Next) {
|
|
|
3999
4089
|
sub: typeof payload.sub === "string" ? payload.sub : undefined,
|
|
4000
4090
|
claims: payload as any,
|
|
4001
4091
|
});
|
|
4002
|
-
log.debug("JWT verified successfully");
|
|
4092
|
+
log.debug("JWT verified successfully for issuer:", issuer);
|
|
4003
4093
|
return next();
|
|
4004
4094
|
} catch (verifyError: any) {
|
|
4005
4095
|
log.error("JWT verification failed:", verifyError?.message || verifyError);
|
|
@@ -4064,10 +4154,22 @@ ${hasAuth ? `export { authMiddleware } from "./auth${ext}";` : ""}
|
|
|
4064
4154
|
* const pg = new Client({ connectionString: process.env.DATABASE_URL });
|
|
4065
4155
|
* await pg.connect();
|
|
4066
4156
|
*
|
|
4067
|
-
* // OR using Neon driver
|
|
4157
|
+
* // OR using Neon driver
|
|
4068
4158
|
* import { Pool } from "@neondatabase/serverless";
|
|
4069
|
-
*
|
|
4070
|
-
*
|
|
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;
|
|
4071
4173
|
*
|
|
4072
4174
|
* // Mount all generated routes
|
|
4073
4175
|
* const app = new Hono();
|
|
@@ -4099,21 +4201,21 @@ export function createRouter(
|
|
|
4099
4201
|
|
|
4100
4202
|
// Register table routes
|
|
4101
4203
|
${registrations}
|
|
4102
|
-
|
|
4204
|
+
|
|
4103
4205
|
// SDK distribution endpoints
|
|
4104
|
-
router.get("/sdk/manifest", (c) => {
|
|
4206
|
+
router.get("/_psdk/sdk/manifest", (c) => {
|
|
4105
4207
|
return c.json({
|
|
4106
4208
|
version: SDK_MANIFEST.version,
|
|
4107
4209
|
generated: SDK_MANIFEST.generated,
|
|
4108
4210
|
files: Object.keys(SDK_MANIFEST.files)
|
|
4109
4211
|
});
|
|
4110
4212
|
});
|
|
4111
|
-
|
|
4112
|
-
router.get("/sdk/download", (c) => {
|
|
4213
|
+
|
|
4214
|
+
router.get("/_psdk/sdk/download", (c) => {
|
|
4113
4215
|
return c.json(SDK_MANIFEST);
|
|
4114
4216
|
});
|
|
4115
|
-
|
|
4116
|
-
router.get("/sdk/files/:path{.*}", (c) => {
|
|
4217
|
+
|
|
4218
|
+
router.get("/_psdk/sdk/files/:path{.*}", (c) => {
|
|
4117
4219
|
const path = c.req.param("path");
|
|
4118
4220
|
const content = SDK_MANIFEST.files[path as keyof typeof SDK_MANIFEST.files];
|
|
4119
4221
|
if (!content) {
|
|
@@ -4123,25 +4225,25 @@ ${registrations}
|
|
|
4123
4225
|
"Content-Type": "text/plain; charset=utf-8"
|
|
4124
4226
|
});
|
|
4125
4227
|
});
|
|
4126
|
-
|
|
4228
|
+
|
|
4127
4229
|
// API Contract endpoints - describes the entire API
|
|
4128
|
-
router.get("/
|
|
4230
|
+
router.get("/_psdk/contract", (c) => {
|
|
4129
4231
|
const format = c.req.query("format") || "json";
|
|
4130
|
-
|
|
4232
|
+
|
|
4131
4233
|
if (format === "markdown") {
|
|
4132
4234
|
return c.text(getContract("markdown") as string, 200, {
|
|
4133
4235
|
"Content-Type": "text/markdown; charset=utf-8"
|
|
4134
4236
|
});
|
|
4135
4237
|
}
|
|
4136
|
-
|
|
4238
|
+
|
|
4137
4239
|
return c.json(getContract("json"));
|
|
4138
4240
|
});
|
|
4139
|
-
|
|
4140
|
-
router.get("/
|
|
4241
|
+
|
|
4242
|
+
router.get("/_psdk/contract.json", (c) => {
|
|
4141
4243
|
return c.json(getContract("json"));
|
|
4142
4244
|
});
|
|
4143
|
-
|
|
4144
|
-
router.get("/
|
|
4245
|
+
|
|
4246
|
+
router.get("/_psdk/contract.md", (c) => {
|
|
4145
4247
|
return c.text(getContract("markdown") as string, 200, {
|
|
4146
4248
|
"Content-Type": "text/markdown; charset=utf-8"
|
|
4147
4249
|
});
|
|
@@ -5397,17 +5499,10 @@ function normalizeAuthConfig(input) {
|
|
|
5397
5499
|
};
|
|
5398
5500
|
}
|
|
5399
5501
|
if ("jwt" in input && input.jwt) {
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
};
|
|
5405
|
-
} else {
|
|
5406
|
-
return {
|
|
5407
|
-
strategy: "jwt-hs256",
|
|
5408
|
-
jwt: input.jwt
|
|
5409
|
-
};
|
|
5410
|
-
}
|
|
5502
|
+
return {
|
|
5503
|
+
strategy: "jwt-hs256",
|
|
5504
|
+
jwt: input.jwt
|
|
5505
|
+
};
|
|
5411
5506
|
}
|
|
5412
5507
|
return { strategy: "none" };
|
|
5413
5508
|
}
|
|
@@ -5423,8 +5518,18 @@ async function generate(configPath) {
|
|
|
5423
5518
|
const model = await introspect(cfg.connectionString, cfg.schema || "public");
|
|
5424
5519
|
console.log("\uD83D\uDD17 Building relationship graph...");
|
|
5425
5520
|
const graph = buildGraph(model);
|
|
5426
|
-
|
|
5427
|
-
|
|
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
|
+
}
|
|
5428
5533
|
const sameDirectory = serverDir === originalClientDir;
|
|
5429
5534
|
let clientDir = originalClientDir;
|
|
5430
5535
|
if (sameDirectory) {
|
|
@@ -5495,7 +5600,8 @@ async function generate(configPath) {
|
|
|
5495
5600
|
softDeleteColumn: cfg.softDeleteColumn || null,
|
|
5496
5601
|
includeMethodsDepth: cfg.includeMethodsDepth || 2,
|
|
5497
5602
|
authStrategy: normalizedAuth?.strategy,
|
|
5498
|
-
useJsExtensions: cfg.useJsExtensions
|
|
5603
|
+
useJsExtensions: cfg.useJsExtensions,
|
|
5604
|
+
apiPathPrefix: cfg.apiPathPrefix || "/v1"
|
|
5499
5605
|
});
|
|
5500
5606
|
} else {
|
|
5501
5607
|
throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
|
|
@@ -5593,6 +5699,17 @@ async function generate(configPath) {
|
|
|
5593
5699
|
console.log(` 2. Edit the script to configure your API server startup`);
|
|
5594
5700
|
console.log(` 3. Run tests: ${testDir}/run-tests.sh`);
|
|
5595
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();`);
|
|
5596
5713
|
}
|
|
5597
5714
|
|
|
5598
5715
|
// src/cli.ts
|
package/dist/index.js
CHANGED
|
@@ -2051,7 +2051,7 @@ const listSchema = z.object({
|
|
|
2051
2051
|
* @param deps.onRequest - Optional hook that runs before each request (for audit logging, RLS, etc.)
|
|
2052
2052
|
*/
|
|
2053
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> }) {
|
|
2054
|
-
const base = "
|
|
2054
|
+
const base = "${opts.apiPathPrefix}/${fileTableName}";
|
|
2055
2055
|
|
|
2056
2056
|
// Create operation context
|
|
2057
2057
|
const ctx: coreOps.OperationContext = {
|
|
@@ -3104,14 +3104,12 @@ function emitAuth(cfgAuth) {
|
|
|
3104
3104
|
const strategy = cfgAuth?.strategy ?? "none";
|
|
3105
3105
|
const apiKeyHeader = cfgAuth?.apiKeyHeader ?? "x-api-key";
|
|
3106
3106
|
const apiKeys = cfgAuth?.apiKeys ?? [];
|
|
3107
|
-
const
|
|
3108
|
-
const jwtIssuer = cfgAuth?.jwt?.issuer ?? undefined;
|
|
3107
|
+
const jwtServices = cfgAuth?.jwt?.services ?? [];
|
|
3109
3108
|
const jwtAudience = cfgAuth?.jwt?.audience ?? undefined;
|
|
3110
3109
|
const STRATEGY = JSON.stringify(strategy);
|
|
3111
3110
|
const API_KEY_HEADER = JSON.stringify(apiKeyHeader);
|
|
3112
3111
|
const RAW_API_KEYS = JSON.stringify(apiKeys);
|
|
3113
|
-
const
|
|
3114
|
-
const JWT_ISSUER = jwtIssuer === undefined ? "undefined" : JSON.stringify(jwtIssuer);
|
|
3112
|
+
const JWT_SERVICES = JSON.stringify(jwtServices);
|
|
3115
3113
|
const JWT_AUDIENCE = jwtAudience === undefined ? "undefined" : JSON.stringify(jwtAudience);
|
|
3116
3114
|
return `/**
|
|
3117
3115
|
* AUTO-GENERATED FILE - DO NOT EDIT
|
|
@@ -3129,8 +3127,7 @@ const STRATEGY = ${STRATEGY} as "none" | "api-key" | "jwt-hs256";
|
|
|
3129
3127
|
const API_KEY_HEADER = ${API_KEY_HEADER} as string;
|
|
3130
3128
|
const RAW_API_KEYS = ${RAW_API_KEYS} as readonly string[];
|
|
3131
3129
|
|
|
3132
|
-
const
|
|
3133
|
-
const JWT_ISSUER = ${JWT_ISSUER} as string | undefined;
|
|
3130
|
+
const JWT_SERVICES = ${JWT_SERVICES} as ReadonlyArray<{ issuer: string; secret: string }>;
|
|
3134
3131
|
const JWT_AUDIENCE = ${JWT_AUDIENCE} as string | undefined;
|
|
3135
3132
|
// -------------------------------------
|
|
3136
3133
|
|
|
@@ -3171,7 +3168,12 @@ function resolveKeys(keys: readonly string[]): string[] {
|
|
|
3171
3168
|
}
|
|
3172
3169
|
|
|
3173
3170
|
const API_KEYS = resolveKeys(RAW_API_KEYS);
|
|
3174
|
-
|
|
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
|
+
}));
|
|
3175
3177
|
|
|
3176
3178
|
// Augment Hono context for DX
|
|
3177
3179
|
declare module "hono" {
|
|
@@ -3215,21 +3217,43 @@ export async function authMiddleware(c: Context, next: Next) {
|
|
|
3215
3217
|
}
|
|
3216
3218
|
const token = m[1];
|
|
3217
3219
|
|
|
3218
|
-
if (!
|
|
3219
|
-
log.error("JWT strategy configured but
|
|
3220
|
+
if (!RESOLVED_JWT_SERVICES.length) {
|
|
3221
|
+
log.error("JWT strategy configured but no services defined");
|
|
3220
3222
|
return c.json({ error: "Unauthorized" }, 401);
|
|
3221
3223
|
}
|
|
3222
3224
|
|
|
3223
3225
|
// Lazy import 'jose' so projects not using JWT don't need it at runtime.
|
|
3224
3226
|
try {
|
|
3225
|
-
const { jwtVerify } = await import("jose");
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
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);
|
|
3229
3253
|
log.debug("Expected audience:", JWT_AUDIENCE);
|
|
3230
|
-
|
|
3254
|
+
|
|
3231
3255
|
const { payload } = await jwtVerify(token, key, {
|
|
3232
|
-
issuer:
|
|
3256
|
+
issuer: issuer,
|
|
3233
3257
|
audience: JWT_AUDIENCE || undefined,
|
|
3234
3258
|
});
|
|
3235
3259
|
|
|
@@ -3239,7 +3263,7 @@ export async function authMiddleware(c: Context, next: Next) {
|
|
|
3239
3263
|
sub: typeof payload.sub === "string" ? payload.sub : undefined,
|
|
3240
3264
|
claims: payload as any,
|
|
3241
3265
|
});
|
|
3242
|
-
log.debug("JWT verified successfully");
|
|
3266
|
+
log.debug("JWT verified successfully for issuer:", issuer);
|
|
3243
3267
|
return next();
|
|
3244
3268
|
} catch (verifyError: any) {
|
|
3245
3269
|
log.error("JWT verification failed:", verifyError?.message || verifyError);
|
|
@@ -3304,10 +3328,22 @@ ${hasAuth ? `export { authMiddleware } from "./auth${ext}";` : ""}
|
|
|
3304
3328
|
* const pg = new Client({ connectionString: process.env.DATABASE_URL });
|
|
3305
3329
|
* await pg.connect();
|
|
3306
3330
|
*
|
|
3307
|
-
* // OR using Neon driver
|
|
3331
|
+
* // OR using Neon driver
|
|
3308
3332
|
* import { Pool } from "@neondatabase/serverless";
|
|
3309
|
-
*
|
|
3310
|
-
*
|
|
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;
|
|
3311
3347
|
*
|
|
3312
3348
|
* // Mount all generated routes
|
|
3313
3349
|
* const app = new Hono();
|
|
@@ -3339,21 +3375,21 @@ export function createRouter(
|
|
|
3339
3375
|
|
|
3340
3376
|
// Register table routes
|
|
3341
3377
|
${registrations}
|
|
3342
|
-
|
|
3378
|
+
|
|
3343
3379
|
// SDK distribution endpoints
|
|
3344
|
-
router.get("/sdk/manifest", (c) => {
|
|
3380
|
+
router.get("/_psdk/sdk/manifest", (c) => {
|
|
3345
3381
|
return c.json({
|
|
3346
3382
|
version: SDK_MANIFEST.version,
|
|
3347
3383
|
generated: SDK_MANIFEST.generated,
|
|
3348
3384
|
files: Object.keys(SDK_MANIFEST.files)
|
|
3349
3385
|
});
|
|
3350
3386
|
});
|
|
3351
|
-
|
|
3352
|
-
router.get("/sdk/download", (c) => {
|
|
3387
|
+
|
|
3388
|
+
router.get("/_psdk/sdk/download", (c) => {
|
|
3353
3389
|
return c.json(SDK_MANIFEST);
|
|
3354
3390
|
});
|
|
3355
|
-
|
|
3356
|
-
router.get("/sdk/files/:path{.*}", (c) => {
|
|
3391
|
+
|
|
3392
|
+
router.get("/_psdk/sdk/files/:path{.*}", (c) => {
|
|
3357
3393
|
const path = c.req.param("path");
|
|
3358
3394
|
const content = SDK_MANIFEST.files[path as keyof typeof SDK_MANIFEST.files];
|
|
3359
3395
|
if (!content) {
|
|
@@ -3363,25 +3399,25 @@ ${registrations}
|
|
|
3363
3399
|
"Content-Type": "text/plain; charset=utf-8"
|
|
3364
3400
|
});
|
|
3365
3401
|
});
|
|
3366
|
-
|
|
3402
|
+
|
|
3367
3403
|
// API Contract endpoints - describes the entire API
|
|
3368
|
-
router.get("/
|
|
3404
|
+
router.get("/_psdk/contract", (c) => {
|
|
3369
3405
|
const format = c.req.query("format") || "json";
|
|
3370
|
-
|
|
3406
|
+
|
|
3371
3407
|
if (format === "markdown") {
|
|
3372
3408
|
return c.text(getContract("markdown") as string, 200, {
|
|
3373
3409
|
"Content-Type": "text/markdown; charset=utf-8"
|
|
3374
3410
|
});
|
|
3375
3411
|
}
|
|
3376
|
-
|
|
3412
|
+
|
|
3377
3413
|
return c.json(getContract("json"));
|
|
3378
3414
|
});
|
|
3379
|
-
|
|
3380
|
-
router.get("/
|
|
3415
|
+
|
|
3416
|
+
router.get("/_psdk/contract.json", (c) => {
|
|
3381
3417
|
return c.json(getContract("json"));
|
|
3382
3418
|
});
|
|
3383
|
-
|
|
3384
|
-
router.get("/
|
|
3419
|
+
|
|
3420
|
+
router.get("/_psdk/contract.md", (c) => {
|
|
3385
3421
|
return c.text(getContract("markdown") as string, 200, {
|
|
3386
3422
|
"Content-Type": "text/markdown; charset=utf-8"
|
|
3387
3423
|
});
|
|
@@ -4637,17 +4673,10 @@ function normalizeAuthConfig(input) {
|
|
|
4637
4673
|
};
|
|
4638
4674
|
}
|
|
4639
4675
|
if ("jwt" in input && input.jwt) {
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
};
|
|
4645
|
-
} else {
|
|
4646
|
-
return {
|
|
4647
|
-
strategy: "jwt-hs256",
|
|
4648
|
-
jwt: input.jwt
|
|
4649
|
-
};
|
|
4650
|
-
}
|
|
4676
|
+
return {
|
|
4677
|
+
strategy: "jwt-hs256",
|
|
4678
|
+
jwt: input.jwt
|
|
4679
|
+
};
|
|
4651
4680
|
}
|
|
4652
4681
|
return { strategy: "none" };
|
|
4653
4682
|
}
|
|
@@ -4663,8 +4692,18 @@ async function generate(configPath) {
|
|
|
4663
4692
|
const model = await introspect(cfg.connectionString, cfg.schema || "public");
|
|
4664
4693
|
console.log("\uD83D\uDD17 Building relationship graph...");
|
|
4665
4694
|
const graph = buildGraph(model);
|
|
4666
|
-
|
|
4667
|
-
|
|
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
|
+
}
|
|
4668
4707
|
const sameDirectory = serverDir === originalClientDir;
|
|
4669
4708
|
let clientDir = originalClientDir;
|
|
4670
4709
|
if (sameDirectory) {
|
|
@@ -4735,7 +4774,8 @@ async function generate(configPath) {
|
|
|
4735
4774
|
softDeleteColumn: cfg.softDeleteColumn || null,
|
|
4736
4775
|
includeMethodsDepth: cfg.includeMethodsDepth || 2,
|
|
4737
4776
|
authStrategy: normalizedAuth?.strategy,
|
|
4738
|
-
useJsExtensions: cfg.useJsExtensions
|
|
4777
|
+
useJsExtensions: cfg.useJsExtensions,
|
|
4778
|
+
apiPathPrefix: cfg.apiPathPrefix || "/v1"
|
|
4739
4779
|
});
|
|
4740
4780
|
} else {
|
|
4741
4781
|
throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
|
|
@@ -4833,6 +4873,17 @@ async function generate(configPath) {
|
|
|
4833
4873
|
console.log(` 2. Edit the script to configure your API server startup`);
|
|
4834
4874
|
console.log(` 3. Run tests: ${testDir}/run-tests.sh`);
|
|
4835
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();`);
|
|
4836
4887
|
}
|
|
4837
4888
|
export {
|
|
4838
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",
|