postgresdk 0.1.1 → 0.1.2-alpha.0

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 CHANGED
@@ -1,6 +1,135 @@
1
1
  # postgresdk
2
2
 
3
- Generate a fully-typed, production-ready SDK from your PostgreSQL database schema. Automatically creates both server-side REST API routes and client-side SDK with TypeScript types, Zod validation, and support for complex relationships.
3
+ Turn your PostgreSQL database into a fully-typed, production-ready SDK in seconds.
4
+
5
+ ## What You Get
6
+
7
+ Start with your existing PostgreSQL database:
8
+
9
+ ```sql
10
+ CREATE TABLE authors (
11
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
12
+ name TEXT NOT NULL,
13
+ bio TEXT
14
+ );
15
+
16
+ CREATE TABLE books (
17
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
18
+ title TEXT NOT NULL,
19
+ author_id UUID REFERENCES authors(id),
20
+ published_at TIMESTAMPTZ
21
+ );
22
+
23
+ CREATE TABLE tags (
24
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
25
+ name TEXT UNIQUE NOT NULL
26
+ );
27
+
28
+ CREATE TABLE book_tags (
29
+ book_id UUID REFERENCES books(id),
30
+ tag_id UUID REFERENCES tags(id),
31
+ PRIMARY KEY (book_id, tag_id)
32
+ );
33
+ ```
34
+
35
+ Run one command:
36
+
37
+ ```bash
38
+ npx postgresdk
39
+ ```
40
+
41
+ Get a complete, type-safe SDK with:
42
+
43
+ ### 🎯 Client SDK with Full TypeScript Support
44
+
45
+ ```typescript
46
+ import { SDK } from "./generated/client";
47
+
48
+ const sdk = new SDK({
49
+ baseUrl: "http://localhost:3000",
50
+ auth: { apiKey: "your-key" } // Optional auth
51
+ });
52
+
53
+ // ✅ Fully typed - autocomplete everything!
54
+ const author = await sdk.authors.create({
55
+ name: "Jane Austen",
56
+ bio: "English novelist known for social commentary"
57
+ });
58
+
59
+ // ✅ Type-safe relationships with eager loading
60
+ const booksWithAuthor = await sdk.books.list({
61
+ include: {
62
+ author: true, // 1:N relationship
63
+ tags: true // M:N relationship
64
+ }
65
+ });
66
+
67
+ // ✅ Complex nested queries
68
+ const authorsWithEverything = await sdk.authors.list({
69
+ include: {
70
+ books: {
71
+ include: {
72
+ tags: true
73
+ }
74
+ }
75
+ }
76
+ });
77
+
78
+ // ✅ Built-in pagination & filtering
79
+ const recentBooks = await sdk.books.list({
80
+ where: { published_at: { gte: "2024-01-01" } },
81
+ orderBy: "published_at",
82
+ order: "desc",
83
+ limit: 10
84
+ });
85
+ ```
86
+
87
+ ### 🚀 Production-Ready REST API
88
+
89
+ ```typescript
90
+ import { Hono } from "hono";
91
+ import { Client } from "pg";
92
+ import { createRouter } from "./generated/server";
93
+
94
+ const app = new Hono();
95
+ const pg = new Client({ connectionString: process.env.DATABASE_URL });
96
+ await pg.connect();
97
+
98
+ // That's it! Full REST API with:
99
+ // - Input validation (Zod)
100
+ // - Error handling
101
+ // - Relationship loading
102
+ // - Auth middleware (if configured)
103
+ // - Type safety throughout
104
+
105
+ const api = createRouter({ pg });
106
+ app.route("/", api);
107
+
108
+ // GET /v1/authors
109
+ // POST /v1/authors
110
+ // GET /v1/authors/:id
111
+ // PATCH /v1/authors/:id
112
+ // DELETE /v1/authors/:id
113
+ // POST /v1/authors/list (with filtering & includes)
114
+ ```
115
+
116
+ ### 🔒 Type Safety Everywhere
117
+
118
+ ```typescript
119
+ // TypeScript catches errors at compile time
120
+ const book = await sdk.books.create({
121
+ title: "Pride and Prejudice",
122
+ author_id: "not-a-uuid", // ❌ Type error!
123
+ published_at: "invalid-date" // ❌ Type error!
124
+ });
125
+
126
+ // Generated Zod schemas for runtime validation
127
+ import { InsertBooksSchema } from "./generated/server/zod/books";
128
+
129
+ const validated = InsertBooksSchema.parse(requestBody);
130
+ ```
131
+
132
+ All from your existing database schema. No manual coding required.
4
133
 
