postgresdk 0.19.2 → 0.19.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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({
package/dist/cli.js CHANGED
@@ -583,7 +583,8 @@ function buildReturnType(baseTable, path, isMany, targets, graph) {
583
583
  continue;
584
584
  const targetType = `Select${pascal(target)}`;
585
585
  if (i === 0) {
586
- parts.push(`${key}: ${isMany[i] ? `${targetType}[]` : targetType}`);
586
+ const edgeNull = !isMany[i] && graph[baseTable]?.[key]?.nullable ? " | null" : "";
587
+ parts.push(`${key}: ${isMany[i] ? `${targetType}[]` : `${targetType}${edgeNull}`}`);
587
588
  } else {
588
589
  let nestedType = targetType;
589
590
  for (let j = i;j < path.length; j++) {
@@ -593,13 +594,26 @@ function buildReturnType(baseTable, path, isMany, targets, graph) {
593
594
  if (!nestedKey || !nestedTarget)
594
595
  continue;
595
596
  const nestedTargetType = `Select${pascal(nestedTarget)}`;
596
- nestedType = `${nestedType} & { ${nestedKey}: ${isMany[j] ? `${nestedTargetType}[]` : nestedTargetType} }`;
597
+ const nestedSource = targets[j - 1];
598
+ const nestedNull = !isMany[j] && graph[nestedSource]?.[nestedKey]?.nullable ? " | null" : "";
599
+ nestedType = `${nestedType} & { ${nestedKey}: ${isMany[j] ? `${nestedTargetType}[]` : `${nestedTargetType}${nestedNull}`} }`;
597
600
  }
598
601
  }
599
602
  const prevKey = path[i - 1];
600
603
  const prevTarget = targets[i - 1];
601
604
  if (prevKey && prevTarget) {
602
- parts[parts.length - 1] = `${prevKey}: ${isMany[i - 1] ? `(Select${pascal(prevTarget)} & { ${key}: ${isMany[i] ? `${targetType}[]` : targetType} })[]` : `Select${pascal(prevTarget)} & { ${key}: ${isMany[i] ? `${targetType}[]` : targetType} }`}`;
605
+ const prevSource = i - 1 === 0 ? baseTable : targets[i - 2];
606
+ const innerNull = !isMany[i] && graph[prevTarget]?.[key]?.nullable ? " | null" : "";
607
+ const prevNullable = !isMany[i - 1] && graph[prevSource]?.[prevKey]?.nullable;
608
+ const inner = isMany[i] ? `${targetType}[]` : `${targetType}${innerNull}`;
609
+ const composite = `Select${pascal(prevTarget)} & { ${key}: ${inner} }`;
610
+ if (isMany[i - 1]) {
611
+ parts[parts.length - 1] = `${prevKey}: (${composite})[]`;
612
+ } else if (prevNullable) {
613
+ parts[parts.length - 1] = `${prevKey}: (${composite}) | null`;
614
+ } else {
615
+ parts[parts.length - 1] = `${prevKey}: ${composite}`;
616
+ }
603
617
  }
604
618
  break;
605
619
  }
@@ -689,8 +703,10 @@ function generateIncludeMethods(table, graph, opts, allTables) {
689
703
  }
690
704
  const combinedPath = [key1, key2];
691
705
  const combinedSuffix = `With${pascal(key1)}And${pascal(key2)}`;
692
- const type1 = `${key1}: ${edge1.kind === "many" ? `Select${pascal(edge1.target)}[]` : `Select${pascal(edge1.target)}`}`;
693
- const type2 = `${key2}: ${edge2.kind === "many" ? `Select${pascal(edge2.target)}[]` : `Select${pascal(edge2.target)}`}`;
706
+ const null1 = edge1.kind === "one" && edge1.nullable ? " | null" : "";
707
+ const null2 = edge2.kind === "one" && edge2.nullable ? " | null" : "";
708
+ const type1 = `${key1}: ${edge1.kind === "many" ? `Select${pascal(edge1.target)}[]` : `Select${pascal(edge1.target)}${null1}`}`;
709
+ const type2 = `${key2}: ${edge2.kind === "many" ? `Select${pascal(edge2.target)}[]` : `Select${pascal(edge2.target)}${null2}`}`;
694
710
  const combinedBaseType = `Select${pascal(baseTableName)} & { ${type1}; ${type2} }`;
695
711
  const combinedTypeName = `Select${pascal(baseTableName)}${combinedSuffix}`;
696
712
  methods.push({
@@ -1205,7 +1221,7 @@ function generateExampleValue(column) {
1205
1221
  }
1206
1222
  function generateQueryParams(table, enums) {
1207
1223
  const params = {
1208
- limit: "number - Max records to return (default: 50)",
1224
+ limit: "number - Max records to return (omit for all)",
1209
1225
  offset: "number - Records to skip",
1210
1226
  orderBy: "string | string[] - Field(s) to sort by",
1211
1227
  order: "'asc' | 'desc' | ('asc' | 'desc')[] - Sort direction(s)"
@@ -2639,7 +2655,14 @@ function buildGraph(model) {
2639
2655
  const upKey = singular(parent.name);
2640
2656
  const downKey = plural(child.name);
2641
2657
  if (!(upKey in childNode)) {
2642
- childNode[upKey] = { from: child.name, key: upKey, kind: "one", target: parent.name };
2658
+ const fkNullable = fk.from.some((colName) => child.columns.find((c) => c.name === colName)?.nullable);
2659
+ childNode[upKey] = {
2660
+ from: child.name,
2661
+ key: upKey,
2662
+ kind: "one",
2663
+ target: parent.name,
2664
+ ...fkNullable && { nullable: true }
2665
+ };
2643
2666
  }
2644
2667
  if (!(downKey in parentNode)) {
2645
2668
  parentNode[downKey] = { from: parent.name, key: downKey, kind: "many", target: child.name };
@@ -2757,10 +2780,11 @@ function emitIncludeResolver(graph, useJsExtensions) {
2757
2780
  : Select${targetType}[]
2758
2781
  )`;
2759
2782
  } else {
2783
+ const nullSuffix = edge.nullable ? " | null" : "";
2760
2784
  out += `(
2761
2785
  TInclude[K] extends { include: infer U extends ${targetType}IncludeSpec }
2762
- ? ${targetType}WithIncludes<U>
2763
- : Select${targetType}
2786
+ ? ${targetType}WithIncludes<U>${nullSuffix}
2787
+ : Select${targetType}${nullSuffix}
2764
2788
  )`;
2765
2789
  }
2766
2790
  out += ` :${isLast ? `
@@ -2896,7 +2920,7 @@ export const Upsert${Type}Schema = z.object({
2896
2920
 
2897
2921
  // src/emit-params-zod.ts
2898
2922
  init_utils();
2899
- function emitParamsZod(table, graph) {
2923
+ function emitParamsZod(table, graph, opts) {
2900
2924
  const Type = pascal(table.name);
2901
2925
  const columnNames = table.columns.map((c) => `"${c.name}"`).join(", ");
2902
2926
  const pkCols = Array.isArray(table.pk) ? table.pk : table.pk ? [table.pk] : [];
@@ -2913,7 +2937,7 @@ export const ${Type}PkSchema = ${pkSchema};
2913
2937
  // Schema for list query parameters
2914
2938
  export const ${Type}ListParamsSchema = z.object({
2915
2939
  include: ${includeSpecSchema}.optional(),
2916
- limit: z.number().int().positive().max(1000).optional(),
2940
+ limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
2917
2941
  offset: z.number().int().nonnegative().optional(),
2918
2942
  where: z.any().optional(),
2919
2943
  vector: VectorSearchParamsSchema.optional(),
@@ -2935,12 +2959,12 @@ export type ${Type}OrderParams = z.infer<typeof ${Type}OrderParamsSchema>;
2935
2959
  }
2936
2960
 
2937
2961
  // src/emit-shared-params-zod.ts
2938
- function emitSharedParamsZod() {
2962
+ function emitSharedParamsZod(opts) {
2939
2963
  return `import { z } from "zod";
2940
2964
 
2941
2965
  // Shared pagination schema (used across all tables)
2942
2966
  export const PaginationParamsSchema = z.object({
2943
- limit: z.number().int().positive().max(1000).optional(),
2967
+ limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
2944
2968
  offset: z.number().int().nonnegative().optional()
2945
2969
  }).strict();
2946
2970
 
@@ -2972,8 +2996,8 @@ export interface PaginatedResponse<T> {
2972
2996
  data: T[];
2973
2997
  /** Total number of records matching the query (across all pages) */
2974
2998
  total: number;
2975
- /** Maximum number of records per page */
2976
- limit: number;
2999
+ /** Maximum number of records per page (absent when no limit was specified) */
3000
+ limit?: number;
2977
3001
  /** Number of records skipped (for pagination) */
2978
3002
  offset: number;
2979
3003
  /** Whether there are more records available after this page */
@@ -3048,7 +3072,7 @@ const listSchema = z.object({
3048
3072
  include: z.any().optional(),
3049
3073
  select: z.array(z.string()).min(1).optional(),
3050
3074
  exclude: z.array(z.string()).min(1).optional(),
3051
- limit: z.number().int().positive().max(1000).optional(),
3075
+ limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
3052
3076
  offset: z.number().int().min(0).optional(),
3053
3077
  orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
3054
3078
  order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional(),
@@ -3961,7 +3985,7 @@ ${hasJsonbColumns ? ` /**
3961
3985
  * @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
3962
3986
  * @param params.orderBy - Column(s) to sort by
3963
3987
  * @param params.order - Sort direction(s): "asc" or "desc"
3964
- * @param params.limit - Maximum number of records to return (default: 50, max: 1000)
3988
+ * @param params.limit - Maximum number of records to return${opts.maxLimit > 0 ? ` (max: ${opts.maxLimit})` : ""}. Omit to return all matching records.
3965
3989
  * @param params.offset - Number of records to skip for pagination
3966
3990
  * @param params.include - Related records to include (return type automatically infers included relations)
3967
3991
  * @returns Paginated results with all fields (and included relations if specified)
@@ -4061,7 +4085,7 @@ ${hasJsonbColumns ? ` /**
4061
4085
  * @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
4062
4086
  * @param params.orderBy - Column(s) to sort by
4063
4087
  * @param params.order - Sort direction(s): "asc" or "desc"
4064
- * @param params.limit - Maximum number of records to return (default: 50, max: 1000)
4088
+ * @param params.limit - Maximum number of records to return${opts.maxLimit > 0 ? ` (max: ${opts.maxLimit})` : ""}. Omit to return all matching records.
4065
4089
  * @param params.offset - Number of records to skip for pagination
4066
4090
  * @param params.include - Related records to include (return type automatically infers included relations)
4067
4091
  * @returns Paginated results with all fields (and included relations if specified)
@@ -6555,7 +6579,7 @@ export async function listRecords(
6555
6579
  }
6556
6580
  ): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
6557
6581
  try {
6558
- const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, trigram, distinctOn, includeSoftDeleted } = params;
6582
+ const { where: whereClause, limit, offset = 0, include, orderBy, order, vector, trigram, distinctOn, includeSoftDeleted } = params;
6559
6583
 
6560
6584
  // DISTINCT ON support
6561
6585
  const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
@@ -6691,10 +6715,22 @@ export async function listRecords(
6691
6715
  orderBySQL = buildOrderBySQL(userOrderCols, userDirs);
6692
6716
  }
6693
6717
 
6694
- // Add limit and offset params
6695
- const limitParam = \`$\${paramIndex}\`;
6696
- const offsetParam = \`$\${paramIndex + 1}\`;
6697
- const allParams = [...queryParams, ...whereParams, limit, offset];
6718
+ // Add limit and offset params (conditional — omit LIMIT clause when limit is undefined)
6719
+ let limitOffsetSQL: string;
6720
+ let allParams: any[];
6721
+ if (limit !== undefined) {
6722
+ const limitParam = \`$\${paramIndex}\`;
6723
+ const offsetParam = \`$\${paramIndex + 1}\`;
6724
+ limitOffsetSQL = \`LIMIT \${limitParam} OFFSET \${offsetParam}\`;
6725
+ allParams = [...queryParams, ...whereParams, limit, offset];
6726
+ } else if (offset > 0) {
6727
+ const offsetParam = \`$\${paramIndex}\`;
6728
+ limitOffsetSQL = \`OFFSET \${offsetParam}\`;
6729
+ allParams = [...queryParams, ...whereParams, offset];
6730
+ } else {
6731
+ limitOffsetSQL = '';
6732
+ allParams = [...queryParams, ...whereParams];
6733
+ }
6698
6734
 
6699
6735
  // Get total count for pagination
6700
6736
  const countText = distinctCols
@@ -6710,9 +6746,9 @@ export async function listRecords(
6710
6746
  // Inner query: DISTINCT ON with only the distinctCols ORDER BY prefix (PG requirement).
6711
6747
  // Outer query: free ORDER BY from the user's full orderBy list, plus LIMIT/OFFSET.
6712
6748
  const innerQuery = \`SELECT DISTINCT ON (\${_distinctOnColsSQL}) \${baseColumns} FROM "\${ctx.table}" \${whereSQL} ORDER BY \${_distinctOnColsSQL}\`;
6713
- text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
6749
+ text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL} \${limitOffsetSQL}\`.trim();
6714
6750
  } else {
6715
- text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
6751
+ text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} \${limitOffsetSQL}\`.trim();
6716
6752
  }
6717
6753
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
6718
6754
 
@@ -6720,7 +6756,7 @@ export async function listRecords(
6720
6756
  const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
6721
6757
 
6722
6758
  // Calculate hasMore
6723
- const hasMore = offset + limit < total;
6759
+ const hasMore = limit !== undefined ? offset + limit < total : false;
6724
6760
 
6725
6761
  const metadata = {
6726
6762
  data: parsedRows,
@@ -7760,6 +7796,7 @@ async function generate(configPath, options) {
7760
7796
  clientDir = join2(originalClientDir, "sdk");
7761
7797
  }
7762
7798
  const serverFramework = cfg.serverFramework || "hono";
7799
+ const maxLimit = cfg.maxLimit ?? 1000;
7763
7800
  const generateTests = cfg.tests?.generate ?? false;
7764
7801
  const originalTestDir = cfg.tests?.output || "./api/tests";
7765
7802
  let testDir = originalTestDir;
@@ -7788,7 +7825,7 @@ async function generate(configPath, options) {
7788
7825
  files.push({ path: join2(clientDir, "include-spec.ts"), content: includeSpec });
7789
7826
  const includeResolver = emitIncludeResolver(graph, cfg.useJsExtensions);
7790
7827
  files.push({ path: join2(clientDir, "include-resolver.ts"), content: includeResolver });
7791
- files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
7828
+ files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod({ maxLimit }) });
7792
7829
  files.push({ path: join2(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
7793
7830
  files.push({ path: join2(clientDir, "base-client.ts"), content: emitBaseClient() });
7794
7831
  files.push({ path: join2(clientDir, "where-types.ts"), content: emitWhereTypes() });
@@ -7825,7 +7862,7 @@ async function generate(configPath, options) {
7825
7862
  const zodSrc = emitZod(table, { numericMode }, model.enums);
7826
7863
  files.push({ path: join2(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
7827
7864
  files.push({ path: join2(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
7828
- const paramsZodSrc = emitParamsZod(table, graph);
7865
+ const paramsZodSrc = emitParamsZod(table, graph, { maxLimit });
7829
7866
  files.push({ path: join2(clientDir, "params", `${table.name}.ts`), content: paramsZodSrc });
7830
7867
  let routeContent;
7831
7868
  if (serverFramework === "hono") {
@@ -7835,7 +7872,8 @@ async function generate(configPath, options) {
7835
7872
  includeMethodsDepth: cfg.includeMethodsDepth || 2,
7836
7873
  authStrategy: getAuthStrategy(normalizedAuth),
7837
7874
  useJsExtensions: cfg.useJsExtensions,
7838
- apiPathPrefix: cfg.apiPathPrefix || "/v1"
7875
+ apiPathPrefix: cfg.apiPathPrefix || "/v1",
7876
+ maxLimit
7839
7877
  });
7840
7878
  } else {
7841
7879
  throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
@@ -7851,7 +7889,8 @@ async function generate(configPath, options) {
7851
7889
  exposeHardDelete,
7852
7890
  useJsExtensions: cfg.useJsExtensionsClient,
7853
7891
  includeMethodsDepth: cfg.includeMethodsDepth ?? 2,
7854
- skipJunctionTables: cfg.skipJunctionTables ?? true
7892
+ skipJunctionTables: cfg.skipJunctionTables ?? true,
7893
+ maxLimit
7855
7894
  }, model)
7856
7895
  });
7857
7896
  }
@@ -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
@@ -582,7 +582,8 @@ function buildReturnType(baseTable, path, isMany, targets, graph) {
582
582
  continue;
583
583
  const targetType = `Select${pascal(target)}`;
584
584
  if (i === 0) {
585
- parts.push(`${key}: ${isMany[i] ? `${targetType}[]` : targetType}`);
585
+ const edgeNull = !isMany[i] && graph[baseTable]?.[key]?.nullable ? " | null" : "";
586
+ parts.push(`${key}: ${isMany[i] ? `${targetType}[]` : `${targetType}${edgeNull}`}`);
586
587
  } else {
587
588
  let nestedType = targetType;
588
589
  for (let j = i;j < path.length; j++) {
@@ -592,13 +593,26 @@ function buildReturnType(baseTable, path, isMany, targets, graph) {
592
593
  if (!nestedKey || !nestedTarget)
593
594
  continue;
594
595
  const nestedTargetType = `Select${pascal(nestedTarget)}`;
595
- nestedType = `${nestedType} & { ${nestedKey}: ${isMany[j] ? `${nestedTargetType}[]` : nestedTargetType} }`;
596
+ const nestedSource = targets[j - 1];
597
+ const nestedNull = !isMany[j] && graph[nestedSource]?.[nestedKey]?.nullable ? " | null" : "";
598
+ nestedType = `${nestedType} & { ${nestedKey}: ${isMany[j] ? `${nestedTargetType}[]` : `${nestedTargetType}${nestedNull}`} }`;
596
599
  }
597
600
  }
598
601
  const prevKey = path[i - 1];
599
602
  const prevTarget = targets[i - 1];
600
603
  if (prevKey && prevTarget) {
601
- parts[parts.length - 1] = `${prevKey}: ${isMany[i - 1] ? `(Select${pascal(prevTarget)} & { ${key}: ${isMany[i] ? `${targetType}[]` : targetType} })[]` : `Select${pascal(prevTarget)} & { ${key}: ${isMany[i] ? `${targetType}[]` : targetType} }`}`;
604
+ const prevSource = i - 1 === 0 ? baseTable : targets[i - 2];
605
+ const innerNull = !isMany[i] && graph[prevTarget]?.[key]?.nullable ? " | null" : "";
606
+ const prevNullable = !isMany[i - 1] && graph[prevSource]?.[prevKey]?.nullable;
607
+ const inner = isMany[i] ? `${targetType}[]` : `${targetType}${innerNull}`;
608
+ const composite = `Select${pascal(prevTarget)} & { ${key}: ${inner} }`;
609
+ if (isMany[i - 1]) {
610
+ parts[parts.length - 1] = `${prevKey}: (${composite})[]`;
611
+ } else if (prevNullable) {
612
+ parts[parts.length - 1] = `${prevKey}: (${composite}) | null`;
613
+ } else {
614
+ parts[parts.length - 1] = `${prevKey}: ${composite}`;
615
+ }
602
616
  }
603
617
  break;
604
618
  }
@@ -688,8 +702,10 @@ function generateIncludeMethods(table, graph, opts, allTables) {
688
702
  }
689
703
  const combinedPath = [key1, key2];
690
704
  const combinedSuffix = `With${pascal(key1)}And${pascal(key2)}`;
691
- const type1 = `${key1}: ${edge1.kind === "many" ? `Select${pascal(edge1.target)}[]` : `Select${pascal(edge1.target)}`}`;
692
- const type2 = `${key2}: ${edge2.kind === "many" ? `Select${pascal(edge2.target)}[]` : `Select${pascal(edge2.target)}`}`;
705
+ const null1 = edge1.kind === "one" && edge1.nullable ? " | null" : "";
706
+ const null2 = edge2.kind === "one" && edge2.nullable ? " | null" : "";
707
+ const type1 = `${key1}: ${edge1.kind === "many" ? `Select${pascal(edge1.target)}[]` : `Select${pascal(edge1.target)}${null1}`}`;
708
+ const type2 = `${key2}: ${edge2.kind === "many" ? `Select${pascal(edge2.target)}[]` : `Select${pascal(edge2.target)}${null2}`}`;
693
709
  const combinedBaseType = `Select${pascal(baseTableName)} & { ${type1}; ${type2} }`;
694
710
  const combinedTypeName = `Select${pascal(baseTableName)}${combinedSuffix}`;
695
711
  methods.push({
@@ -1204,7 +1220,7 @@ function generateExampleValue(column) {
1204
1220
  }
1205
1221
  function generateQueryParams(table, enums) {
1206
1222
  const params = {
1207
- limit: "number - Max records to return (default: 50)",
1223
+ limit: "number - Max records to return (omit for all)",
1208
1224
  offset: "number - Records to skip",
1209
1225
  orderBy: "string | string[] - Field(s) to sort by",
1210
1226
  order: "'asc' | 'desc' | ('asc' | 'desc')[] - Sort direction(s)"
@@ -1679,7 +1695,14 @@ function buildGraph(model) {
1679
1695
  const upKey = singular(parent.name);
1680
1696
  const downKey = plural(child.name);
1681
1697
  if (!(upKey in childNode)) {
1682
- childNode[upKey] = { from: child.name, key: upKey, kind: "one", target: parent.name };
1698
+ const fkNullable = fk.from.some((colName) => child.columns.find((c) => c.name === colName)?.nullable);
1699
+ childNode[upKey] = {
1700
+ from: child.name,
1701
+ key: upKey,
1702
+ kind: "one",
1703
+ target: parent.name,
1704
+ ...fkNullable && { nullable: true }
1705
+ };
1683
1706
  }
1684
1707
  if (!(downKey in parentNode)) {
1685
1708
  parentNode[downKey] = { from: parent.name, key: downKey, kind: "many", target: child.name };
@@ -1797,10 +1820,11 @@ function emitIncludeResolver(graph, useJsExtensions) {
1797
1820
  : Select${targetType}[]
1798
1821
  )`;
1799
1822
  } else {
1823
+ const nullSuffix = edge.nullable ? " | null" : "";
1800
1824
  out += `(
1801
1825
  TInclude[K] extends { include: infer U extends ${targetType}IncludeSpec }
1802
- ? ${targetType}WithIncludes<U>
1803
- : Select${targetType}
1826
+ ? ${targetType}WithIncludes<U>${nullSuffix}
1827
+ : Select${targetType}${nullSuffix}
1804
1828
  )`;
1805
1829
  }
1806
1830
  out += ` :${isLast ? `
@@ -1936,7 +1960,7 @@ export const Upsert${Type}Schema = z.object({
1936
1960
 
1937
1961
  // src/emit-params-zod.ts
1938
1962
  init_utils();
1939
- function emitParamsZod(table, graph) {
1963
+ function emitParamsZod(table, graph, opts) {
1940
1964
  const Type = pascal(table.name);
1941
1965
  const columnNames = table.columns.map((c) => `"${c.name}"`).join(", ");
1942
1966
  const pkCols = Array.isArray(table.pk) ? table.pk : table.pk ? [table.pk] : [];
@@ -1953,7 +1977,7 @@ export const ${Type}PkSchema = ${pkSchema};
1953
1977
  // Schema for list query parameters
1954
1978
  export const ${Type}ListParamsSchema = z.object({
1955
1979
  include: ${includeSpecSchema}.optional(),
1956
- limit: z.number().int().positive().max(1000).optional(),
1980
+ limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
1957
1981
  offset: z.number().int().nonnegative().optional(),
1958
1982
  where: z.any().optional(),
1959
1983
  vector: VectorSearchParamsSchema.optional(),
@@ -1975,12 +1999,12 @@ export type ${Type}OrderParams = z.infer<typeof ${Type}OrderParamsSchema>;
1975
1999
  }
1976
2000
 
1977
2001
  // src/emit-shared-params-zod.ts
1978
- function emitSharedParamsZod() {
2002
+ function emitSharedParamsZod(opts) {
1979
2003
  return `import { z } from "zod";
1980
2004
 
1981
2005
  // Shared pagination schema (used across all tables)
1982
2006
  export const PaginationParamsSchema = z.object({
1983
- limit: z.number().int().positive().max(1000).optional(),
2007
+ limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
1984
2008
  offset: z.number().int().nonnegative().optional()
1985
2009
  }).strict();
1986
2010
 
@@ -2012,8 +2036,8 @@ export interface PaginatedResponse<T> {
2012
2036
  data: T[];
2013
2037
  /** Total number of records matching the query (across all pages) */
2014
2038
  total: number;
2015
- /** Maximum number of records per page */
2016
- limit: number;
2039
+ /** Maximum number of records per page (absent when no limit was specified) */
2040
+ limit?: number;
2017
2041
  /** Number of records skipped (for pagination) */
2018
2042
  offset: number;
2019
2043
  /** Whether there are more records available after this page */
@@ -2088,7 +2112,7 @@ const listSchema = z.object({
2088
2112
  include: z.any().optional(),
2089
2113
  select: z.array(z.string()).min(1).optional(),
2090
2114
  exclude: z.array(z.string()).min(1).optional(),
2091
- limit: z.number().int().positive().max(1000).optional(),
2115
+ limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
2092
2116
  offset: z.number().int().min(0).optional(),
2093
2117
  orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
2094
2118
  order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional(),
@@ -3001,7 +3025,7 @@ ${hasJsonbColumns ? ` /**
3001
3025
  * @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
3002
3026
  * @param params.orderBy - Column(s) to sort by
3003
3027
  * @param params.order - Sort direction(s): "asc" or "desc"
3004
- * @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.
3005
3029
  * @param params.offset - Number of records to skip for pagination
3006
3030
  * @param params.include - Related records to include (return type automatically infers included relations)
3007
3031
  * @returns Paginated results with all fields (and included relations if specified)
@@ -3101,7 +3125,7 @@ ${hasJsonbColumns ? ` /**
3101
3125
  * @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
3102
3126
  * @param params.orderBy - Column(s) to sort by
3103
3127
  * @param params.order - Sort direction(s): "asc" or "desc"
3104
- * @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.
3105
3129
  * @param params.offset - Number of records to skip for pagination
3106
3130
  * @param params.include - Related records to include (return type automatically infers included relations)
3107
3131
  * @returns Paginated results with all fields (and included relations if specified)
@@ -5595,7 +5619,7 @@ export async function listRecords(
5595
5619
  }
5596
5620
  ): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
5597
5621
  try {
5598
- 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;
5599
5623
 
5600
5624
  // DISTINCT ON support
5601
5625
  const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
@@ -5731,10 +5755,22 @@ export async function listRecords(
5731
5755
  orderBySQL = buildOrderBySQL(userOrderCols, userDirs);
5732
5756
  }
5733
5757
 
5734
- // Add limit and offset params
5735
- const limitParam = \`$\${paramIndex}\`;
5736
- const offsetParam = \`$\${paramIndex + 1}\`;
5737
- 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
+ }
5738
5774
 
5739
5775
  // Get total count for pagination
5740
5776
  const countText = distinctCols
@@ -5750,9 +5786,9 @@ export async function listRecords(
5750
5786
  // Inner query: DISTINCT ON with only the distinctCols ORDER BY prefix (PG requirement).
5751
5787
  // Outer query: free ORDER BY from the user's full orderBy list, plus LIMIT/OFFSET.
5752
5788
  const innerQuery = \`SELECT DISTINCT ON (\${_distinctOnColsSQL}) \${baseColumns} FROM "\${ctx.table}" \${whereSQL} ORDER BY \${_distinctOnColsSQL}\`;
5753
- text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
5789
+ text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL} \${limitOffsetSQL}\`.trim();
5754
5790
  } else {
5755
- text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
5791
+ text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} \${limitOffsetSQL}\`.trim();
5756
5792
  }
5757
5793
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
5758
5794
 
@@ -5760,7 +5796,7 @@ export async function listRecords(
5760
5796
  const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
5761
5797
 
5762
5798
  // Calculate hasMore
5763
- const hasMore = offset + limit < total;
5799
+ const hasMore = limit !== undefined ? offset + limit < total : false;
5764
5800
 
5765
5801
  const metadata = {
5766
5802
  data: parsedRows,
@@ -6800,6 +6836,7 @@ async function generate(configPath, options) {
6800
6836
  clientDir = join2(originalClientDir, "sdk");
6801
6837
  }
6802
6838
  const serverFramework = cfg.serverFramework || "hono";
6839
+ const maxLimit = cfg.maxLimit ?? 1000;
6803
6840
  const generateTests = cfg.tests?.generate ?? false;
6804
6841
  const originalTestDir = cfg.tests?.output || "./api/tests";
6805
6842
  let testDir = originalTestDir;
@@ -6828,7 +6865,7 @@ async function generate(configPath, options) {
6828
6865
  files.push({ path: join2(clientDir, "include-spec.ts"), content: includeSpec });
6829
6866
  const includeResolver = emitIncludeResolver(graph, cfg.useJsExtensions);
6830
6867
  files.push({ path: join2(clientDir, "include-resolver.ts"), content: includeResolver });
6831
- files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
6868
+ files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod({ maxLimit }) });
6832
6869
  files.push({ path: join2(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
6833
6870
  files.push({ path: join2(clientDir, "base-client.ts"), content: emitBaseClient() });
6834
6871
  files.push({ path: join2(clientDir, "where-types.ts"), content: emitWhereTypes() });
@@ -6865,7 +6902,7 @@ async function generate(configPath, options) {
6865
6902
  const zodSrc = emitZod(table, { numericMode }, model.enums);
6866
6903
  files.push({ path: join2(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
6867
6904
  files.push({ path: join2(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
6868
- const paramsZodSrc = emitParamsZod(table, graph);
6905
+ const paramsZodSrc = emitParamsZod(table, graph, { maxLimit });
6869
6906
  files.push({ path: join2(clientDir, "params", `${table.name}.ts`), content: paramsZodSrc });
6870
6907
  let routeContent;
6871
6908
  if (serverFramework === "hono") {
@@ -6875,7 +6912,8 @@ async function generate(configPath, options) {
6875
6912
  includeMethodsDepth: cfg.includeMethodsDepth || 2,
6876
6913
  authStrategy: getAuthStrategy(normalizedAuth),
6877
6914
  useJsExtensions: cfg.useJsExtensions,
6878
- apiPathPrefix: cfg.apiPathPrefix || "/v1"
6915
+ apiPathPrefix: cfg.apiPathPrefix || "/v1",
6916
+ maxLimit
6879
6917
  });
6880
6918
  } else {
6881
6919
  throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
@@ -6891,7 +6929,8 @@ async function generate(configPath, options) {
6891
6929
  exposeHardDelete,
6892
6930
  useJsExtensions: cfg.useJsExtensionsClient,
6893
6931
  includeMethodsDepth: cfg.includeMethodsDepth ?? 2,
6894
- skipJunctionTables: cfg.skipJunctionTables ?? true
6932
+ skipJunctionTables: cfg.skipJunctionTables ?? true,
6933
+ maxLimit
6895
6934
  }, model)
6896
6935
  });
6897
6936
  }
@@ -5,6 +5,8 @@ export type Edge = {
5
5
  kind: "one" | "many";
6
6
  target: string;
7
7
  via?: string;
8
+ /** True when the FK column(s) are nullable (belongs-to may return null). */
9
+ nullable?: boolean;
8
10
  };
9
11
  export type Graph = Record<string, Record<string, Edge>>;
10
12
  export declare function buildGraph(model: Model): Graph;
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.2",
3
+ "version": "0.19.4",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "scripts": {
24
24
  "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",
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
26
  "test:write-files": "bun test/test-write-files-if-changed.ts",
27
27
  "test:init": "bun test/test-init.ts",
28
28
  "test:gen": "bun test/test-gen.ts",