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 +390 -214
- package/dist/cli.js +22 -7
- package/dist/index.js +17 -2
- package/package.json +1 -1
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
+
### Database Drivers
|
|
173
183
|
|
|
174
|
-
|
|
184
|
+
The generated code works with any PostgreSQL client that implements a simple `query` interface:
|
|
175
185
|
|
|
176
|
-
|
|
186
|
+
#### Node.js `pg` Driver
|
|
177
187
|
|
|
178
188
|
```typescript
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
188
|
-
|
|
192
|
+
const pg = new Client({ connectionString: process.env.DATABASE_URL });
|
|
193
|
+
await pg.connect();
|
|
189
194
|
|
|
190
|
-
|
|
191
|
-
const deleted = await sdk.users.delete(123);
|
|
195
|
+
const apiRouter = createRouter({ pg });
|
|
192
196
|
```
|
|
193
197
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
Automatically handles relationships with the `include` parameter:
|
|
198
|
+
#### Neon Serverless Driver (Edge-Compatible)
|
|
197
199
|
|
|
198
200
|
```typescript
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
212
|
-
const
|
|
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
|
-
###
|
|
208
|
+
### Server Integration
|
|
225
209
|
|
|
226
|
-
|
|
210
|
+
postgresdk generates Hono-compatible routes:
|
|
227
211
|
|
|
228
212
|
```typescript
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
245
|
-
const
|
|
246
|
-
|
|
220
|
+
// Database connection
|
|
221
|
+
const pg = new Client({ connectionString: process.env.DATABASE_URL });
|
|
222
|
+
await pg.connect();
|
|
247
223
|
|
|
248
|
-
//
|
|
249
|
-
const
|
|
250
|
-
|
|
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
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
+
### Authentication
|
|
317
268
|
|
|
318
269
|
postgresdk supports API key and JWT authentication:
|
|
319
270
|
|
|
320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
328
|
+
#### Service-to-Service Authorization
|
|
378
329
|
|
|
379
|
-
|
|
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
|
-
|
|
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
|
-
|
|
385
|
-
import {
|
|
338
|
+
// 1. Your auth service issues JWTs with scopes in claims
|
|
339
|
+
import { sign } from "jsonwebtoken";
|
|
386
340
|
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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
|
-
|
|
396
|
-
import { Pool } from "@neondatabase/serverless";
|
|
363
|
+
// 3. Enforce scopes in onRequest hook
|
|
397
364
|
import { createRouter } from "./api/server/router";
|
|
398
365
|
|
|
399
|
-
|
|
400
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
417
|
-
const auth = c.get('auth');
|
|
384
|
+
const auth = c.get("auth");
|
|
418
385
|
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
//
|
|
425
|
-
if (auth?.
|
|
426
|
-
await pg.query(`SET LOCAL app.
|
|
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
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
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
|
-
//
|
|
459
|
-
|
|
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
|
-
|
|
446
|
+
### Deployment
|
|
463
447
|
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
-
|
|
539
|
+
Then run:
|
|
540
|
+
```bash
|
|
541
|
+
npx postgresdk pull
|
|
542
|
+
# or
|
|
543
|
+
bunx postgresdk pull
|
|
544
|
+
```
|
|
521
545
|
|
|
522
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
600
|
+
#### Filtering & Pagination
|
|
536
601
|
|
|
537
|
-
|
|
538
|
-
chmod +x api/tests/run-tests.sh
|
|
539
|
-
./api/tests/run-tests.sh
|
|
602
|
+
All `list()` methods return pagination metadata:
|
|
540
603
|
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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;
|