5
134
  ## Features
6
135
 
@@ -312,29 +441,285 @@ const sdk = new SDK({
312
441
  });
313
442
  ```
314
443
 
315
- ## Server Integration
444
+ ## Server Integration with Hono
445
+
446
+ The generated code integrates seamlessly with [Hono](https://hono.dev/), a lightweight web framework for the Edge.
447
+
448
+ ### Basic Setup
316
449
 
317
- The generated server code is designed for [Hono](https://hono.dev/) but can be adapted to other frameworks:
450
+ postgresdk generates a `createRouter` function that returns a Hono router with all your routes:
318
451
 
319
452
  ```typescript
320
453
  import { Hono } from "hono";
321
454
  import { serve } from "@hono/node-server";
322
455
  import { Client } from "pg";
456
+ import { createRouter } from "./generated/server";
457
+
458
+ const app = new Hono();
459
+
460
+ // Database connection
461
+ const pg = new Client({ connectionString: process.env.DATABASE_URL });
462
+ await pg.connect();
463
+
464
+ // Mount all generated routes at once
465
+ const apiRouter = createRouter({ pg });
466
+ app.route("/", apiRouter);
467
+
468
+ // Start server
469
+ serve({ fetch: app.fetch, port: 3000 });
470
+ console.log("Server running on http://localhost:3000");
471
+ ```
472
+
473
+ ### Mounting Routes at Different Paths
474
+
475
+ The `createRouter` function returns a Hono router that can be mounted anywhere:
476
+
477
+ ```typescript
478
+ import { Hono } from "hono";
479
+ import { createRouter } from "./generated/server";
480
+
481
+ const app = new Hono();
482
+
483
+ // Your existing routes
484
+ app.get("/", (c) => c.json({ message: "Welcome" }));
485
+ app.get("/health", (c) => c.json({ status: "ok" }));
486
+
487
+ // Mount generated routes under /api
488
+ const apiRouter = createRouter({ pg });
489
+ app.route("/api", apiRouter); // Routes will be at /api/v1/users, /api/v1/posts, etc.
490
+
491
+ // Or mount under different version
492
+ app.route("/v2", apiRouter); // Routes will be at /v2/v1/users, /v2/v1/posts, etc.
493
+ ```
494
+
495
+ ### Alternative: Register Routes Directly
496
+
497
+ If you prefer to register routes directly on your app without a sub-router:
498
+
499
+ ```typescript
500
+ import { registerAllRoutes } from "./generated/server";
501
+
502
+ const app = new Hono();
503
+ const pg = new Client({ connectionString: process.env.DATABASE_URL });
504
+ await pg.connect();
505
+
506
+ // Register all routes directly on app
507
+ registerAllRoutes(app, { pg });
508
+ ```
323
509
 
324
- // Import generated route registrations
510
+ ### Selective Route Registration
511
+
512
+ You can also import and register individual routes:
513
+
514
+ ```typescript
515
+ import { registerUsersRoutes, registerPostsRoutes } from "./generated/server";
516
+
517
+ const app = new Hono();
518
+
519
+ // Only register specific routes
520
+ registerUsersRoutes(app, { pg });
521
+ registerPostsRoutes(app, { pg });
522
+
523
+ ### Adding to an Existing Hono App
524
+
525
+ ```typescript
526
+ import { Hono } from "hono";
527
+ import { cors } from "hono/cors";
528
+ import { logger } from "hono/logger";
529
+
530
+ // Your existing Hono app
531
+ const app = new Hono();
532
+
533
+ // Your existing middleware
534
+ app.use("*", cors());
535
+ app.use("*", logger());
536
+
537
+ // Your existing routes
538
+ app.get("/", (c) => c.json({ message: "Hello World" }));
539
+ app.get("/health", (c) => c.json({ status: "ok" }));
540
+
541
+ // Add postgresdk generated routes
542
+ const pg = new Client({ connectionString: process.env.DATABASE_URL });
543
+ await pg.connect();
544
+
545
+ // All generated routes are prefixed with /v1 by default
325
546
  import { registerUsersRoutes } from "./generated/server/routes/users";
326
547
  import { registerPostsRoutes } from "./generated/server/routes/posts";
327
548
 
549
+ registerUsersRoutes(app, { pg }); // Adds /v1/users/*
550
+ registerPostsRoutes(app, { pg }); // Adds /v1/posts/*
551
+
552
+ // Your routes continue to work alongside generated ones
553
+ app.get("/custom", (c) => c.json({ custom: true }));
554
+ ```
555
+
556
+ ### With Error Handling & Logging
557
+
558
+ ```typescript
559
+ import { Hono } from "hono";
560
+ import { HTTPException } from "hono/http-exception";
561
+
328
562
  const app = new Hono();
563
+
564
+ // Global error handling
565
+ app.onError((err, c) => {
566
+ if (err instanceof HTTPException) {
567
+ return err.getResponse();
568
+ }
569
+ console.error("Unhandled error:", err);
570
+ return c.json({ error: "Internal Server Error" }, 500);
571
+ });
572
+
573
+ // Request logging middleware
574
+ app.use("*", async (c, next) => {
575
+ const start = Date.now();
576
+ await next();
577
+ const ms = Date.now() - start;
578
+ console.log(`${c.req.method} ${c.req.path} - ${c.res.status} ${ms}ms`);
579
+ });
580
+
581
+ // Register generated routes with database
329
582
  const pg = new Client({ connectionString: process.env.DATABASE_URL });
330
583
  await pg.connect();
331
584
 
332
- // Register routes
585
+ import { registerUsersRoutes } from "./generated/server/routes/users";
333
586
  registerUsersRoutes(app, { pg });
334
- registerPostsRoutes(app, { pg });
587
+ ```
335
588
 
