postgresdk 0.14.2 → 0.14.4

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
@@ -4,7 +4,7 @@
4
4
 
5
5
  Generate a typed server/client SDK from your PostgreSQL database schema.
6
6
 
7
- ## See It In Action
7
+ ## What It Does
8
8
 
9
9
  **Your database:**
10
10
  ```sql
@@ -58,7 +58,11 @@ const filtered = await sdk.users.list({
58
58
  - 🎯 **Zero Config** - Works out of the box with sensible defaults
59
59
  - 📦 **Lightweight** - Minimal dependencies, optimized bundle size
60
60
 
61
- ## Installation
61
+ ---
62
+
63
+ ## Getting Started
64
+
65
+ ### Installation
62
66
 
63
67
  ```bash
64
68
  npm install -g postgresdk
@@ -73,7 +77,7 @@ bunx postgresdk generate
73
77
 
74
78
  > **Note:** Currently only generates **Hono** server code. See [Supported Frameworks](#supported-frameworks) for details.
75
79
 
76
- ## Quick Start
80
+ ### Quick Start
77
81
 
78
82
  1. Initialize your project:
79
83
 
@@ -107,7 +111,7 @@ bunx postgresdk generate
107
111
  ```typescript
108
112
  import { Hono } from "hono";
109
113
  import { Client } from "pg";
110
- import { createRouter } from "./api/server/router";
114
+ import { createRouter } from "./api/server/router"; // Path depends on your outDir config
111
115
 
112
116
  const app = new Hono();
113
117
  const pg = new Client({ connectionString: "..." });
@@ -120,7 +124,7 @@ app.route("/", api);
120
124
  5. Use the client SDK:
121
125
 
122
126
  ```typescript
123
- import { SDK } from "./api/client";
127
+ import { SDK } from "./api/client"; // Path depends on your outDir config
124
128
 
125
129
  const sdk = new SDK({ baseUrl: "http://localhost:3000" });
126
130
 
@@ -131,7 +135,13 @@ await sdk.users.update(user.id, { name: "Alice Smith" });
131
135
  await sdk.users.delete(user.id);
132
136
  ```
133
137
 
134
- ## Configuration
138
+ ---
139
+
140
+ ## API Server Setup
141
+
142
+ > **Note:** Code examples in this section use default output paths (`./api/server/`, `./api/client/`). If you configure a custom `outDir`, adjust import paths accordingly.
143
+
144
+ ### Configuration
135
145
 
136
146
  Create a `postgresdk.config.ts` file in your project root:
137
147
 
@@ -148,7 +158,7 @@ export default {
148
158
  dateType: "date", // "date" | "string" - How to handle timestamps
149
159
  serverFramework: "hono", // Currently only hono is supported
150
160
  useJsExtensions: false, // Add .js to imports (for Vercel Edge, Deno)
151
-
161
+
152
162
  // Authentication (optional)
153
163
  auth: {
154
164
  apiKey: process.env.API_KEY, // Simple API key auth
@@ -159,7 +169,7 @@ export default {
159
169
  ]
160
170
  }
161
171
  },
162
-
172
+
163
173
  // Test generation (optional)
164
174
  tests: {
165
175
  generate: true, // Generate test files
@@ -169,155 +179,96 @@ export default {
169
179
  };
170
180
  ```
171
181
 
172
- ## Generated SDK Usage
182
+ ### Database Drivers
173
183
 
174
- ### CRUD Operations
184
+ The generated code works with any PostgreSQL client that implements a simple `query` interface:
175
185
 
176
- Every table gets a complete set of CRUD operations:
186
+ #### Node.js `pg` Driver
177
187
 
178
188
  ```typescript
179
- // Create
180
- const user = await sdk.users.create({ name: "Bob", email: "bob@example.com" });
181
-
182
- // Read
183
- const user = await sdk.users.getByPk(123);
184
- const result = await sdk.users.list();
185
- const users = result.data; // Array of users
189
+ import { Client } from "pg";
190
+ import { createRouter } from "./api/server/router";
186
191
 
187
- // Update
188
- const updated = await sdk.users.update(123, { name: "Robert" });
192
+ const pg = new Client({ connectionString: process.env.DATABASE_URL });
193
+ await pg.connect();
189
194
 
190
- // Delete
191
- const deleted = await sdk.users.delete(123);
195
+ const apiRouter = createRouter({ pg });
192
196
  ```
193
197
 
194
- ### Relationships & Eager Loading
195
-
196
- Automatically handles relationships with the `include` parameter:
198
+ #### Neon Serverless Driver (Edge-Compatible)
197
199
 
198
200
  ```typescript
199
- // 1:N relationship - Get authors with their books
200
- const authorsResult = await sdk.authors.list({
201
- include: { books: true }
202
- });
203
- const authors = authorsResult.data;
204
-
205
- // M:N relationship - Get books with their tags
206
- const booksResult = await sdk.books.list({
207
- include: { tags: true }
208
- });
209
- const books = booksResult.data;
201
+ import { Pool } from "@neondatabase/serverless";
202
+ import { createRouter } from "./api/server/router";
210
203
 
211
- // Nested includes - Get authors with books and their tags
212
- const nestedResult = await sdk.authors.list({
213
- include: {
214
- books: {
215
- include: {
216
- tags: true
217
- }
218
- }
219
- }
220
- });
221
- const authorsWithBooksAndTags = nestedResult.data;
204
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
205
+ const apiRouter = createRouter({ pg: pool });
222
206
  ```
223
207
 
224
- ### Filtering & Pagination
208
+ ### Server Integration
225
209
 
226
- All `list()` methods return pagination metadata:
210
+ postgresdk generates Hono-compatible routes:
227
211
 
228
212
  ```typescript
229
- const result = await sdk.users.list({
230
- where: { status: "active" },
231
- orderBy: "created_at",
232
- order: "desc",
233
- limit: 20,
234
- offset: 40
235
- });
213
+ import { Hono } from "hono";
214
+ import { serve } from "@hono/node-server";
215
+ import { Client } from "pg";
216
+ import { createRouter } from "./api/server/router";
236
217
 
237
- // Access results
238
- result.data; // User[] - array of records
239
- result.total; // number - total matching records
240
- result.limit; // number - page size used
241
- result.offset; // number - offset used
242
- result.hasMore; // boolean - more pages available
218
+ const app = new Hono();
243
219
 
244
- // Calculate pagination info
245
- const totalPages = Math.ceil(result.total / result.limit);
246
- const currentPage = Math.floor(result.offset / result.limit) + 1;
220
+ // Database connection
221
+ const pg = new Client({ connectionString: process.env.DATABASE_URL });
222
+ await pg.connect();
247
223
 
248
- // Multi-column sorting
249
- const sorted = await sdk.users.list({
250
- orderBy: ["status", "created_at"],
251
- order: ["asc", "desc"] // or use single direction: order: "asc"
252
- });
224
+ // Mount all generated routes
225
+ const apiRouter = createRouter({ pg });
226
+ app.route("/", apiRouter);
253
227
 
254
- // Advanced WHERE operators
255
- const filtered = await sdk.users.list({
256
- where: {
257
- age: { $gte: 18, $lt: 65 }, // Range queries
258
- email: { $ilike: '%@company.com' }, // Pattern matching
259
- status: { $in: ['active', 'pending'] }, // Array matching
260
- deleted_at: { $is: null } // NULL checks
261
- }
262
- });
263
- // filtered.total respects WHERE clause for accurate counts
228
+ // Start server
229
+ serve({ fetch: app.fetch, port: 3000 });
230
+ ```
264
231
 
265
- // OR logic - match any condition
266
- const results = await sdk.users.list({
267
- where: {
268
- $or: [
269
- { email: { $ilike: '%@gmail.com' } },
270
- { email: { $ilike: '%@yahoo.com' } },
271
- { status: 'premium' }
272
- ]
273
- }
274
- });
232
+ #### Request-Level Middleware (onRequest Hook)
275
233
 
276
- // Complex queries with AND/OR
277
- const complex = await sdk.users.list({
278
- where: {
279
- status: 'active', // Implicit AND at root level
280
- $or: [
281
- { age: { $lt: 18 } },
282
- { age: { $gt: 65 } }
283
- ]
284
- }
285
- });
234
+ The `onRequest` hook executes before every endpoint operation, enabling:
235
+ - Setting PostgreSQL session variables for audit logging
236
+ - Configuring Row-Level Security (RLS) based on authenticated user
237
+ - Request-level logging or monitoring
286
238
 
287
- // Nested logic (2 levels)
288
- const nested = await sdk.users.list({
289
- where: {
290
- $and: [
291
- {
292
- $or: [
293
- { firstName: { $ilike: '%john%' } },
294
- { lastName: { $ilike: '%john%' } }
295
- ]
296
- },
297
- { status: 'active' }
298
- ]
239
+ ```typescript
240
+ import { createRouter } from "./api/server/router";
241
+
242
+ const apiRouter = createRouter({
243
+ pg,
244
+ onRequest: async (c, pg) => {
245
+ // Access Hono context - fully type-safe
246
+ const auth = c.get('auth');
247
+
248
+ // Set PostgreSQL session variable for audit triggers
249
+ if (auth?.kind === 'jwt' && auth.claims?.sub) {
250
+ await pg.query(`SET LOCAL app.user_id = '${auth.claims.sub}'`);
251
+ }
252
+
253
+ // Or configure RLS policies
254
+ if (auth?.tenant_id) {
255
+ await pg.query(`SET LOCAL app.tenant_id = '${auth.tenant_id}'`);
256
+ }
299
257
  }
300
258
  });
301
-
302
- // Pagination with filtered results
303
- let allResults = [];
304
- let offset = 0;
305
- const limit = 50;
306
- do {
307
- const page = await sdk.users.list({ where: { status: 'active' }, limit, offset });
308
- allResults = allResults.concat(page.data);
309
- offset += limit;
310
- if (!page.hasMore) break;
311
- } while (true);
312
259
  ```
313
260
 
314
- See the generated SDK documentation for all available operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$ilike`, `$is`, `$isNot`, `$or`, `$and`.
261
+ The hook receives:
262
+ - `c` - Hono Context object with full type safety and IDE autocomplete
263
+ - `pg` - PostgreSQL client for setting session variables
264
+
265
+ **Note:** The router works with or without the `onRequest` hook - fully backward compatible.
315
266
 
316
- ## Authentication
267
+ ### Authentication
317
268
 
318
269
  postgresdk supports API key and JWT authentication:
319
270
 
320
- ### API Key Authentication
271
+ #### API Key Authentication
321
272
 
322
273
  ```typescript
323
274
  // postgresdk.config.ts
@@ -335,7 +286,7 @@ const sdk = new SDK({
335
286
  });
336
287
  ```
337
288
 
338
- ### JWT Authentication (HS256)
289
+ #### JWT Authentication (HS256)
339
290
 
340
291
  ```typescript
341
292
  // postgresdk.config.ts
@@ -374,94 +325,127 @@ const sdk = new SDK({
374
325
  });
375
326
  ```
376
327
 
377
- ## Database Drivers
328
+ #### Service-to-Service Authorization
378
329
 
379
- The generated code works with any PostgreSQL client that implements a simple `query` interface:
330
+ For service-to-service authorization (controlling which services can access which tables/actions), use JWT claims with the `onRequest` hook instead of built-in config scopes:
380
331
 
381
- ### Node.js `pg` Driver
332
+ **Why this approach?**
333
+ - **Standard**: Follows OAuth2/OIDC conventions (authorization in token claims, not API config)
334
+ - **Dynamic**: Different tokens can have different permissions (service accounts vs user sessions)
335
+ - **Flexible**: Supports table-level, row-level, and field-level authorization in one place
382
336
 
383
337
  ```typescript
384
- import { Client } from "pg";
385
- import { createRouter } from "./api/server/router";
338
+ // 1. Your auth service issues JWTs with scopes in claims
339
+ import { sign } from "jsonwebtoken";
386
340
 
387
- const pg = new Client({ connectionString: process.env.DATABASE_URL });
388
- await pg.connect();
389
-
390
- const apiRouter = createRouter({ pg });
391
- ```
341
+ const token = sign({
342
+ iss: "analytics-service",
343
+ sub: "service-123",
344
+ aud: "my-api",
345
+ scopes: ["users:read", "posts:read", "analytics:*"] // ← Authorization here
346
+ }, process.env.ANALYTICS_SECRET);
392
347
 
393
- ### Neon Serverless Driver (Edge-Compatible)
348
+ // 2. API config remains simple (authentication only)
349
+ export default {
350
+ connectionString: process.env.DATABASE_URL,
351
+ auth: {
352
+ strategy: "jwt-hs256",
353
+ jwt: {
354
+ services: [
355
+ { issuer: "web-app", secret: process.env.WEB_SECRET },
356
+ { issuer: "analytics-service", secret: process.env.ANALYTICS_SECRET }
357
+ ],
358
+ audience: "my-api"
359
+ }
360
+ }
361
+ };
394
362
 
395
- ```typescript
396
- import { Pool } from "@neondatabase/serverless";
363
+ // 3. Enforce scopes in onRequest hook
397
364
  import { createRouter } from "./api/server/router";
398
365
 
399
- const pool = new Pool({ connectionString: process.env.DATABASE_URL });
400
- const apiRouter = createRouter({ pg: pool });
401
- ```
366
+ function hasPermission(scopes: string[], table: string, method: string): boolean {
367
+ const action = { POST: "create", GET: "read", PUT: "update", DELETE: "delete" }[method];
402
368
 
403
- ### Request-Level Middleware (onRequest Hook)
369
+ return scopes.some(scope => {
370
+ const [scopeTable, scopeAction] = scope.split(":");
371
+ return (scopeTable === "*" || scopeTable === table) &&
372
+ (scopeAction === "*" || scopeAction === action);
373
+ });
374
+ }
404
375
 
405
- The `onRequest` hook executes before every endpoint operation, enabling:
406
- - Setting PostgreSQL session variables for audit logging
407
- - Configuring Row-Level Security (RLS) based on authenticated user
408
- - Request-level logging or monitoring
409
-
410
- ```typescript
411
- import { createRouter } from "./api/server/router";
376
+ function getTableFromPath(path: string): string {
377
+ // Extract table from path like "/v1/users" or "/v1/posts/123"
378
+ return path.split("/")[2];
379
+ }
412
380
 
413
381
  const apiRouter = createRouter({
414
382
  pg,
415
383
  onRequest: async (c, pg) => {
416
- // Access Hono context - fully type-safe
417
- const auth = c.get('auth');
384
+ const auth = c.get("auth");
418
385
 
419
- // Set PostgreSQL session variable for audit triggers
420
- if (auth?.kind === 'jwt' && auth.claims?.sub) {
421
- await pg.query(`SET LOCAL app.user_id = '${auth.claims.sub}'`);
386
+ // Extract scopes from JWT claims
387
+ const scopes = auth?.claims?.scopes || [];
388
+ const table = getTableFromPath(c.req.path);
389
+ const method = c.req.method;
390
+
391
+ // Enforce permission
392
+ if (!hasPermission(scopes, table, method)) {
393
+ throw new Error(`Forbidden: ${auth?.claims?.iss} lacks ${table}:${method}`);
422
394
  }
423
395
 
424
- // Or configure RLS policies
425
- if (auth?.tenant_id) {
426
- await pg.query(`SET LOCAL app.tenant_id = '${auth.tenant_id}'`);
396
+ // Optional: Set session variables for audit logging
397
+ if (auth?.claims?.sub) {
398
+ await pg.query(`SET LOCAL app.service_id = $1`, [auth.claims.sub]);
427
399
  }
428
400
  }
429
401
  });
430
402
  ```
431
403
 
432
- The hook receives:
433
- - `c` - Hono Context object with full type safety and IDE autocomplete
434
- - `pg` - PostgreSQL client for setting session variables
435
-
436
- **Note:** The router works with or without the `onRequest` hook - fully backward compatible.
437
-
438
- ## Server Integration
439
-
440
- postgresdk generates Hono-compatible routes:
404
+ **Advanced patterns:**
441
405
 
442
406
  ```typescript
443
- import { Hono } from "hono";
444
- import { serve } from "@hono/node-server";
445
- import { Client } from "pg";
446
- import { createRouter } from "./api/server/router";
447
-
448
- const app = new Hono();
449
-
450
- // Database connection
451
- const pg = new Client({ connectionString: process.env.DATABASE_URL });
452
- await pg.connect();
453
-
454
- // Mount all generated routes
455
- const apiRouter = createRouter({ pg });
456
- app.route("/", apiRouter);
407
+ // Row-level security (RLS)
408
+ onRequest: async (c, pg) => {
409
+ const auth = c.get("auth");
410
+ const userId = auth?.claims?.sub;
411
+
412
+ // Enable RLS for this session
413
+ await pg.query(`SET LOCAL app.user_id = $1`, [userId]);
414
+ // Now your RLS policies automatically filter rows
415
+ }
416
+
417
+ // Field-level restrictions
418
+ onRequest: async (c, pg) => {
419
+ const auth = c.get("auth");
420
+ const scopes = auth?.claims?.scopes || [];
421
+
422
+ // Store scopes in session for use in SELECT queries
423
+ await pg.query(`SET LOCAL app.scopes = $1`, [JSON.stringify(scopes)]);
424
+
425
+ // Your stored procedures/views can read app.scopes to hide sensitive fields
426
+ }
427
+
428
+ // Complex business logic
429
+ onRequest: async (c, pg) => {
430
+ const auth = c.get("auth");
431
+ const table = getTableFromPath(c.req.path);
432
+
433
+ // Custom rules per service
434
+ if (auth?.claims?.iss === "analytics-service" && c.req.method !== "GET") {
435
+ throw new Error("Analytics service is read-only");
436
+ }
457
437
 
458
- // Start server
459
- serve({ fetch: app.fetch, port: 3000 });
438
+ // Time-based restrictions
439
+ const hour = new Date().getHours();
440
+ if (auth?.claims?.iss === "batch-processor" && hour >= 8 && hour <= 17) {
441
+ throw new Error("Batch jobs only run outside business hours");
442
+ }
443
+ }
460
444
  ```
461
445
 
462
- ## Deployment
446
+ ### Deployment
463
447
 
464
- ### Serverless (Vercel, Netlify, Cloudflare Workers)
448
+ #### Serverless (Vercel, Netlify, Cloudflare Workers)
465
449
 
466
450
  Use `max: 1` - each serverless instance should hold one connection:
467
451
 
@@ -478,7 +462,7 @@ const apiRouter = createRouter({ pg: pool });
478
462
 
479
463
  **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
464
 
481
- ### Traditional Servers (Railway, Render, VPS)
465
+ #### Traditional Servers (Railway, Render, VPS)
482
466
 
483
467
  Use connection pooling to reuse connections across requests:
484
468
 
@@ -495,16 +479,51 @@ const apiRouter = createRouter({ pg: pool });
495
479
 
496
480
  **Why `max: 10`?** Long-running servers handle many concurrent requests. Pooling prevents opening/closing connections for every request, significantly improving performance.
497
481
 
498
- ## SDK Distribution
482
+ ---
483
+
484
+ ## Client SDK
485
+
486
+ ### SDK Distribution
487
+
488
+ When you run `postgresdk generate`, the client SDK is automatically bundled into your server code and exposed via HTTP endpoints. This allows client applications to pull the SDK directly from your running API.
489
+
490
+ #### How It Works
499
491
 
500
- Your generated SDK can be pulled by client applications:
492
+ **On the API server:**
493
+ - SDK files are bundled into your server output directory as `sdk-bundle.ts` (embedded as strings)
494
+ - Auto-generated endpoints serve the SDK:
495
+ - `GET /_psdk/sdk/manifest` - Lists available files and metadata
496
+ - `GET /_psdk/sdk/download` - Returns complete SDK bundle
497
+ - `GET /_psdk/sdk/files/:path` - Individual file access
498
+
499
+ **On client applications:**
500
+
501
+ 1. Install postgresdk in your client project:
502
+
503
+ ```bash
504
+ npm install -D postgresdk
505
+ # or
506
+ bun install -D postgresdk
507
+ ```
508
+
509
+ 2. Pull the SDK from your API:
501
510
 
502
511
  ```bash
503
- # In your client app
504
- postgresdk pull --from=https://api.myapp.com --output=./src/sdk
512
+ npx postgresdk pull --from=https://api.myapp.com --output=./src/sdk
513
+ # or with Bun
514
+ bunx postgresdk pull --from=https://api.myapp.com --output=./src/sdk
505
515
  ```
506
516
 
507
- Or with configuration:
517
+ 3. Use the generated SDK with full TypeScript types:
518
+
519
+ ```typescript
520
+ import { SDK } from "./src/sdk";
521
+
522
+ const sdk = new SDK({ baseUrl: "https://api.myapp.com" });
523
+ const users = await sdk.users.list();
524
+ ```
525
+
526
+ **Using a config file (recommended):**
508
527
 
509
528
  ```typescript
510
529
  // postgresdk.config.ts in client app
@@ -512,37 +531,169 @@ export default {
512
531
  pull: {
513
532
  from: "https://api.myapp.com",
514
533
  output: "./src/sdk",
515
- token: process.env.API_TOKEN // Optional auth
534
+ token: process.env.API_TOKEN // Optional auth for protected APIs
516
535
  }
517
536
  };
518
537
  ```
519
538
 
520
- ## Generated Tests
539
+ Then run:
540
+ ```bash
541
+ npx postgresdk pull
542
+ # or
543
+ bunx postgresdk pull
544
+ ```
521
545
 
522
- Enable test generation in your config:
546
+ The SDK files are written directly to your client project, giving you full TypeScript autocomplete and type safety.
547
+
548
+ ### Using the SDK
549
+
550
+ #### CRUD Operations
551
+
552
+ Every table gets a complete set of CRUD operations:
523
553
 
524
554
  ```typescript
525
- export default {
526
- connectionString: process.env.DATABASE_URL,
527
- tests: {
528
- generate: true,
529
- output: "./api/tests",
530
- framework: "vitest"
555
+ // Create
556
+ const user = await sdk.users.create({ name: "Bob", email: "bob@example.com" });
557
+
558
+ // Read
559
+ const user = await sdk.users.getByPk(123);
560
+ const result = await sdk.users.list();
561
+ const users = result.data; // Array of users
562
+
563
+ // Update
564
+ const updated = await sdk.users.update(123, { name: "Robert" });
565
+
566
+ // Delete
567
+ const deleted = await sdk.users.delete(123);
568
+ ```
569
+
570
+ #### Relationships & Eager Loading
571
+
572
+ Automatically handles relationships with the `include` parameter:
573
+
574
+ ```typescript
575
+ // 1:N relationship - Get authors with their books
576
+ const authorsResult = await sdk.authors.list({
577
+ include: { books: true }
578
+ });
579
+ const authors = authorsResult.data;
580
+
581
+ // M:N relationship - Get books with their tags
582
+ const booksResult = await sdk.books.list({
583
+ include: { tags: true }
584
+ });
585
+ const books = booksResult.data;
586
+
587
+ // Nested includes - Get authors with books and their tags
588
+ const nestedResult = await sdk.authors.list({
589
+ include: {
590
+ books: {
591
+ include: {
592
+ tags: true
593
+ }
594
+ }
531
595
  }
532
- };
596
+ });
597
+ const authorsWithBooksAndTags = nestedResult.data;
533
598
  ```
534
599
 
535
- Run tests with the included Docker setup:
600
+ #### Filtering & Pagination
536
601
 
537
- ```bash
538
- chmod +x api/tests/run-tests.sh
539
- ./api/tests/run-tests.sh
602
+ All `list()` methods return pagination metadata:
540
603
 
541
- # Or with Bun's built-in test runner (if framework: "bun")
542
- bun test
604
+ ```typescript
605
+ const result = await sdk.users.list({
606
+ where: { status: "active" },
607
+ orderBy: "created_at",
608
+ order: "desc",
609
+ limit: 20,
610
+ offset: 40
611
+ });
612
+
613
+ // Access results
614
+ result.data; // User[] - array of records
615
+ result.total; // number - total matching records
616
+ result.limit; // number - page size used
617
+ result.offset; // number - offset used
618
+ result.hasMore; // boolean - more pages available
619
+
620
+ // Calculate pagination info
621
+ const totalPages = Math.ceil(result.total / result.limit);
622
+ const currentPage = Math.floor(result.offset / result.limit) + 1;
623
+
624
+ // Multi-column sorting
625
+ const sorted = await sdk.users.list({
626
+ orderBy: ["status", "created_at"],
627
+ order: ["asc", "desc"] // or use single direction: order: "asc"
628
+ });
629
+
630
+ // Advanced WHERE operators
631
+ const filtered = await sdk.users.list({
632
+ where: {
633
+ age: { $gte: 18, $lt: 65 }, // Range queries
634
+ email: { $ilike: '%@company.com' }, // Pattern matching
635
+ status: { $in: ['active', 'pending'] }, // Array matching
636
+ deleted_at: { $is: null } // NULL checks
637
+ }
638
+ });
639
+ // filtered.total respects WHERE clause for accurate counts
640
+
641
+ // OR logic - match any condition
642
+ const results = await sdk.users.list({
643
+ where: {
644
+ $or: [
645
+ { email: { $ilike: '%@gmail.com' } },
646
+ { email: { $ilike: '%@yahoo.com' } },
647
+ { status: 'premium' }
648
+ ]
649
+ }
650
+ });
651
+
652
+ // Complex queries with AND/OR
653
+ const complex = await sdk.users.list({
654
+ where: {
655
+ status: 'active', // Implicit AND at root level
656
+ $or: [
657
+ { age: { $lt: 18 } },
658
+ { age: { $gt: 65 } }
659
+ ]
660
+ }
661
+ });
662
+
663
+ // Nested logic (2 levels)
664
+ const nested = await sdk.users.list({
665
+ where: {
666
+ $and: [
667
+ {
668
+ $or: [
669
+ { firstName: { $ilike: '%john%' } },
670
+ { lastName: { $ilike: '%john%' } }
671
+ ]
672
+ },
673
+ { status: 'active' }
674
+ ]
675
+ }
676
+ });
677
+
678
+ // Pagination with filtered results
679
+ let allResults = [];
680
+ let offset = 0;
681
+ const limit = 50;
682
+ do {
683
+ const page = await sdk.users.list({ where: { status: 'active' }, limit, offset });
684
+ allResults = allResults.concat(page.data);
685
+ offset += limit;
686
+ if (!page.hasMore) break;
687
+ } while (true);
543
688
  ```
544
689
 
545
- ## CLI Commands
690
+ See the generated SDK documentation for all available operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$ilike`, `$is`, `$isNot`, `$or`, `$and`.
691
+
692
+ ---
693
+
694
+ ## Reference
695
+
696
+ ### CLI Commands
546
697
 
547
698
  ```bash
548
699
  postgresdk <command> [options]
@@ -570,19 +721,44 @@ Examples:
570
721
  postgresdk pull --from=https://api.com --output=./src/sdk
571
722
  ```
572
723
 
573
- ## Requirements
724
+ ### Generated Tests
725
+
726
+ Enable test generation in your config:
727
+
728
+ ```typescript
729
+ export default {
730
+ connectionString: process.env.DATABASE_URL,
731
+ tests: {
732
+ generate: true,
733
+ output: "./api/tests",
734
+ framework: "vitest"
735
+ }
736
+ };
737
+ ```
738
+
739
+ Run tests with the included Docker setup:
740
+
741
+ ```bash
742
+ chmod +x api/tests/run-tests.sh
743
+ ./api/tests/run-tests.sh
744
+
745
+ # Or with Bun's built-in test runner (if framework: "bun")
746
+ bun test
747
+ ```
748
+
749
+ ### Requirements
574
750
 
575
751
  - Node.js 18+
576
752
  - PostgreSQL 12+
577
753
  - TypeScript project (for using generated code)
578
754
 
579
- ## Supported Frameworks
755
+ ### Supported Frameworks
580
756
 
581
757
  **Currently, postgresdk only generates server code for Hono.**
582
758
 
583
759
  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
760
 
585
- ### Why Hono?
761
+ #### Why Hono?
586
762
 
587
763
  Hono was chosen as the initial framework because:
588
764
  - **Edge-first design** - Works seamlessly in serverless and edge environments (Cloudflare Workers, Vercel Edge, Deno Deploy)
@@ -590,7 +766,7 @@ Hono was chosen as the initial framework because:
590
766
  - **Modern patterns** - Web Standard APIs (Request/Response), TypeScript-first
591
767
  - **Framework compatibility** - Works across Node.js, Bun, Deno, and edge runtimes
592
768
 
593
- ### Future Framework Support
769
+ #### Future Framework Support
594
770
 
595
771
  The codebase architecture is designed to support multiple frameworks. Adding Express or Fastify support would require:
596
772
  - Implementing framework-specific route emitters (`emit-routes-express.ts`, etc.)
@@ -600,4 +776,4 @@ Contributions to add additional framework support are welcome.
600
776
 
601
777
  ## License
602
778
 
603
- MIT
779
+ MIT
package/dist/cli.js CHANGED
@@ -635,7 +635,15 @@ function generateIncludeMethods(table, graph, opts, allTables) {
635
635
  }
636
636
  }
637
637
  explore(baseTableName, [], [], [], new Set([baseTableName]), 1);
638
- return methods;
638
+ const seen = new Set;
639
+ const dedupedMethods = [];
640
+ for (const method of methods) {
641
+ if (!seen.has(method.name)) {
642
+ seen.add(method.name);
643
+ dedupedMethods.push(method);
644
+ }
645
+ }
646
+ return dedupedMethods;
639
647
  }
640
648
  var init_emit_include_methods = __esm(() => {
641
649
  init_utils();
@@ -1978,7 +1986,7 @@ var exports_cli_init = {};
1978
1986
  __export(exports_cli_init, {
1979
1987
  initCommand: () => initCommand
1980
1988
  });
1981
- import { existsSync, writeFileSync, readFileSync, copyFileSync } from "fs";
1989
+ import { existsSync as existsSync2, writeFileSync, readFileSync, copyFileSync } from "fs";
1982
1990
  import { resolve } from "path";
1983
1991
  import prompts from "prompts";
1984
1992
  async function initCommand(args) {
@@ -1988,7 +1996,7 @@ async function initCommand(args) {
1988
1996
  const isApiSide = args.includes("--api");
1989
1997
  const isSdkSide = args.includes("--sdk");
1990
1998
  const configPath = resolve(process.cwd(), "postgresdk.config.ts");
1991
- if (existsSync(configPath)) {
1999
+ if (existsSync2(configPath)) {
1992
2000
  if (forceError) {
1993
2001
  console.error("❌ Error: postgresdk.config.ts already exists");
1994
2002
  console.log(" To reinitialize, please remove or rename the existing file first.");
@@ -2142,7 +2150,7 @@ async function initCommand(args) {
2142
2150
  }
2143
2151
  const template = projectType === "api" ? CONFIG_TEMPLATE_API : CONFIG_TEMPLATE_SDK;
2144
2152
  const envPath = resolve(process.cwd(), ".env");
2145
- const hasEnv = existsSync(envPath);
2153
+ const hasEnv = existsSync2(envPath);
2146
2154
  try {
2147
2155
  writeFileSync(configPath, template, "utf-8");
2148
2156
  console.log("✅ Created postgresdk.config.ts");
@@ -2350,7 +2358,7 @@ __export(exports_cli_pull, {
2350
2358
  });
2351
2359
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
2352
2360
  import { join as join2, dirname as dirname2, resolve as resolve2 } from "path";
2353
- import { existsSync as existsSync2 } from "fs";
2361
+ import { existsSync as existsSync3 } from "fs";
2354
2362
  import { pathToFileURL as pathToFileURL2 } from "url";
2355
2363
  async function pullCommand(args) {
2356
2364
  let configPath = "postgresdk.config.ts";
@@ -2360,7 +2368,7 @@ async function pullCommand(args) {
2360
2368
  }
2361
2369
  let fileConfig = {};
2362
2370
  const fullConfigPath = resolve2(process.cwd(), configPath);
2363
- if (existsSync2(fullConfigPath)) {
2371
+ if (existsSync3(fullConfigPath)) {
2364
2372
  console.log(`\uD83D\uDCCB Loading ${configPath}`);
2365
2373
  try {
2366
2374
  const configUrl = pathToFileURL2(fullConfigPath).href;
@@ -2436,6 +2444,7 @@ var init_cli_pull = () => {};
2436
2444
  var import_config = __toESM(require_config(), 1);
2437
2445
  import { join, relative } from "node:path";
2438
2446
  import { pathToFileURL } from "node:url";
2447
+ import { existsSync } from "node:fs";
2439
2448
 
2440
2449
  // src/introspect.ts
2441
2450
  import { Client } from "pg";
@@ -3953,7 +3962,7 @@ const STRATEGY = ${STRATEGY} as "none" | "api-key" | "jwt-hs256";
3953
3962
  const API_KEY_HEADER = ${API_KEY_HEADER} as string;
3954
3963
  const RAW_API_KEYS = ${RAW_API_KEYS} as readonly string[];
3955
3964
 
3956
- const JWT_SERVICES = ${JWT_SERVICES} as ReadonlyArray<{ issuer: string; secret: string }>;
3965
+ const JWT_SERVICES = ${JWT_SERVICES} as ReadonlyArray<{ issuer: string; secret?: string }>;
3957
3966
  const JWT_AUDIENCE = ${JWT_AUDIENCE} as string | undefined;
3958
3967
  // -------------------------------------
3959
3968
 
@@ -5509,6 +5518,12 @@ function normalizeAuthConfig(input) {
5509
5518
 
5510
5519
  // src/index.ts
5511
5520
  async function generate(configPath) {
5521
+ if (!existsSync(configPath)) {
5522
+ throw new Error(`Config file not found: ${configPath}
5523
+
5524
+ ` + `Run 'postgresdk init' to create a config file, or specify a custom path with:
5525
+ ` + ` postgresdk generate --config <path>`);
5526
+ }
5512
5527
  const configUrl = pathToFileURL(configPath).href;
5513
5528
  const module = await import(configUrl);
5514
5529
  const rawCfg = module.default || module;
package/dist/index.js CHANGED
@@ -634,7 +634,15 @@ function generateIncludeMethods(table, graph, opts, allTables) {
634
634
  }
635
635
  }
636
636
  explore(baseTableName, [], [], [], new Set([baseTableName]), 1);
637
- return methods;
637
+ const seen = new Set;
638
+ const dedupedMethods = [];
639
+ for (const method of methods) {
640
+ if (!seen.has(method.name)) {
641
+ seen.add(method.name);
642
+ dedupedMethods.push(method);
643
+ }
644
+ }
645
+ return dedupedMethods;
638
646
  }
639
647
  var init_emit_include_methods = __esm(() => {
640
648
  init_utils();
@@ -1610,6 +1618,7 @@ var init_emit_sdk_contract = __esm(() => {
1610
1618
  var import_config = __toESM(require_config(), 1);
1611
1619
  import { join, relative } from "node:path";
1612
1620
  import { pathToFileURL } from "node:url";
1621
+ import { existsSync } from "node:fs";
1613
1622
 
1614
1623
  // src/introspect.ts
1615
1624
  import { Client } from "pg";
@@ -3127,7 +3136,7 @@ const STRATEGY = ${STRATEGY} as "none" | "api-key" | "jwt-hs256";
3127
3136
  const API_KEY_HEADER = ${API_KEY_HEADER} as string;
3128
3137
  const RAW_API_KEYS = ${RAW_API_KEYS} as readonly string[];
3129
3138
 
3130
- const JWT_SERVICES = ${JWT_SERVICES} as ReadonlyArray<{ issuer: string; secret: string }>;
3139
+ const JWT_SERVICES = ${JWT_SERVICES} as ReadonlyArray<{ issuer: string; secret?: string }>;
3131
3140
  const JWT_AUDIENCE = ${JWT_AUDIENCE} as string | undefined;
3132
3141
  // -------------------------------------
3133
3142
 
@@ -4683,6 +4692,12 @@ function normalizeAuthConfig(input) {
4683
4692
 
4684
4693
  // src/index.ts
4685
4694
  async function generate(configPath) {
4695
+ if (!existsSync(configPath)) {
4696
+ throw new Error(`Config file not found: ${configPath}
4697
+
4698
+ ` + `Run 'postgresdk init' to create a config file, or specify a custom path with:
4699
+ ` + ` postgresdk generate --config <path>`);
4700
+ }
4686
4701
  const configUrl = pathToFileURL(configPath).href;
4687
4702
  const module = await import(configUrl);
4688
4703
  const rawCfg = module.default || module;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {