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 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";
@@ -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 readFileSync3 } from "node:fs";
8039
- import { fileURLToPath as fileURLToPath2 } from "node:url";
8040
- import { dirname as dirname4, join as join4 } from "node:path";
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 __filename3 = fileURLToPath2(import.meta.url);
8043
- var __dirname3 = dirname4(__filename3);
8044
- var packageJson = JSON.parse(readFileSync3(join4(__dirname3, "../package.json"), "utf-8"));
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.4",
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.