336
- // Start server
589
+ ### With Database Connection Pooling
590
+
591
+ For production, use connection pooling:
592
+
593
+ ```typescript
594
+ import { Pool } from "pg";
595
+ import { Hono } from "hono";
596
+
597
+ // Use a connection pool instead of a single client
598
+ const pool = new Pool({
599
+ connectionString: process.env.DATABASE_URL,
600
+ max: 20, // Maximum number of clients in the pool
601
+ idleTimeoutMillis: 30000,
602
+ connectionTimeoutMillis: 2000,
603
+ });
604
+
605
+ const app = new Hono();
606
+
607
+ // The generated routes work with both Client and Pool
608
+ import { registerUsersRoutes } from "./generated/server/routes/users";
609
+ import { registerPostsRoutes } from "./generated/server/routes/posts";
610
+
611
+ registerUsersRoutes(app, { pg: pool });
612
+ registerPostsRoutes(app, { pg: pool });
613
+
614
+ // Graceful shutdown
615
+ process.on("SIGTERM", async () => {
616
+ await pool.end();
617
+ process.exit(0);
618
+ });
619
+ ```
620
+
621
+ ### With Different Deployment Targets
622
+
623
+ ```typescript
624
+ // For Node.js
625
+ import { serve } from "@hono/node-server";
337
626
  serve({ fetch: app.fetch, port: 3000 });
627
+
628
+ // For Cloudflare Workers
629
+ export default app;
630
+
631
+ // For Vercel
632
+ import { handle } from "@hono/vercel";
633
+ export default handle(app);
634
+
635
+ // For AWS Lambda
636
+ import { handle } from "@hono/aws-lambda";
637
+ export const handler = handle(app);
638
+
639
+ // For Deno
640
+ Deno.serve(app.fetch);
641
+
642
+ // For Bun
643
+ export default {
644
+ port: 3000,
645
+ fetch: app.fetch,
646
+ };
647
+ ```
648
+
649
+ ### Complete Production Example
650
+
651
+ ```typescript
652
+ import { Hono } from "hono";
653
+ import { cors } from "hono/cors";
654
+ import { compress } from "hono/compress";
655
+ import { secureHeaders } from "hono/secure-headers";
656
+ import { serve } from "@hono/node-server";
657
+ import { Pool } from "pg";
658
+
659
+ // Import all generated route registrations
660
+ import { registerUsersRoutes } from "./generated/server/routes/users";
661
+ import { registerPostsRoutes } from "./generated/server/routes/posts";
662
+ import { registerCommentsRoutes } from "./generated/server/routes/comments";
663
+
664
+ // Create app with type safety
665
+ const app = new Hono();
666
+
667
+ // Production middleware stack
668
+ app.use("*", cors({
669
+ origin: process.env.ALLOWED_ORIGINS?.split(",") || "*",
670
+ credentials: true,
671
+ }));
672
+ app.use("*", compress());
673
+ app.use("*", secureHeaders());
674
+
675
+ // Health check
676
+ app.get("/health", (c) => c.json({
677
+ status: "ok",
678
+ timestamp: new Date().toISOString()
679
+ }));
680
+
681
+ // Database connection pool
682
+ const pool = new Pool({
683
+ connectionString: process.env.DATABASE_URL,
684
+ ssl: process.env.NODE_ENV === "production" ? { rejectUnauthorized: false } : false,
685
+ max: 20,
686
+ });
687
+
688
+ // Register all generated routes
689
+ registerUsersRoutes(app, { pg: pool });
690
+ registerPostsRoutes(app, { pg: pool });
691
+ registerCommentsRoutes(app, { pg: pool });
692
+
693
+ // 404 handler
694
+ app.notFound((c) => c.json({ error: "Not Found" }, 404));
695
+
696
+ // Global error handler
697
+ app.onError((err, c) => {
698
+ console.error(`Error ${c.req.method} ${c.req.path}:`, err);
699
+ return c.json({
700
+ error: process.env.NODE_ENV === "production"
701
+ ? "Internal Server Error"
702
+ : err.message
703
+ }, 500);
704
+ });
705
+
706
+ // Start server
707
+ const port = parseInt(process.env.PORT || "3000");
708
+ serve({
709
+ fetch: app.fetch,
710
+ port,
711
+ hostname: "0.0.0.0"
712
+ });
713
+
714
+ console.log(`Server running on http://localhost:${port}`);
715
+ console.log(`Environment: ${process.env.NODE_ENV || "development"}`);
716
+
717
+ // Graceful shutdown
718
+ process.on("SIGTERM", async () => {
719
+ console.log("SIGTERM received, closing connections...");
720
+ await pool.end();
721
+ process.exit(0);
722
+ });
338
723
  ```
339
724
 
340
725
  ## CLI Options
package/dist/cli.js CHANGED
@@ -1198,6 +1198,87 @@ export async function authMiddleware(c: Context, next: Next) {
1198
1198
  `;
