postgresdk 0.19.4 → 0.19.5
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 +3 -1
- package/dist/cli-install-skill.d.ts +5 -0
- package/dist/cli.js +52 -6
- package/package.json +2 -1
- package/skills/postgresdk/SKILL.md +394 -0
package/README.md
CHANGED
|
@@ -1063,12 +1063,13 @@ Commands:
|
|
|
1063
1063
|
init Create a postgresdk.config.ts file
|
|
1064
1064
|
generate Generate SDK from database
|
|
1065
1065
|
pull Pull SDK from API endpoint
|
|
1066
|
+
install-skill Install Claude Code skill for PostgreSDK
|
|
1066
1067
|
version Show version
|
|
1067
1068
|
help Show help
|
|
1068
1069
|
|
|
1069
1070
|
Options:
|
|
1070
1071
|
-c, --config <path> Path to config file (default: postgresdk.config.ts)
|
|
1071
|
-
--force, -y Delete stale files without prompting (generate & pull)
|
|
1072
|
+
--force, -y Delete stale files without prompting (generate & pull); overwrite existing skill (install-skill)
|
|
1072
1073
|
|
|
1073
1074
|
Init subcommands/flags:
|
|
1074
1075
|
init pull Generate pull-only config (alias for --sdk)
|
|
@@ -1084,6 +1085,7 @@ Examples:
|
|
|
1084
1085
|
npx postgresdk@latest generate --force # Skip stale file prompts
|
|
1085
1086
|
npx postgresdk@latest pull --from=https://api.com --output=./src/sdk
|
|
1086
1087
|
npx postgresdk@latest pull --from=https://api.com --output=./src/sdk --force
|
|
1088
|
+
npx postgresdk@latest install-skill # Install Claude Code skill
|
|
1087
1089
|
```
|
|
1088
1090
|
|
|
1089
1091
|
### Generated Tests
|
package/dist/cli.js
CHANGED
|
@@ -2483,6 +2483,44 @@ var init_cli_pull = __esm(() => {
|
|
|
2483
2483
|
init_cli_utils();
|
|
2484
2484
|
});
|
|
2485
2485
|
|
|
2486
|
+
// src/cli-install-skill.ts
|
|
2487
|
+
var exports_cli_install_skill = {};
|
|
2488
|
+
__export(exports_cli_install_skill, {
|
|
2489
|
+
installSkillCommand: () => installSkillCommand
|
|
2490
|
+
});
|
|
2491
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync5 } from "node:fs";
|
|
2492
|
+
import { join as join4, dirname as dirname4 } from "node:path";
|
|
2493
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2494
|
+
async function installSkillCommand(args) {
|
|
2495
|
+
const force = parseForceFlag(args);
|
|
2496
|
+
const destDir = join4(process.cwd(), ".claude", "skills", "postgresdk");
|
|
2497
|
+
const destPath = join4(destDir, "SKILL.md");
|
|
2498
|
+
if (existsSync5(destPath) && !force) {
|
|
2499
|
+
console.log("⚠️ Skill already exists at .claude/skills/postgresdk/SKILL.md");
|
|
2500
|
+
console.log(" Use --force to overwrite.");
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
const srcPath = join4(__dirname3, "..", "skills", "postgresdk", "SKILL.md");
|
|
2504
|
+
if (!existsSync5(srcPath)) {
|
|
2505
|
+
console.error("❌ Could not find bundled skill file. This is a bug — please report it.");
|
|
2506
|
+
process.exit(1);
|
|
2507
|
+
}
|
|
2508
|
+
const content = readFileSync3(srcPath, "utf-8");
|
|
2509
|
+
mkdirSync(destDir, { recursive: true });
|
|
2510
|
+
writeFileSync2(destPath, content, "utf-8");
|
|
2511
|
+
console.log("✅ Installed PostgreSDK skill to .claude/skills/postgresdk/SKILL.md");
|
|
2512
|
+
console.log("");
|
|
2513
|
+
console.log(" Claude Code will now use this skill when you ask about your");
|
|
2514
|
+
console.log(" generated API or SDK. Try asking it to help with queries,");
|
|
2515
|
+
console.log(" filtering, includes, auth setup, or transactions.");
|
|
2516
|
+
}
|
|
2517
|
+
var __filename3, __dirname3;
|
|
2518
|
+
var init_cli_install_skill = __esm(() => {
|
|
2519
|
+
init_cli_utils();
|
|
2520
|
+
__filename3 = fileURLToPath2(import.meta.url);
|
|
2521
|
+
__dirname3 = dirname4(__filename3);
|
|
2522
|
+
});
|
|
2523
|
+
|
|
2486
2524
|
// src/index.ts
|
|
2487
2525
|
var import_config = __toESM(require_config(), 1);
|
|
2488
2526
|
import { join as join2, relative, dirname as dirname2 } from "node:path";
|
|
@@ -8035,13 +8073,13 @@ async function generate(configPath, options) {
|
|
|
8035
8073
|
// src/cli.ts
|
|
8036
8074
|
var import_config2 = __toESM(require_config(), 1);
|
|
8037
8075
|
import { resolve as resolve3 } from "node:path";
|
|
8038
|
-
import { readFileSync as
|
|
8039
|
-
import { fileURLToPath as
|
|
8040
|
-
import { dirname as
|
|
8076
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
8077
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
8078
|
+
import { dirname as dirname5, join as join5 } from "node:path";
|
|
8041
8079
|
init_cli_utils();
|
|
8042
|
-
var
|
|
8043
|
-
var
|
|
8044
|
-
var packageJson = JSON.parse(
|
|
8080
|
+
var __filename4 = fileURLToPath3(import.meta.url);
|
|
8081
|
+
var __dirname4 = dirname5(__filename4);
|
|
8082
|
+
var packageJson = JSON.parse(readFileSync4(join5(__dirname4, "../package.json"), "utf-8"));
|
|
8045
8083
|
var VERSION = packageJson.version;
|
|
8046
8084
|
var args = process.argv.slice(2);
|
|
8047
8085
|
var command = args[0];
|
|
@@ -8060,6 +8098,7 @@ Commands:
|
|
|
8060
8098
|
init Create a postgresdk.config.ts file
|
|
8061
8099
|
generate, gen Generate SDK from database
|
|
8062
8100
|
pull Pull SDK from API endpoint
|
|
8101
|
+
install-skill Install Claude Code skill for PostgreSDK
|
|
8063
8102
|
version Show version
|
|
8064
8103
|
help Show help
|
|
8065
8104
|
|
|
@@ -8077,6 +8116,9 @@ Pull Options:
|
|
|
8077
8116
|
--force, -y Delete stale files without prompting
|
|
8078
8117
|
-c, --config <path> Path to config file with pull settings
|
|
8079
8118
|
|
|
8119
|
+
Install-skill Options:
|
|
8120
|
+
--force, -y Overwrite existing skill
|
|
8121
|
+
|
|
8080
8122
|
Examples:
|
|
8081
8123
|
postgresdk init # Create config file
|
|
8082
8124
|
postgresdk generate # Generate using postgresdk.config.ts
|
|
@@ -8084,6 +8126,7 @@ Examples:
|
|
|
8084
8126
|
postgresdk generate -c custom.config.ts
|
|
8085
8127
|
postgresdk pull --from=https://api.com --output=./src/sdk
|
|
8086
8128
|
postgresdk pull # Pull using config file
|
|
8129
|
+
postgresdk install-skill # Install Claude Code skill
|
|
8087
8130
|
`);
|
|
8088
8131
|
process.exit(0);
|
|
8089
8132
|
}
|
|
@@ -8106,6 +8149,9 @@ if (command === "init") {
|
|
|
8106
8149
|
} else if (command === "pull") {
|
|
8107
8150
|
const { pullCommand: pullCommand2 } = await Promise.resolve().then(() => (init_cli_pull(), exports_cli_pull));
|
|
8108
8151
|
await pullCommand2(args.slice(1));
|
|
8152
|
+
} else if (command === "install-skill") {
|
|
8153
|
+
const { installSkillCommand: installSkillCommand2 } = await Promise.resolve().then(() => (init_cli_install_skill(), exports_cli_install_skill));
|
|
8154
|
+
await installSkillCommand2(args.slice(1));
|
|
8109
8155
|
} else {
|
|
8110
8156
|
console.error(`❌ Unknown command: ${command}`);
|
|
8111
8157
|
console.error(`Run 'postgresdk help' for usage information`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postgresdk",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.5",
|
|
4
4
|
"description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
|
+
"skills",
|
|
17
18
|
"README.md",
|
|
18
19
|
"LICENSE"
|
|
19
20
|
],
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: postgresdk
|
|
3
|
+
description: "How to use a PostgreSDK-generated API and client SDK. Use this skill whenever the user is working with code generated by PostgreSDK — including the typed client SDK (e.g., `sdk.users.list()`, `sdk.books.create()`), the generated Hono API server (router, routes, auth middleware), or the CONTRACT.md/contract.ts reference files. Trigger on: SDK method calls like `.list()`, `.getByPk()`, `.create()`, `.update()`, `.upsert()`, `.$transaction()`, `.listWith*()`, WHERE clause operators like `$ilike`, `$in`, `$gte`, `$jsonbContains`, mentions of `postgresdk`, `CONTRACT.md`, `createRouter`, `BaseClient`, `PaginatedResponse`, `InsertX`/`SelectX`/`UpdateX` types, `postgresdk.config.ts`, or any questions about filtering, includes, pagination, vector search, or trigram search in the context of a generated API."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PostgreSDK Assistant
|
|
7
|
+
|
|
8
|
+
You are helping a developer who is using code generated by [PostgreSDK](https://github.com/nickreese/postgresdk) — a tool that generates a fully typed Hono API server and TypeScript client SDK from a PostgreSQL database schema.
|
|
9
|
+
|
|
10
|
+
## First: Discover the Generated Code
|
|
11
|
+
|
|
12
|
+
The output directory is fully configurable via `postgresdk.config.ts` — don't assume paths like `./api/server` or `./api/client`. Discover where the generated code actually lives:
|
|
13
|
+
|
|
14
|
+
1. **Search for `postgresdk.config.ts`** — if present, read the `outDir` value. It can be a string (e.g. `"./generated"`) or an object (`{ client: "./sdk", server: "./backend" }`). This tells you where the generated directories are.
|
|
15
|
+
2. **Search for generated marker files** — if no config, glob for `**/router.ts`, `**/base-client.ts`, or `**/CONTRACT.md` and look for the `AUTO-GENERATED FILE` header to find the generated output directories.
|
|
16
|
+
|
|
17
|
+
Once you've found the generated directories, identify which side the user is working on:
|
|
18
|
+
|
|
19
|
+
**API-side** (runs the server):
|
|
20
|
+
- Has a directory containing `router.ts`, `routes/*.ts`, `contract.ts`, `sdk-bundle.ts`
|
|
21
|
+
- Has `postgresdk.config.ts` with `connectionString`
|
|
22
|
+
- Asks about `createRouter`, `onRequest`, auth config, deployment, database drivers
|
|
23
|
+
|
|
24
|
+
**SDK-side** (consumes the API):
|
|
25
|
+
- Has a directory containing `base-client.ts`, table client files (e.g. `users.ts`), `CONTRACT.md`
|
|
26
|
+
- May have `postgresdk.config.ts` with a `pull` section instead of `connectionString`
|
|
27
|
+
- Asks about `sdk.tableName.method()`, filtering, includes, transactions
|
|
28
|
+
|
|
29
|
+
**Both** — common when a single `outDir` generates server and client subdirectories.
|
|
30
|
+
|
|
31
|
+
## Second: Read the Contract
|
|
32
|
+
|
|
33
|
+
The generated `CONTRACT.md` is the single source of truth for the user's specific schema. It contains every table, field, type, method, endpoint, and relationship. Read it before answering schema-specific questions.
|
|
34
|
+
|
|
35
|
+
Search with `**/CONTRACT.md` and look for the one with the `AUTO-GENERATED` header. It exists in both the server and client output directories — either copy works, they're identical.
|
|
36
|
+
|
|
37
|
+
If you can't find it, ask the user where their generated code lives.
|
|
38
|
+
|
|
39
|
+
## SDK Reference
|
|
40
|
+
|
|
41
|
+
### Initialization
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// Import path depends on outDir config — find the generated client directory
|
|
45
|
+
import { SDK } from '<client-dir>';
|
|
46
|
+
|
|
47
|
+
const sdk = new SDK({
|
|
48
|
+
baseUrl: 'http://localhost:3000',
|
|
49
|
+
auth: {
|
|
50
|
+
apiKey: 'your-key', // API key auth
|
|
51
|
+
// OR
|
|
52
|
+
jwt: 'your-token', // Static JWT
|
|
53
|
+
// OR
|
|
54
|
+
jwt: async () => getToken(), // Async JWT provider
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### CRUD Operations
|
|
60
|
+
|
|
61
|
+
Every table gets these methods (assuming single primary key):
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// Create
|
|
65
|
+
const item = await sdk.users.create({ name: 'Alice', email: 'alice@example.com' });
|
|
66
|
+
|
|
67
|
+
// Read
|
|
68
|
+
const user = await sdk.users.getByPk('id-123'); // single record or null
|
|
69
|
+
const result = await sdk.users.list({ limit: 20 }); // paginated
|
|
70
|
+
|
|
71
|
+
// Update
|
|
72
|
+
const updated = await sdk.users.update('id-123', { name: 'Bob' });
|
|
73
|
+
|
|
74
|
+
// Upsert (insert or update on conflict)
|
|
75
|
+
const upserted = await sdk.users.upsert({
|
|
76
|
+
where: { email: 'alice@example.com' }, // unique constraint columns
|
|
77
|
+
create: { email: 'alice@example.com', name: 'Alice' },
|
|
78
|
+
update: { name: 'Alice Updated' },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Delete
|
|
82
|
+
await sdk.users.hardDelete('id-123'); // permanent
|
|
83
|
+
await sdk.users.softDelete('id-123'); // sets soft-delete column (if configured)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Paginated Response Shape
|
|
87
|
+
|
|
88
|
+
All `list()` calls return:
|
|
89
|
+
```typescript
|
|
90
|
+
{
|
|
91
|
+
data: T[]; // array of records
|
|
92
|
+
total: number; // total matching records (respects WHERE)
|
|
93
|
+
limit?: number; // page size (absent when no limit)
|
|
94
|
+
offset: number; // current offset
|
|
95
|
+
hasMore: boolean; // more pages available
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### WHERE Filtering
|
|
100
|
+
|
|
101
|
+
Root-level keys are AND'd. Use `$or`/`$and` for logic (2 nesting levels max).
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
await sdk.users.list({
|
|
105
|
+
where: {
|
|
106
|
+
age: { $gte: 18, $lt: 65 },
|
|
107
|
+
email: { $ilike: '%@company.com' },
|
|
108
|
+
status: { $in: ['active', 'pending'] },
|
|
109
|
+
deleted_at: { $is: null },
|
|
110
|
+
$or: [{ role: 'admin' }, { role: 'mod' }]
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Available operators:**
|
|
116
|
+
|
|
117
|
+
| Operator | SQL | Applies to |
|
|
118
|
+
|----------|-----|------------|
|
|
119
|
+
| `$eq`, `$ne` | `=`, `!=` | All types |
|
|
120
|
+
| `$gt`, `$gte`, `$lt`, `$lte` | `>`, `>=`, `<`, `<=` | Number, Date |
|
|
121
|
+
| `$in`, `$nin` | `IN`, `NOT IN` | All types |
|
|
122
|
+
| `$like`, `$ilike` | `LIKE`, `ILIKE` | Strings |
|
|
123
|
+
| `$is`, `$isNot` | `IS NULL`, `IS NOT NULL` | Nullable fields |
|
|
124
|
+
| `$similarity`, `$wordSimilarity`, `$strictWordSimilarity` | pg_trgm operators | Strings (requires pg_trgm) |
|
|
125
|
+
| `$jsonbContains` | `@>` | JSONB |
|
|
126
|
+
| `$jsonbContainedBy` | `<@` | JSONB |
|
|
127
|
+
| `$jsonbHasKey` | `?` | JSONB |
|
|
128
|
+
| `$jsonbHasAnyKeys` | `?\|` | JSONB |
|
|
129
|
+
| `$jsonbHasAllKeys` | `?&` | JSONB |
|
|
130
|
+
| `$jsonbPath` | Path-based query | JSONB |
|
|
131
|
+
| `$or`, `$and` | `OR`, `AND` | Logical combinators |
|
|
132
|
+
|
|
133
|
+
### Sorting
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// Single column
|
|
137
|
+
await sdk.users.list({ orderBy: 'created_at', order: 'desc' });
|
|
138
|
+
|
|
139
|
+
// Multi-column with per-column direction
|
|
140
|
+
await sdk.users.list({
|
|
141
|
+
orderBy: ['status', 'created_at'],
|
|
142
|
+
order: ['asc', 'desc']
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### DISTINCT ON
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const latestPerUser = await sdk.events.list({
|
|
150
|
+
distinctOn: 'user_id',
|
|
151
|
+
orderBy: 'created_at',
|
|
152
|
+
order: 'desc'
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Field Selection
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// Only return specific fields
|
|
160
|
+
await sdk.users.list({ select: ['id', 'email', 'name'] });
|
|
161
|
+
|
|
162
|
+
// Return all fields except these
|
|
163
|
+
await sdk.users.list({ exclude: ['password_hash', 'secret_token'] });
|
|
164
|
+
|
|
165
|
+
// Works on single-record operations too
|
|
166
|
+
await sdk.users.create(data, { select: ['id', 'email'] });
|
|
167
|
+
await sdk.users.update(id, patch, { exclude: ['updated_at'] });
|
|
168
|
+
await sdk.users.getByPk(id, { select: ['id', 'name'] });
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Relationships & Includes
|
|
172
|
+
|
|
173
|
+
**Generic include:**
|
|
174
|
+
```typescript
|
|
175
|
+
const result = await sdk.authors.list({
|
|
176
|
+
include: { books: true }
|
|
177
|
+
});
|
|
178
|
+
// result.data[0].books is SelectBooks[]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Typed convenience methods** (generated per-table based on relationships):
|
|
182
|
+
```typescript
|
|
183
|
+
// listWith* and getByPkWith* — check CONTRACT.md for what's available
|
|
184
|
+
const result = await sdk.authors.listWithBooks({ limit: 10 });
|
|
185
|
+
const author = await sdk.authors.getByPkWithBooksAndTags('id');
|
|
186
|
+
|
|
187
|
+
// Control included relations
|
|
188
|
+
await sdk.authors.listWithBooks({
|
|
189
|
+
booksInclude: { orderBy: 'published_at', order: 'desc', limit: 5 }
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Nested includes:**
|
|
194
|
+
```typescript
|
|
195
|
+
const result = await sdk.authors.list({
|
|
196
|
+
include: { books: { tags: true } }
|
|
197
|
+
});
|
|
198
|
+
// result.data[0].books[0].tags is SelectTags[]
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Select/exclude on includes:**
|
|
202
|
+
```typescript
|
|
203
|
+
await sdk.authors.list({
|
|
204
|
+
select: ['id', 'name'],
|
|
205
|
+
include: {
|
|
206
|
+
books: { select: ['id', 'title'], orderBy: 'published_at', limit: 5 }
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Atomic Transactions
|
|
212
|
+
|
|
213
|
+
Use `$`-prefixed lazy builders inside `$transaction`:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
const [order, updatedUser] = await sdk.$transaction([
|
|
217
|
+
sdk.orders.$create({ user_id: user.id, total: 99 }),
|
|
218
|
+
sdk.users.$update(user.id, { last_order_at: new Date().toISOString() }),
|
|
219
|
+
]);
|
|
220
|
+
// TypeScript infers: [SelectOrders, SelectUsers | null]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Available builders: `$create`, `$update`, `$softDelete`, `$hardDelete`, `$upsert`.
|
|
224
|
+
|
|
225
|
+
All operations are Zod-validated before `BEGIN`. On failure, the entire transaction rolls back and throws with a `.failedAt` index.
|
|
226
|
+
|
|
227
|
+
### Soft Deletes
|
|
228
|
+
|
|
229
|
+
When `softDeleteColumn` is configured in `postgresdk.config.ts`:
|
|
230
|
+
- `softDelete(id)` — sets the column (e.g. `deleted_at = NOW()`)
|
|
231
|
+
- `hardDelete(id)` — permanent `DELETE` (unless `exposeHardDelete: false`)
|
|
232
|
+
- Soft-deleted rows are hidden from `list`/`getByPk` by default
|
|
233
|
+
- Pass `includeSoftDeleted: true` to see them
|
|
234
|
+
|
|
235
|
+
### Vector Search (pgvector)
|
|
236
|
+
|
|
237
|
+
For tables with `vector` columns. Results auto-include `_distance`.
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const results = await sdk.video_sections.list({
|
|
241
|
+
vector: {
|
|
242
|
+
field: 'vision_embedding',
|
|
243
|
+
query: embeddingArray, // number[]
|
|
244
|
+
metric: 'cosine', // 'cosine' | 'l2' | 'inner'
|
|
245
|
+
maxDistance: 0.5 // optional threshold
|
|
246
|
+
},
|
|
247
|
+
where: { status: 'published' }, // combine with regular filters
|
|
248
|
+
limit: 10
|
|
249
|
+
});
|
|
250
|
+
// results.data[0]._distance
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Trigram Search (pg_trgm)
|
|
254
|
+
|
|
255
|
+
Fuzzy text search. Results auto-include `_similarity`.
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
const results = await sdk.books.list({
|
|
259
|
+
trigram: {
|
|
260
|
+
field: 'title',
|
|
261
|
+
query: 'postgrs', // typo-tolerant
|
|
262
|
+
metric: 'similarity', // 'similarity' | 'wordSimilarity' | 'strictWordSimilarity'
|
|
263
|
+
threshold: 0.3 // min score 0–1
|
|
264
|
+
},
|
|
265
|
+
limit: 10
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Multi-field:** `fields: ['name', 'url']` with `strategy: 'greatest' | 'concat'`, or weighted: `fields: [{ field: 'name', weight: 2 }, { field: 'url', weight: 1 }]`.
|
|
270
|
+
|
|
271
|
+
Note: `trigram` and `vector` are mutually exclusive on a single `list()` call.
|
|
272
|
+
|
|
273
|
+
### Type Imports
|
|
274
|
+
|
|
275
|
+
Import paths below use `<client>` as a placeholder — substitute the actual path to the generated client directory (determined by the `outDir` config).
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
import { SDK } from '<client>';
|
|
279
|
+
import type { SelectUsers, InsertUsers, UpdateUsers } from '<client>/types/users';
|
|
280
|
+
import type { PaginatedResponse } from '<client>/types/shared';
|
|
281
|
+
|
|
282
|
+
// Zod schemas (for form validation, etc.)
|
|
283
|
+
import { InsertUsersSchema, UpdateUsersSchema } from '<client>/zod/users';
|
|
284
|
+
|
|
285
|
+
// Params schemas
|
|
286
|
+
import { UsersListParamsSchema, UsersPkSchema } from '<client>/params/users';
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## API Server Reference
|
|
290
|
+
|
|
291
|
+
### Basic Setup
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
import { Hono } from 'hono';
|
|
295
|
+
import { serve } from '@hono/node-server';
|
|
296
|
+
import { Client } from 'pg';
|
|
297
|
+
import { createRouter } from '<server>/router'; // path depends on outDir config
|
|
298
|
+
|
|
299
|
+
const app = new Hono();
|
|
300
|
+
const pg = new Client({ connectionString: process.env.DATABASE_URL });
|
|
301
|
+
await pg.connect();
|
|
302
|
+
|
|
303
|
+
const apiRouter = createRouter({ pg });
|
|
304
|
+
app.route('/', apiRouter);
|
|
305
|
+
|
|
306
|
+
serve({ fetch: app.fetch, port: 3000 });
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Auth Configuration (in postgresdk.config.ts)
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
export default {
|
|
313
|
+
connectionString: process.env.DATABASE_URL,
|
|
314
|
+
auth: {
|
|
315
|
+
// Option A: API key
|
|
316
|
+
apiKey: process.env.API_KEY,
|
|
317
|
+
|
|
318
|
+
// Option B: JWT (supports multiple services)
|
|
319
|
+
jwt: {
|
|
320
|
+
services: [
|
|
321
|
+
{ issuer: 'web-app', secret: 'env:WEB_SECRET' },
|
|
322
|
+
{ issuer: 'mobile', secret: 'env:MOBILE_SECRET' },
|
|
323
|
+
],
|
|
324
|
+
audience: 'my-api' // optional
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### onRequest Hook
|
|
331
|
+
|
|
332
|
+
Runs before every endpoint. Use for RLS, audit logging, authorization:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
const apiRouter = createRouter({
|
|
336
|
+
pg,
|
|
337
|
+
onRequest: async (c, pg) => {
|
|
338
|
+
const auth = c.get('auth');
|
|
339
|
+
|
|
340
|
+
// Set PostgreSQL session variable for RLS
|
|
341
|
+
if (auth?.kind === 'jwt' && auth.claims?.sub) {
|
|
342
|
+
await pg.query(`SET LOCAL app.user_id = '${auth.claims.sub}'`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Scope-based authorization
|
|
346
|
+
const scopes = auth?.claims?.scopes || [];
|
|
347
|
+
const table = c.req.path.split('/')[2];
|
|
348
|
+
if (!hasPermission(scopes, table, c.req.method)) {
|
|
349
|
+
throw new Error('Forbidden');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Deployment Patterns
|
|
356
|
+
|
|
357
|
+
- **Serverless** (Vercel, Cloudflare): Use `Pool` with `max: 1` — each instance is ephemeral
|
|
358
|
+
- **Traditional** (Railway, VPS): Use `Pool` with `max: 10` — reuse connections across requests
|
|
359
|
+
- **Edge**: Use `@neondatabase/serverless` Pool, set `useJsExtensions: true` in config
|
|
360
|
+
|
|
361
|
+
### SDK Distribution
|
|
362
|
+
|
|
363
|
+
The generated server auto-serves the client SDK:
|
|
364
|
+
- `GET /_psdk/sdk/manifest` — file listing
|
|
365
|
+
- `GET /_psdk/sdk/download` — complete bundle
|
|
366
|
+
- `GET /_psdk/contract.md` — markdown contract
|
|
367
|
+
- `GET /_psdk/contract.json` — JSON contract
|
|
368
|
+
|
|
369
|
+
Clients pull with: `npx postgresdk@latest pull --from=https://api.example.com --output=./src/sdk`
|
|
370
|
+
|
|
371
|
+
Protect with `pullToken` in config if needed.
|
|
372
|
+
|
|
373
|
+
## Key Configuration Options
|
|
374
|
+
|
|
375
|
+
| Option | Default | Description |
|
|
376
|
+
|--------|---------|-------------|
|
|
377
|
+
| `schema` | `"public"` | Database schema to introspect |
|
|
378
|
+
| `outDir` | `"./api"` | Output dir (or `{ client, server }`) |
|
|
379
|
+
| `numericMode` | `"auto"` | `"auto"` / `"number"` / `"string"` |
|
|
380
|
+
| `maxLimit` | `1000` | Max allowed `limit` (0 = no cap) |
|
|
381
|
+
| `includeMethodsDepth` | `2` | Max depth for `listWith*` methods |
|
|
382
|
+
| `dateType` | `"date"` | `"date"` / `"string"` |
|
|
383
|
+
| `useJsExtensions` | `false` | Add `.js` to imports (Edge/Deno) |
|
|
384
|
+
| `delete.softDeleteColumn` | — | Column name for soft deletes |
|
|
385
|
+
| `delete.exposeHardDelete` | `true` | Also expose `hardDelete()` |
|
|
386
|
+
|
|
387
|
+
## Common Gotchas
|
|
388
|
+
|
|
389
|
+
- **list() uses POST** — the `list` method sends a POST to `/v1/{table}/list` (not GET) because complex WHERE/include payloads don't fit in query strings. The simple GET endpoint exists too but only supports basic query params.
|
|
390
|
+
- **Composite PKs** — tables with composite primary keys don't get `getByPk`, `update`, `delete`, or `upsert` methods. Use `list` with WHERE filters instead.
|
|
391
|
+
- **Junction tables** — M:N junction tables (like `book_tags`) are detected automatically. The SDK generates include methods that skip the junction table: `sdk.books.listWithTags()` gives you tags directly, not book_tags.
|
|
392
|
+
- **Import paths** — depend entirely on the `outDir` config in `postgresdk.config.ts`. Always discover the actual generated directory before suggesting imports.
|
|
393
|
+
- **Types naming** — `Select{PascalTable}`, `Insert{PascalTable}`, `Update{PascalTable}` (e.g., `SelectUsers`, `InsertUsers`).
|
|
394
|
+
- **Regeneration** — all generated files have `AUTO-GENERATED FILE - DO NOT EDIT` headers. Changes are overwritten on `generate` or `pull`. Extend behavior via `onRequest` hooks, not by editing generated code.
|