postgresdk 0.19.4 → 0.19.6

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
@@ -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
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Install the PostgreSDK Claude Code skill into the current project.
3
+ * Copies skills/postgresdk/SKILL.md → .claude/skills/postgresdk/SKILL.md
4
+ */
5
+ export declare function installSkillCommand(args: string[]): Promise<void>;
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";
@@ -4457,7 +4495,13 @@ export abstract class BaseClient {
4457
4495
  protected baseUrl: string,
4458
4496
  protected fetchFn: typeof fetch = fetch,
4459
4497
  protected auth?: AuthConfig
4460
- ) {}
4498
+ ) {
4499
+ // Rebind so \`this.fetchFn(...)\` calls don't pass the client as the receiver.
4500
+ // Browser \`fetch\` requires \`window\`/\`globalThis\` as \`this\` and throws
4501
+ // "Illegal invocation" otherwise. Covers both the default and any caller-supplied fetch.
4502
+ const f = this.fetchFn;
4503
+ this.fetchFn = ((...args: Parameters<typeof fetch>) => f(...args)) as typeof fetch;
4504
+ }
4461
4505
 
4462
4506
  protected async authHeaders(): Promise<HeaderMap> {
4463
4507
  if (!this.auth) return {};
@@ -8035,13 +8079,13 @@ async function generate(configPath, options) {
8035
8079
  // src/cli.ts
8036
8080
  var import_config2 = __toESM(require_config(), 1);
8037
8081
  import { resolve as resolve3 } from "node:path";
8038
- import { readFileSync as readFileSync3 } from "node:fs";
8039
- import { fileURLToPath as fileURLToPath2 } from "node:url";
8040
- import { dirname as dirname4, join as join4 } from "node:path";
8082
+ import { readFileSync as readFileSync4 } from "node:fs";
8083
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
8084
+ import { dirname as dirname5, join as join5 } from "node:path";
8041
8085
  init_cli_utils();
8042
- var __filename3 = fileURLToPath2(import.meta.url);
8043
- var __dirname3 = dirname4(__filename3);
8044
- var packageJson = JSON.parse(readFileSync3(join4(__dirname3, "../package.json"), "utf-8"));
8086
+ var __filename4 = fileURLToPath3(import.meta.url);
8087
+ var __dirname4 = dirname5(__filename4);
8088
+ var packageJson = JSON.parse(readFileSync4(join5(__dirname4, "../package.json"), "utf-8"));
8045
8089
  var VERSION = packageJson.version;
8046
8090
  var args = process.argv.slice(2);
8047
8091
  var command = args[0];
@@ -8060,6 +8104,7 @@ Commands:
8060
8104
  init Create a postgresdk.config.ts file
8061
8105
  generate, gen Generate SDK from database
8062
8106
  pull Pull SDK from API endpoint
8107
+ install-skill Install Claude Code skill for PostgreSDK
8063
8108
  version Show version
8064
8109
  help Show help
8065
8110
 
@@ -8077,6 +8122,9 @@ Pull Options:
8077
8122
  --force, -y Delete stale files without prompting
8078
8123
  -c, --config <path> Path to config file with pull settings
8079
8124
 
8125
+ Install-skill Options:
8126
+ --force, -y Overwrite existing skill
8127
+
8080
8128
  Examples:
8081
8129
  postgresdk init # Create config file
8082
8130
  postgresdk generate # Generate using postgresdk.config.ts
@@ -8084,6 +8132,7 @@ Examples:
8084
8132
  postgresdk generate -c custom.config.ts
8085
8133
  postgresdk pull --from=https://api.com --output=./src/sdk
8086
8134
  postgresdk pull # Pull using config file
8135
+ postgresdk install-skill # Install Claude Code skill
8087
8136
  `);
8088
8137
  process.exit(0);
8089
8138
  }
@@ -8106,6 +8155,9 @@ if (command === "init") {
8106
8155
  } else if (command === "pull") {
8107
8156
  const { pullCommand: pullCommand2 } = await Promise.resolve().then(() => (init_cli_pull(), exports_cli_pull));
8108
8157
  await pullCommand2(args.slice(1));
8158
+ } else if (command === "install-skill") {
8159
+ const { installSkillCommand: installSkillCommand2 } = await Promise.resolve().then(() => (init_cli_install_skill(), exports_cli_install_skill));
8160
+ await installSkillCommand2(args.slice(1));
8109
8161
  } else {
8110
8162
  console.error(`❌ Unknown command: ${command}`);
8111
8163
  console.error(`Run 'postgresdk help' for usage information`);
package/dist/index.js CHANGED
@@ -3497,7 +3497,13 @@ export abstract class BaseClient {
3497
3497
  protected baseUrl: string,
3498
3498
  protected fetchFn: typeof fetch = fetch,
3499
3499
  protected auth?: AuthConfig
3500
- ) {}
3500
+ ) {
3501
+ // Rebind so \`this.fetchFn(...)\` calls don't pass the client as the receiver.
3502
+ // Browser \`fetch\` requires \`window\`/\`globalThis\` as \`this\` and throws
3503
+ // "Illegal invocation" otherwise. Covers both the default and any caller-supplied fetch.
3504
+ const f = this.fetchFn;
3505
+ this.fetchFn = ((...args: Parameters<typeof fetch>) => f(...args)) as typeof fetch;
3506
+ }
3501
3507
 
3502
3508
  protected async authHeaders(): Promise<HeaderMap> {
3503
3509
  if (!this.auth) return {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.19.4",
3
+ "version": "0.19.6",
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
  ],
@@ -22,7 +23,7 @@
22
23
  },
23
24
  "scripts": {
24
25
  "build": "bun build src/cli.ts src/index.ts --outdir dist --target node --format esm --external=pg --external=zod --external=hono --external=prompts --external=node:* && tsc -p tsconfig.build.json --emitDeclarationOnly",
25
- "test": "bun test:write-files && bun test:init && bun test:gen && bun test test/test-where-clause.test.ts && bun test test/test-where-or-and.test.ts && bun test test/test-nested-include-options.test.ts && bun test test/test-include-methods-with-options.test.ts && bun test:gen-with-tests && bun test:pull && bun test:enums && bun test:typecheck && bun test:drizzle-e2e && bun test test/test-numeric-mode-integration.test.ts && bun test test/test-jsonb-array-serialization.test.ts && bun test test/test-trigram-search.test.ts && bun test test/test-soft-delete-config.test.ts && bun test test/test-soft-delete-include-loader.test.ts && bun test test/test-soft-delete-nested-include.test.ts && bun test test/test-transaction.test.ts && bun test test/test-nullable-belongs-to.test.ts && bun test test/test-no-default-limit.test.ts",
26
+ "test": "bun test:write-files && bun test:init && bun test:gen && bun test test/test-where-clause.test.ts && bun test test/test-where-or-and.test.ts && bun test test/test-nested-include-options.test.ts && bun test test/test-include-methods-with-options.test.ts && bun test:gen-with-tests && bun test:pull && bun test:enums && bun test:typecheck && bun test:drizzle-e2e && bun test test/test-numeric-mode-integration.test.ts && bun test test/test-jsonb-array-serialization.test.ts && bun test test/test-trigram-search.test.ts && bun test test/test-soft-delete-config.test.ts && bun test test/test-soft-delete-include-loader.test.ts && bun test test/test-soft-delete-nested-include.test.ts && bun test test/test-transaction.test.ts && bun test test/test-nullable-belongs-to.test.ts && bun test test/test-no-default-limit.test.ts && bun test test/test-base-client-fetch-receiver.test.ts",
26
27
  "test:write-files": "bun test/test-write-files-if-changed.ts",
27
28
  "test:init": "bun test/test-init.ts",
28
29
  "test:gen": "bun test/test-gen.ts",
@@ -53,7 +54,8 @@
53
54
  "drizzle-kit": "^0.31.4",
54
55
  "drizzle-orm": "^0.44.4",
55
56
  "jose": "^6.0.12",
56
- "typescript": "^5.5.0"
57
+ "typescript": "^5.5.0",
58
+ "vitest": "^4.1.6"
57
59
  },
58
60
  "author": "Ben Honda <ben@theadpharm.com>",
59
61
  "license": "MIT",
@@ -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.