1199
1199
  }
1200
1200
 
1201
+ // src/emit-server-index.ts
1202
+ function emitServerIndex(tables, hasAuth) {
1203
+ const tableNames = tables.map((t) => t.name).sort();
1204
+ const imports = tableNames.map((name) => {
1205
+ const Type = pascal(name);
1206
+ return `import { register${Type}Routes } from "./routes/${name}";`;
1207
+ }).join(`
1208
+ `);
1209
+ const registrations = tableNames.map((name) => {
1210
+ const Type = pascal(name);
1211
+ return ` register${Type}Routes(router, deps);`;
1212
+ }).join(`
1213
+ `);
1214
+ const reExports = tableNames.map((name) => {
1215
+ const Type = pascal(name);
1216
+ return `export { register${Type}Routes } from "./routes/${name}";`;
1217
+ }).join(`
1218
+ `);
1219
+ return `/* Generated. Do not edit. */
1220
+ import { Hono } from "hono";
1221
+ ${imports}
1222
+ ${hasAuth ? `export { authMiddleware } from "./auth";` : ""}
1223
+
1224
+ /**
1225
+ * Creates a Hono router with all generated routes that can be mounted into your existing app.
1226
+ *
1227
+ * @example
1228
+ * import { Hono } from "hono";
1229
+ * import { Client } from "pg";
1230
+ * import { createRouter } from "./generated/server";
1231
+ *
1232
+ * const app = new Hono();
1233
+ * const pg = new Client({ connectionString: process.env.DATABASE_URL });
1234
+ * await pg.connect();
1235
+ *
1236
+ * // Mount all generated routes under /api
1237
+ * const apiRouter = createRouter({ pg });
1238
+ * app.route("/api", apiRouter);
1239
+ *
1240
+ * // Or mount directly at root
1241
+ * const router = createRouter({ pg });
1242
+ * app.route("/", router);
1243
+ */
1244
+ export function createRouter(
1245
+ deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> } }
1246
+ ): Hono {
1247
+ const router = new Hono();
1248
+ ${registrations}
1249
+ return router;
1250
+ }
1251
+
1252
+ /**
1253
+ * Register all generated routes directly on an existing Hono app.
1254
+ *
1255
+ * @example
1256
+ * import { Hono } from "hono";
1257
+ * import { Client } from "pg";
1258
+ * import { registerAllRoutes } from "./generated/server";
1259
+ *
1260
+ * const app = new Hono();
1261
+ * const pg = new Client({ connectionString: process.env.DATABASE_URL });
1262
+ * await pg.connect();
1263
+ *
1264
+ * // Register all routes at once
1265
+ * registerAllRoutes(app, { pg });
1266
+ */
1267
+ export function registerAllRoutes(
1268
+ app: Hono,
1269
+ deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> } }
1270
+ ) {
1271
+ ${registrations.replace(/router/g, "app")}
1272
+ }
1273
+
1274
+ // Individual route registrations (for selective use)
1275
+ ${reExports}
1276
+
1277
+ // Re-export types and schemas for convenience
1278
+ export * from "./include-spec";
1279
+ `;
1280
+ }
1281
+
1201
1282
  // src/index.ts
