postgresdk 0.19.3 → 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
@@ -154,6 +154,7 @@ export default {
154
154
  },
155
155
  },
156
156
  numericMode: "auto", // "auto" | "number" | "string" - How to type numeric columns
157
+ maxLimit: 1000, // Max allowed `limit` value (0 = no cap)
157
158
  includeMethodsDepth: 2, // Max depth for nested includes
158
159
  dateType: "date", // "date" | "string" - How to handle timestamps
159
160
  serverFramework: "hono", // Currently only hono is supported
@@ -715,15 +716,15 @@ const result = await sdk.users.list({
715
716
  // Access results
716
717
  result.data; // User[] - array of records
717
718
  result.total; // number - total matching records
718
- result.limit; // number - page size used
719
+ result.limit; // number | undefined - page size used (absent when no limit specified)
719
720
  result.offset; // number - offset used
720
- result.hasMore; // boolean - more pages available
721
+ result.hasMore; // boolean - more pages available (false when no limit)
721
722
 
722
- // Note: Maximum limit is 1000 records per request
723
+ // Note: Omitting `limit` returns all matching records. Max limit is controlled by `maxLimit` config (default: 1000).
723
724
 
724
- // Calculate pagination info
725
- const totalPages = Math.ceil(result.total / result.limit);
726
- const currentPage = Math.floor(result.offset / result.limit) + 1;
725
+ // Calculate pagination info (when using explicit limit)
726
+ const totalPages = result.limit ? Math.ceil(result.total / result.limit) : 1;
727
+ const currentPage = result.limit ? Math.floor(result.offset / result.limit) + 1 : 1;
727
728
 
728
729
  // Multi-column sorting
729
730
  const sorted = await sdk.users.list({
@@ -1062,12 +1063,13 @@ Commands:
1062
1063
  init Create a postgresdk.config.ts file
1063
1064
  generate Generate SDK from database
1064
1065
  pull Pull SDK from API endpoint
1066
+ install-skill Install Claude Code skill for PostgreSDK
1065
1067
  version Show version
1066
1068
  help Show help
1067
1069
 
1068
1070
  Options:
1069
1071
  -c, --config <path> Path to config file (default: postgresdk.config.ts)
1070
- --force, -y Delete stale files without prompting (generate & pull)
1072
+ --force, -y Delete stale files without prompting (generate & pull); overwrite existing skill (install-skill)
1071
1073
 
1072
1074
  Init subcommands/flags:
1073
1075
  init pull Generate pull-only config (alias for --sdk)
@@ -1083,6 +1085,7 @@ Examples:
1083
1085
  npx postgresdk@latest generate --force # Skip stale file prompts
1084
1086
  npx postgresdk@latest pull --from=https://api.com --output=./src/sdk
1085
1087
  npx postgresdk@latest pull --from=https://api.com --output=./src/sdk --force
1088
+ npx postgresdk@latest install-skill # Install Claude Code skill
1086
1089
  ```
1087
1090
 
1088
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
@@ -1221,7 +1221,7 @@ function generateExampleValue(column) {
1221
1221
  }
1222
1222
  function generateQueryParams(table, enums) {
1223
1223
  const params = {
1224
- limit: "number - Max records to return (default: 50)",
1224
+ limit: "number - Max records to return (omit for all)",
1225
1225
  offset: "number - Records to skip",
1226
1226
  orderBy: "string | string[] - Field(s) to sort by",
1227
1227
  order: "'asc' | 'desc' | ('asc' | 'desc')[] - Sort direction(s)"
@@ -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";
@@ -2920,7 +2958,7 @@ export const Upsert${Type}Schema = z.object({
2920
2958
 
2921
2959
  // src/emit-params-zod.ts
2922
2960
  init_utils();
2923
- function emitParamsZod(table, graph) {
2961
+ function emitParamsZod(table, graph, opts) {
2924
2962
  const Type = pascal(table.name);
2925
2963
  const columnNames = table.columns.map((c) => `"${c.name}"`).join(", ");
2926
2964
  const pkCols = Array.isArray(table.pk) ? table.pk : table.pk ? [table.pk] : [];
@@ -2937,7 +2975,7 @@ export const ${Type}PkSchema = ${pkSchema};
2937
2975
  // Schema for list query parameters
2938
2976
  export const ${Type}ListParamsSchema = z.object({
2939
2977
  include: ${includeSpecSchema}.optional(),
2940
- limit: z.number().int().positive().max(1000).optional(),
2978
+ limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
2941
2979
  offset: z.number().int().nonnegative().optional(),
2942
2980
  where: z.any().optional(),
2943
2981
  vector: VectorSearchParamsSchema.optional(),
@@ -2959,12 +2997,12 @@ export type ${Type}OrderParams = z.infer<typeof ${Type}OrderParamsSchema>;
2959
2997
  }
2960
2998
 
2961
2999
  // src/emit-shared-params-zod.ts
2962
- function emitSharedParamsZod() {
3000
+ function emitSharedParamsZod(opts) {
2963
3001
  return `import { z } from "zod";
2964
3002
 
2965
3003
  // Shared pagination schema (used across all tables)
2966
3004
  export const PaginationParamsSchema = z.object({
2967
- limit: z.number().int().positive().max(1000).optional(),
3005
+ limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
2968
3006
  offset: z.number().int().nonnegative().optional()
2969
3007
  }).strict();
2970
3008
 
@@ -2996,8 +3034,8 @@ export interface PaginatedResponse<T> {
2996
3034
  data: T[];
2997
3035
  /** Total number of records matching the query (across all pages) */
2998
3036
  total: number;
2999
- /** Maximum number of records per page */
3000
- limit: number;
3037
+ /** Maximum number of records per page (absent when no limit was specified) */
3038
+ limit?: number;
3001
3039
  /** Number of records skipped (for pagination) */
3002
3040
  offset: number;
3003
3041
  /** Whether there are more records available after this page */
@@ -3072,7 +3110,7 @@ const listSchema = z.object({
3072
3110
  include: z.any().optional(),
3073
3111
  select: z.array(z.string()).min(1).optional(),
3074
3112
  exclude: z.array(z.string()).min(1).optional(),
3075
- limit: z.number().int().positive().max(1000).optional(),
3113
+ limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
3076
3114
  offset: z.number().int().min(0).optional(),
3077
3115
  orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
3078
3116
  order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional(),
@@ -3985,7 +4023,7 @@ ${hasJsonbColumns ? ` /**
3985
4023
  * @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
3986
4024
  * @param params.orderBy - Column(s) to sort by
3987
4025
  * @param params.order - Sort direction(s): "asc" or "desc"
3988
- * @param params.limit - Maximum number of records to return (default: 50, max: 1000)
4026
+ * @param params.limit - Maximum number of records to return${opts.maxLimit > 0 ? ` (max: ${opts.maxLimit})` : ""}. Omit to return all matching records.
3989
4027
  * @param params.offset - Number of records to skip for pagination
3990
4028
  * @param params.include - Related records to include (return type automatically infers included relations)
3991
4029
  * @returns Paginated results with all fields (and included relations if specified)
@@ -4085,7 +4123,7 @@ ${hasJsonbColumns ? ` /**
4085
4123
  * @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
4086
4124
  * @param params.orderBy - Column(s) to sort by
4087
4125
  * @param params.order - Sort direction(s): "asc" or "desc"
4088
- * @param params.limit - Maximum number of records to return (default: 50, max: 1000)
4126
+ * @param params.limit - Maximum number of records to return${opts.maxLimit > 0 ? ` (max: ${opts.maxLimit})` : ""}. Omit to return all matching records.
4089
4127
  * @param params.offset - Number of records to skip for pagination
4090
4128
  * @param params.include - Related records to include (return type automatically infers included relations)
4091
4129
  * @returns Paginated results with all fields (and included relations if specified)
@@ -6579,7 +6617,7 @@ export async function listRecords(
6579
6617
  }
6580
6618
  ): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
6581
6619
  try {
6582
- const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, trigram, distinctOn, includeSoftDeleted } = params;
6620
+ const { where: whereClause, limit, offset = 0, include, orderBy, order, vector, trigram, distinctOn, includeSoftDeleted } = params;
6583
6621
 
6584
6622
  // DISTINCT ON support
6585
6623
  const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
@@ -6715,10 +6753,22 @@ export async function listRecords(
6715
6753
  orderBySQL = buildOrderBySQL(userOrderCols, userDirs);
6716
6754
  }
6717
6755
 
6718
- // Add limit and offset params
6719
- const limitParam = \`$\${paramIndex}\`;
6720
- const offsetParam = \`$\${paramIndex + 1}\`;
6721
- const allParams = [...queryParams, ...whereParams, limit, offset];
6756
+ // Add limit and offset params (conditional — omit LIMIT clause when limit is undefined)
6757
+ let limitOffsetSQL: string;
6758
+ let allParams: any[];
6759
+ if (limit !== undefined) {
6760
+ const limitParam = \`$\${paramIndex}\`;
6761
+ const offsetParam = \`$\${paramIndex + 1}\`;
6762
+ limitOffsetSQL = \`LIMIT \${limitParam} OFFSET \${offsetParam}\`;
6763
+ allParams = [...queryParams, ...whereParams, limit, offset];
6764
+ } else if (offset > 0) {
6765
+ const offsetParam = \`$\${paramIndex}\`;
6766
+ limitOffsetSQL = \`OFFSET \${offsetParam}\`;
6767
+ allParams = [...queryParams, ...whereParams, offset];
6768
+ } else {
6769
+ limitOffsetSQL = '';
6770
+ allParams = [...queryParams, ...whereParams];
6771
+ }
6722
6772
 
6723
6773
  // Get total count for pagination
6724
6774
  const countText = distinctCols
@@ -6734,9 +6784,9 @@ export async function listRecords(
6734
6784
  // Inner query: DISTINCT ON with only the distinctCols ORDER BY prefix (PG requirement).
6735
6785
  // Outer query: free ORDER BY from the user's full orderBy list, plus LIMIT/OFFSET.
6736
6786
  const innerQuery = \`SELECT DISTINCT ON (\${_distinctOnColsSQL}) \${baseColumns} FROM "\${ctx.table}" \${whereSQL} ORDER BY \${_distinctOnColsSQL}\`;
6737
- text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
6787
+ text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL} \${limitOffsetSQL}\`.trim();
6738
6788
  } else {
6739
- text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
6789
+ text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} \${limitOffsetSQL}\`.trim();
6740
6790
  }
6741
6791
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
6742
6792
 
@@ -6744,7 +6794,7 @@ export async function listRecords(
6744
6794
  const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
6745
6795
 
6746
6796
  // Calculate hasMore
6747
- const hasMore = offset + limit < total;
6797
+ const hasMore = limit !== undefined ? offset + limit < total : false;
6748
6798
 
6749
6799
  const metadata = {
6750
6800
  data: parsedRows,
@@ -7784,6 +7834,7 @@ async function generate(configPath, options) {
7784
7834
  clientDir = join2(originalClientDir, "sdk");
7785
7835
  }
7786
7836
  const serverFramework = cfg.serverFramework || "hono";
7837
+ const maxLimit = cfg.maxLimit ?? 1000;
7787
7838
  const generateTests = cfg.tests?.generate ?? false;
7788
7839
  const originalTestDir = cfg.tests?.output || "./api/tests";
7789
7840
  let testDir = originalTestDir;
@@ -7812,7 +7863,7 @@ async function generate(configPath, options) {
7812
7863
  files.push({ path: join2(clientDir, "include-spec.ts"), content: includeSpec });
7813
7864
  const includeResolver = emitIncludeResolver(graph, cfg.useJsExtensions);
7814
7865
  files.push({ path: join2(clientDir, "include-resolver.ts"), content: includeResolver });
7815
- files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
7866
+ files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod({ maxLimit }) });
7816
7867
  files.push({ path: join2(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
7817
7868
  files.push({ path: join2(clientDir, "base-client.ts"), content: emitBaseClient() });
7818
7869
  files.push({ path: join2(clientDir, "where-types.ts"), content: emitWhereTypes() });
@@ -7849,7 +7900,7 @@ async function generate(configPath, options) {
7849
7900
  const zodSrc = emitZod(table, { numericMode }, model.enums);
7850
7901
  files.push({ path: join2(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
7851
7902
  files.push({ path: join2(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
7852
- const paramsZodSrc = emitParamsZod(table, graph);
7903
+ const paramsZodSrc = emitParamsZod(table, graph, { maxLimit });
7853
7904
  files.push({ path: join2(clientDir, "params", `${table.name}.ts`), content: paramsZodSrc });
7854
7905
  let routeContent;
7855
7906
  if (serverFramework === "hono") {
@@ -7859,7 +7910,8 @@ async function generate(configPath, options) {
7859
7910
  includeMethodsDepth: cfg.includeMethodsDepth || 2,
7860
7911
  authStrategy: getAuthStrategy(normalizedAuth),
7861
7912
  useJsExtensions: cfg.useJsExtensions,
7862
- apiPathPrefix: cfg.apiPathPrefix || "/v1"
7913
+ apiPathPrefix: cfg.apiPathPrefix || "/v1",
7914
+ maxLimit
7863
7915
  });
7864
7916
  } else {
7865
7917
  throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
@@ -7875,7 +7927,8 @@ async function generate(configPath, options) {
7875
7927
  exposeHardDelete,
7876
7928
  useJsExtensions: cfg.useJsExtensionsClient,
7877
7929
  includeMethodsDepth: cfg.includeMethodsDepth ?? 2,
7878
- skipJunctionTables: cfg.skipJunctionTables ?? true
7930
+ skipJunctionTables: cfg.skipJunctionTables ?? true,
7931
+ maxLimit
7879
7932
  }, model)
7880
7933
  });
7881
7934
  }
@@ -8020,13 +8073,13 @@ async function generate(configPath, options) {
8020
8073
  // src/cli.ts
8021
8074
  var import_config2 = __toESM(require_config(), 1);
8022
8075
  import { resolve as resolve3 } from "node:path";
8023
- import { readFileSync as readFileSync3 } from "node:fs";
8024
- import { fileURLToPath as fileURLToPath2 } from "node:url";
8025
- 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";
8026
8079
  init_cli_utils();
8027
- var __filename3 = fileURLToPath2(import.meta.url);
8028
- var __dirname3 = dirname4(__filename3);
8029
- 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"));
8030
8083
  var VERSION = packageJson.version;
8031
8084
  var args = process.argv.slice(2);
8032
8085
  var command = args[0];
@@ -8045,6 +8098,7 @@ Commands:
8045
8098
  init Create a postgresdk.config.ts file
8046
8099
  generate, gen Generate SDK from database
8047
8100
  pull Pull SDK from API endpoint
8101
+ install-skill Install Claude Code skill for PostgreSDK
8048
8102
  version Show version
8049
8103
  help Show help
8050
8104
 
@@ -8062,6 +8116,9 @@ Pull Options:
8062
8116
  --force, -y Delete stale files without prompting
8063
8117
  -c, --config <path> Path to config file with pull settings
8064
8118
 
8119
+ Install-skill Options:
8120
+ --force, -y Overwrite existing skill
8121
+
8065
8122
  Examples:
8066
8123
  postgresdk init # Create config file
8067
8124
  postgresdk generate # Generate using postgresdk.config.ts
@@ -8069,6 +8126,7 @@ Examples:
8069
8126
  postgresdk generate -c custom.config.ts
8070
8127
  postgresdk pull --from=https://api.com --output=./src/sdk
8071
8128
  postgresdk pull # Pull using config file
8129
+ postgresdk install-skill # Install Claude Code skill
8072
8130
  `);
8073
8131
  process.exit(0);
8074
8132
  }
@@ -8091,6 +8149,9 @@ if (command === "init") {
8091
8149
  } else if (command === "pull") {
8092
8150
  const { pullCommand: pullCommand2 } = await Promise.resolve().then(() => (init_cli_pull(), exports_cli_pull));
8093
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));
8094
8155
  } else {
8095
8156
  console.error(`❌ Unknown command: ${command}`);
8096
8157
  console.error(`Run 'postgresdk help' for usage information`);
@@ -6,6 +6,7 @@ export declare function emitClient(table: Table, graph: Graph, opts: {
6
6
  useJsExtensions?: boolean;
7
7
  includeMethodsDepth?: number;
8
8
  skipJunctionTables?: boolean;
9
+ maxLimit: number;
9
10
  }, model?: Model): string;
10
11
  export declare function emitClientIndex(tables: Table[], useJsExtensions?: boolean, graph?: Graph, includeOpts?: {
11
12
  maxDepth: number;
@@ -1,3 +1,5 @@
1
1
  import type { Table } from "./introspect";
2
2
  import type { Graph } from "./rel-classify";
3
- export declare function emitParamsZod(table: Table, graph: Graph): string;
3
+ export declare function emitParamsZod(table: Table, graph: Graph, opts: {
4
+ maxLimit: number;
5
+ }): string;
@@ -10,4 +10,5 @@ export declare function emitHonoRoutes(table: Table, _graph: Graph, opts: {
10
10
  authStrategy?: string;
11
11
  useJsExtensions?: boolean;
12
12
  apiPathPrefix: string;
13
+ maxLimit: number;
13
14
  }): string;
@@ -18,4 +18,5 @@ export declare function emitRoutes(table: Table, _graph: Graph, opts: {
18
18
  softDeleteColumn: string | null;
19
19
  includeMethodsDepth: number;
20
20
  authStrategy?: string;
21
+ maxLimit: number;
21
22
  }): string;
@@ -1 +1,3 @@
1
- export declare function emitSharedParamsZod(): string;
1
+ export declare function emitSharedParamsZod(opts: {
2
+ maxLimit: number;
3
+ }): string;
package/dist/index.js CHANGED
@@ -1220,7 +1220,7 @@ function generateExampleValue(column) {
1220
1220
  }
1221
1221
  function generateQueryParams(table, enums) {
1222
1222
  const params = {
1223
- limit: "number - Max records to return (default: 50)",
1223
+ limit: "number - Max records to return (omit for all)",
1224
1224
  offset: "number - Records to skip",
1225
1225
  orderBy: "string | string[] - Field(s) to sort by",
1226
1226
  order: "'asc' | 'desc' | ('asc' | 'desc')[] - Sort direction(s)"
@@ -1960,7 +1960,7 @@ export const Upsert${Type}Schema = z.object({
1960
1960
 
1961
1961
  // src/emit-params-zod.ts
1962
1962
  init_utils();
1963
- function emitParamsZod(table, graph) {
1963
+ function emitParamsZod(table, graph, opts) {
1964
1964
  const Type = pascal(table.name);
1965
1965
  const columnNames = table.columns.map((c) => `"${c.name}"`).join(", ");
1966
1966
  const pkCols = Array.isArray(table.pk) ? table.pk : table.pk ? [table.pk] : [];
@@ -1977,7 +1977,7 @@ export const ${Type}PkSchema = ${pkSchema};
1977
1977
  // Schema for list query parameters
1978
1978
  export const ${Type}ListParamsSchema = z.object({
1979
1979
  include: ${includeSpecSchema}.optional(),
1980
- limit: z.number().int().positive().max(1000).optional(),
1980
+ limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
1981
1981
  offset: z.number().int().nonnegative().optional(),
1982
1982
  where: z.any().optional(),
1983
1983
  vector: VectorSearchParamsSchema.optional(),
@@ -1999,12 +1999,12 @@ export type ${Type}OrderParams = z.infer<typeof ${Type}OrderParamsSchema>;
1999
1999
  }
2000
2000
 
2001
2001
  // src/emit-shared-params-zod.ts
2002
- function emitSharedParamsZod() {
2002
+ function emitSharedParamsZod(opts) {
2003
2003
  return `import { z } from "zod";
2004
2004
 
2005
2005
  // Shared pagination schema (used across all tables)
2006
2006
  export const PaginationParamsSchema = z.object({
2007
- limit: z.number().int().positive().max(1000).optional(),
2007
+ limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
2008
2008
  offset: z.number().int().nonnegative().optional()
2009
2009
  }).strict();
2010
2010
 
@@ -2036,8 +2036,8 @@ export interface PaginatedResponse<T> {
2036
2036
  data: T[];
2037
2037
  /** Total number of records matching the query (across all pages) */
2038
2038
  total: number;
2039
- /** Maximum number of records per page */
2040
- limit: number;
2039
+ /** Maximum number of records per page (absent when no limit was specified) */
2040
+ limit?: number;
2041
2041
  /** Number of records skipped (for pagination) */
2042
2042
  offset: number;
2043
2043
  /** Whether there are more records available after this page */
@@ -2112,7 +2112,7 @@ const listSchema = z.object({
2112
2112
  include: z.any().optional(),
2113
2113
  select: z.array(z.string()).min(1).optional(),
2114
2114
  exclude: z.array(z.string()).min(1).optional(),
2115
- limit: z.number().int().positive().max(1000).optional(),
2115
+ limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
2116
2116
  offset: z.number().int().min(0).optional(),
2117
2117
  orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
2118
2118
  order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional(),
@@ -3025,7 +3025,7 @@ ${hasJsonbColumns ? ` /**
3025
3025
  * @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
3026
3026
  * @param params.orderBy - Column(s) to sort by
3027
3027
  * @param params.order - Sort direction(s): "asc" or "desc"
3028
- * @param params.limit - Maximum number of records to return (default: 50, max: 1000)
3028
+ * @param params.limit - Maximum number of records to return${opts.maxLimit > 0 ? ` (max: ${opts.maxLimit})` : ""}. Omit to return all matching records.
3029
3029
  * @param params.offset - Number of records to skip for pagination
3030
3030
  * @param params.include - Related records to include (return type automatically infers included relations)
3031
3031
  * @returns Paginated results with all fields (and included relations if specified)
@@ -3125,7 +3125,7 @@ ${hasJsonbColumns ? ` /**
3125
3125
  * @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
3126
3126
  * @param params.orderBy - Column(s) to sort by
3127
3127
  * @param params.order - Sort direction(s): "asc" or "desc"
3128
- * @param params.limit - Maximum number of records to return (default: 50, max: 1000)
3128
+ * @param params.limit - Maximum number of records to return${opts.maxLimit > 0 ? ` (max: ${opts.maxLimit})` : ""}. Omit to return all matching records.
3129
3129
  * @param params.offset - Number of records to skip for pagination
3130
3130
  * @param params.include - Related records to include (return type automatically infers included relations)
3131
3131
  * @returns Paginated results with all fields (and included relations if specified)
@@ -5619,7 +5619,7 @@ export async function listRecords(
5619
5619
  }
5620
5620
  ): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
5621
5621
  try {
5622
- const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, trigram, distinctOn, includeSoftDeleted } = params;
5622
+ const { where: whereClause, limit, offset = 0, include, orderBy, order, vector, trigram, distinctOn, includeSoftDeleted } = params;
5623
5623
 
5624
5624
  // DISTINCT ON support
5625
5625
  const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
@@ -5755,10 +5755,22 @@ export async function listRecords(
5755
5755
  orderBySQL = buildOrderBySQL(userOrderCols, userDirs);
5756
5756
  }
5757
5757
 
5758
- // Add limit and offset params
5759
- const limitParam = \`$\${paramIndex}\`;
5760
- const offsetParam = \`$\${paramIndex + 1}\`;
5761
- const allParams = [...queryParams, ...whereParams, limit, offset];
5758
+ // Add limit and offset params (conditional — omit LIMIT clause when limit is undefined)
5759
+ let limitOffsetSQL: string;
5760
+ let allParams: any[];
5761
+ if (limit !== undefined) {
5762
+ const limitParam = \`$\${paramIndex}\`;
5763
+ const offsetParam = \`$\${paramIndex + 1}\`;
5764
+ limitOffsetSQL = \`LIMIT \${limitParam} OFFSET \${offsetParam}\`;
5765
+ allParams = [...queryParams, ...whereParams, limit, offset];
5766
+ } else if (offset > 0) {
5767
+ const offsetParam = \`$\${paramIndex}\`;
5768
+ limitOffsetSQL = \`OFFSET \${offsetParam}\`;
5769
+ allParams = [...queryParams, ...whereParams, offset];
5770
+ } else {
5771
+ limitOffsetSQL = '';
5772
+ allParams = [...queryParams, ...whereParams];
5773
+ }
5762
5774
 
5763
5775
  // Get total count for pagination
5764
5776
  const countText = distinctCols
@@ -5774,9 +5786,9 @@ export async function listRecords(
5774
5786
  // Inner query: DISTINCT ON with only the distinctCols ORDER BY prefix (PG requirement).
5775
5787
  // Outer query: free ORDER BY from the user's full orderBy list, plus LIMIT/OFFSET.
5776
5788
  const innerQuery = \`SELECT DISTINCT ON (\${_distinctOnColsSQL}) \${baseColumns} FROM "\${ctx.table}" \${whereSQL} ORDER BY \${_distinctOnColsSQL}\`;
5777
- text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
5789
+ text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL} \${limitOffsetSQL}\`.trim();
5778
5790
  } else {
5779
- text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
5791
+ text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} \${limitOffsetSQL}\`.trim();
5780
5792
  }
5781
5793
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
5782
5794
 
@@ -5784,7 +5796,7 @@ export async function listRecords(
5784
5796
  const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
5785
5797
 
5786
5798
  // Calculate hasMore
5787
- const hasMore = offset + limit < total;
5799
+ const hasMore = limit !== undefined ? offset + limit < total : false;
5788
5800
 
5789
5801
  const metadata = {
5790
5802
  data: parsedRows,
@@ -6824,6 +6836,7 @@ async function generate(configPath, options) {
6824
6836
  clientDir = join2(originalClientDir, "sdk");
6825
6837
  }
6826
6838
  const serverFramework = cfg.serverFramework || "hono";
6839
+ const maxLimit = cfg.maxLimit ?? 1000;
6827
6840
  const generateTests = cfg.tests?.generate ?? false;
6828
6841
  const originalTestDir = cfg.tests?.output || "./api/tests";
6829
6842
  let testDir = originalTestDir;
@@ -6852,7 +6865,7 @@ async function generate(configPath, options) {
6852
6865
  files.push({ path: join2(clientDir, "include-spec.ts"), content: includeSpec });
6853
6866
  const includeResolver = emitIncludeResolver(graph, cfg.useJsExtensions);
6854
6867
  files.push({ path: join2(clientDir, "include-resolver.ts"), content: includeResolver });
6855
- files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
6868
+ files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod({ maxLimit }) });
6856
6869
  files.push({ path: join2(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
6857
6870
  files.push({ path: join2(clientDir, "base-client.ts"), content: emitBaseClient() });
6858
6871
  files.push({ path: join2(clientDir, "where-types.ts"), content: emitWhereTypes() });
@@ -6889,7 +6902,7 @@ async function generate(configPath, options) {
6889
6902
  const zodSrc = emitZod(table, { numericMode }, model.enums);
6890
6903
  files.push({ path: join2(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
6891
6904
  files.push({ path: join2(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
6892
- const paramsZodSrc = emitParamsZod(table, graph);
6905
+ const paramsZodSrc = emitParamsZod(table, graph, { maxLimit });
6893
6906
  files.push({ path: join2(clientDir, "params", `${table.name}.ts`), content: paramsZodSrc });
6894
6907
  let routeContent;
6895
6908
  if (serverFramework === "hono") {
@@ -6899,7 +6912,8 @@ async function generate(configPath, options) {
6899
6912
  includeMethodsDepth: cfg.includeMethodsDepth || 2,
6900
6913
  authStrategy: getAuthStrategy(normalizedAuth),
6901
6914
  useJsExtensions: cfg.useJsExtensions,
6902
- apiPathPrefix: cfg.apiPathPrefix || "/v1"
6915
+ apiPathPrefix: cfg.apiPathPrefix || "/v1",
6916
+ maxLimit
6903
6917
  });
6904
6918
  } else {
6905
6919
  throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
@@ -6915,7 +6929,8 @@ async function generate(configPath, options) {
6915
6929
  exposeHardDelete,
6916
6930
  useJsExtensions: cfg.useJsExtensionsClient,
6917
6931
  includeMethodsDepth: cfg.includeMethodsDepth ?? 2,
6918
- skipJunctionTables: cfg.skipJunctionTables ?? true
6932
+ skipJunctionTables: cfg.skipJunctionTables ?? true,
6933
+ maxLimit
6919
6934
  }, model)
6920
6935
  });
6921
6936
  }
package/dist/types.d.ts CHANGED
@@ -38,6 +38,8 @@ export interface Config {
38
38
  skipJunctionTables?: boolean;
39
39
  serverFramework?: "hono" | "express" | "fastify";
40
40
  apiPathPrefix?: string;
41
+ /** Maximum allowed value for the `limit` parameter in list operations (default: 1000). Set to 0 to disable. */
42
+ maxLimit?: number;
41
43
  auth?: AuthConfigInput;
42
44
  pullToken?: string;
43
45
  pull?: PullConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.19.3",
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
  ],
@@ -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",
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",
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",
@@ -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.