1202
1283
  async function generate(configPath) {
1203
1284
  const configUrl = pathToFileURL(configPath).href;
@@ -1260,6 +1341,10 @@ async function generate(configPath) {
1260
1341
  path: join(clientDir, "index.ts"),
1261
1342
  content: emitClientIndex(Object.values(model.tables))
1262
1343
  });
1344
+ files.push({
1345
+ path: join(serverDir, "index.ts"),
1346
+ content: emitServerIndex(Object.values(model.tables), !!cfg.auth?.strategy && cfg.auth.strategy !== "none")
1347
+ });
1263
1348
  console.log("✍️ Writing files...");
1264
1349
  await writeFiles(files);
1265
1350
  console.log(`✅ Generated ${files.length} files`);
@@ -0,0 +1,5 @@
1
+ import type { Table } from "./introspect";
2
+ /**
3
+ * Emits the server index file that exports helper functions for route registration
4
+ */
5
+ export declare function emitServerIndex(tables: Table[], hasAuth: boolean): string;
package/dist/index.js CHANGED
@@ -1196,6 +1196,87 @@ export async function authMiddleware(c: Context, next: Next) {
1196
1196
  `;
1197
1197
  }
1198
1198
 
1199
+ // src/emit-server-index.ts
1200
+ function emitServerIndex(tables, hasAuth) {
1201
+ const tableNames = tables.map((t) => t.name).sort();
1202
+ const imports = tableNames.map((name) => {
1203
+ const Type = pascal(name);
1204
+ return `import { register${Type}Routes } from "./routes/${name}";`;
1205
+ }).join(`
1206
+ `);
1207
+ const registrations = tableNames.map((name) => {
1208
+ const Type = pascal(name);
1209
+ return ` register${Type}Routes(router, deps);`;
1210
+ }).join(`
1211
+ `);
1212
+ const reExports = tableNames.map((name) => {
1213
+ const Type = pascal(name);
1214
+ return `export { register${Type}Routes } from "./routes/${name}";`;
1215
+ }).join(`
1216
+ `);
1217
+ return `/* Generated. Do not edit. */
1218
+ import { Hono } from "hono";
1219
+ ${imports}
1220
+ ${hasAuth ? `export { authMiddleware } from "./auth";` : ""}
1221
+
1222
+ /**
1223
+ * Creates a Hono router with all generated routes that can be mounted into your existing app.
1224
+ *
1225
+ * @example
1226
+ * import { Hono } from "hono";
1227
+ * import { Client } from "pg";
1228
+ * import { createRouter } from "./generated/server";
1229
+ *
1230
+ * const app = new Hono();
1231
+ * const pg = new Client({ connectionString: process.env.DATABASE_URL });
1232
+ * await pg.connect();
1233
+ *
1234
+ * // Mount all generated routes under /api
1235
+ * const apiRouter = createRouter({ pg });
1236
+ * app.route("/api", apiRouter);
1237
+ *
1238
+ * // Or mount directly at root
1239
+ * const router = createRouter({ pg });
1240
+ * app.route("/", router);
1241
+ */
1242
+ export function createRouter(
1243
+ deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> } }
1244
+ ): Hono {
1245
+ const router = new Hono();
1246
+ ${registrations}
1247
+ return router;
1248
+ }
1249
+
1250
+ /**
1251
+ * Register all generated routes directly on an existing Hono app.
1252
+ *
1253
+ * @example
1254
+ * import { Hono } from "hono";
1255
+ * import { Client } from "pg";
1256
+ * import { registerAllRoutes } from "./generated/server";
1257
+ *
1258
+ * const app = new Hono();
1259
+ * const pg = new Client({ connectionString: process.env.DATABASE_URL });
1260
+ * await pg.connect();
1261
+ *
1262
+ * // Register all routes at once
1263
+ * registerAllRoutes(app, { pg });
1264
+ */
1265
+ export function registerAllRoutes(
1266
+ app: Hono,
1267
+ deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> } }
1268
+ ) {
1269
+ ${registrations.replace(/router/g, "app")}
1270
+ }
1271
+
1272
+ // Individual route registrations (for selective use)
1273
+ ${reExports}
1274
+
1275
+ // Re-export types and schemas for convenience
1276
+ export * from "./include-spec";
1277
+ `;
1278
+ }
1279
+
1199
1280
  // src/index.ts
1200
1281
  async function generate(configPath) {
1201
1282
  const configUrl = pathToFileURL(configPath).href;
@@ -1258,6 +1339,10 @@ async function generate(configPath) {
1258
1339
  path: join(clientDir, "index.ts"),
1259
1340
  content: emitClientIndex(Object.values(model.tables))
1260
1341
  });
1342
+ files.push({
1343
+ path: join(serverDir, "index.ts"),
1344
+ content: emitServerIndex(Object.values(model.tables), !!cfg.auth?.strategy && cfg.auth.strategy !== "none")
1345
+ });
1261
1346
  console.log("✍️ Writing files...");
1262
1347
  await writeFiles(files);
1263
1348
  console.log(`✅ Generated ${files.length} files`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.1.1",
3
+ "version": "0.1.2-alpha.0",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {