postgresdk 0.16.12 → 0.16.14

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
@@ -146,6 +146,7 @@ export default {
146
146
  schema: "public", // Database schema to introspect
147
147
  outDir: "./api", // Output directory (or { client: "./sdk", server: "./api" })
148
148
  softDeleteColumn: null, // Column name for soft deletes (e.g., "deleted_at")
149
+ numericMode: "auto", // "auto" | "number" | "string" - How to type numeric columns
149
150
  includeMethodsDepth: 2, // Max depth for nested includes
150
151
  dateType: "date", // "date" | "string" - How to handle timestamps
151
152
  serverFramework: "hono", // Currently only hono is supported
@@ -175,6 +176,14 @@ export default {
175
176
  };
176
177
  ```
177
178
 
179
+ #### Type Mapping (numericMode)
180
+
181
+ Controls how PostgreSQL numeric types map to TypeScript:
182
+
183
+ - **`"auto"` (default)**: `int2`/`int4`/floats → `number`, `int8`/`numeric` → `string`
184
+ - **`"number"`**: All numeric → `number` (⚠️ unsafe for bigint - JS can't handle values > 2^53)
185
+ - **`"string"`**: All numeric → `string` (safe but requires parsing)
186
+
178
187
  ### Database Drivers
179
188
 
180
189
  The generated code works with any PostgreSQL client that implements a simple `query` interface:
package/dist/cli.js CHANGED
@@ -1751,6 +1751,15 @@ function extractConfigFields(configContent) {
1751
1751
  isCommented: !!depthMatch[1]
1752
1752
  });
1753
1753
  }
1754
+ const numericModeMatch = configContent.match(/^\s*(\/\/)?\s*numericMode:\s*"(.+)"/m);
1755
+ if (numericModeMatch) {
1756
+ fields.push({
1757
+ key: "numericMode",
1758
+ value: numericModeMatch[2],
1759
+ description: "How to type numeric columns in TypeScript",
1760
+ isCommented: !!numericModeMatch[1]
1761
+ });
1762
+ }
1754
1763
  const frameworkMatch = configContent.match(/^\s*(\/\/)?\s*serverFramework:\s*"(.+)"/m);
1755
1764
  if (frameworkMatch) {
1756
1765
  fields.push({
@@ -1918,7 +1927,16 @@ export default {
1918
1927
  * @example "deleted_at"
1919
1928
  */
1920
1929
  ${getFieldLine("softDeleteColumn", existingFields, mergeStrategy, "null", userChoices)}
1921
-
1930
+
1931
+ /**
1932
+ * How to type numeric columns in TypeScript
1933
+ * - "auto": int2/int4/float → number, int8/numeric → string (recommended)
1934
+ * - "number": All numeric types become TypeScript number (unsafe for bigint)
1935
+ * - "string": All numeric types become TypeScript string (legacy)
1936
+ * @default "auto"
1937
+ */
1938
+ ${getFieldLine("numericMode", existingFields, mergeStrategy, '"auto"', userChoices)}
1939
+
1922
1940
  /**
1923
1941
  * Maximum depth for nested relationship includes to prevent infinite loops
1924
1942
  * @default 2
@@ -2347,6 +2365,20 @@ export default {
2347
2365
  */
2348
2366
  // softDeleteColumn: null,
2349
2367
 
2368
+ /**
2369
+ * How to type numeric columns in TypeScript
2370
+ * Options:
2371
+ * - "auto": int2/int4/float → number, int8/numeric → string (recommended, default)
2372
+ * - "number": All numeric types become TypeScript number (unsafe for bigint)
2373
+ * - "string": All numeric types become TypeScript string (legacy behavior)
2374
+ *
2375
+ * Auto mode is safest - keeps JavaScript-safe integers as numbers,
2376
+ * but preserves precision for bigint/numeric by using strings.
2377
+ *
2378
+ * Default: "auto"
2379
+ */
2380
+ // numericMode: "auto",
2381
+
2350
2382
  /**
2351
2383
  * Maximum depth for nested relationship includes to prevent infinite loops
2352
2384
  * Default: 2
@@ -2879,10 +2911,20 @@ function emitZod(table, opts, enums) {
2879
2911
  return `z.string()`;
2880
2912
  if (t === "bool" || t === "boolean")
2881
2913
  return `z.boolean()`;
2882
- if (t === "int2" || t === "int4" || t === "int8")
2883
- return opts.numericMode === "number" ? `z.number()` : `z.string()`;
2884
- if (t === "numeric" || t === "float4" || t === "float8")
2885
- return opts.numericMode === "number" ? `z.number()` : `z.string()`;
2914
+ if (t === "int2" || t === "int4" || t === "int8") {
2915
+ if (opts.numericMode === "number")
2916
+ return `z.number()`;
2917
+ if (opts.numericMode === "string")
2918
+ return `z.string()`;
2919
+ return t === "int2" || t === "int4" ? `z.number()` : `z.string()`;
2920
+ }
2921
+ if (t === "numeric" || t === "float4" || t === "float8") {
2922
+ if (opts.numericMode === "number")
2923
+ return `z.number()`;
2924
+ if (opts.numericMode === "string")
2925
+ return `z.string()`;
2926
+ return t === "float4" || t === "float8" ? `z.number()` : `z.string()`;
2927
+ }
2886
2928
  if (t === "jsonb" || t === "json")
2887
2929
  return `z.unknown()`;
2888
2930
  if (t === "date" || t.startsWith("timestamp"))
@@ -3497,7 +3539,7 @@ ${hasJsonbColumns ? ` /**
3497
3539
  * const user = await client.create<{ metadata: Metadata }>({ name: 'Alice', metadata: { tags: [], prefs: { theme: 'light' } } });
3498
3540
  */
3499
3541
  async create<TJsonb extends Partial<Select${Type}> = {}>(
3500
- data: Insert${Type}<TJsonb>
3542
+ data: NoInfer<Insert${Type}<TJsonb>>
3501
3543
  ): Promise<Select${Type}<TJsonb>> {
3502
3544
  return this.post<Select${Type}<TJsonb>>(this.resource, data);
3503
3545
  }` : ` /**
@@ -3600,7 +3642,7 @@ ${hasJsonbColumns ? ` /**
3600
3642
  */
3601
3643
  async update<TJsonb extends Partial<Select${Type}> = {}>(
3602
3644
  pk: ${pkType},
3603
- patch: Update${Type}<TJsonb>
3645
+ patch: NoInfer<Update${Type}<TJsonb>>
3604
3646
  ): Promise<Select${Type}<TJsonb> | null> {
3605
3647
  const path = ${pkPathExpr};
3606
3648
  return this.patch<Select${Type}<TJsonb> | null>(\`\${this.resource}/\${path}\`, patch);
@@ -3812,18 +3854,18 @@ export abstract class BaseClient {
3812
3854
  /**
3813
3855
  * Make a POST request
3814
3856
  */
3815
- protected async post<T>(path: string, body?: any): Promise<T> {
3857
+ protected async post<T>(path: string, body?: unknown): Promise<T> {
3816
3858
  const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
3817
3859
  method: "POST",
3818
3860
  headers: await this.headers(true),
3819
3861
  body: JSON.stringify(body),
3820
3862
  });
3821
-
3863
+
3822
3864
  // Handle 404 specially for operations that might return null
3823
3865
  if (res.status === 404) {
3824
3866
  return null as T;
3825
3867
  }
3826
-
3868
+
3827
3869
  await this.okOrThrow(res, "POST", path);
3828
3870
  return (await res.json()) as T;
3829
3871
  }
@@ -3847,17 +3889,17 @@ export abstract class BaseClient {
3847
3889
  /**
3848
3890
  * Make a PATCH request
3849
3891
  */
3850
- protected async patch<T>(path: string, body?: any): Promise<T> {
3892
+ protected async patch<T>(path: string, body?: unknown): Promise<T> {
3851
3893
  const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
3852
3894
  method: "PATCH",
3853
3895
  headers: await this.headers(true),
3854
3896
  body: JSON.stringify(body),
3855
3897
  });
3856
-
3898
+
3857
3899
  if (res.status === 404) {
3858
3900
  return null as T;
3859
3901
  }
3860
-
3902
+
3861
3903
  await this.okOrThrow(res, "PATCH", path);
3862
3904
  return (await res.json()) as T;
3863
3905
  }
@@ -4346,7 +4388,11 @@ function tsTypeFor(pgType, opts, enums) {
4346
4388
  if (t === "bool" || t === "boolean")
4347
4389
  return "boolean";
4348
4390
  if (t === "int2" || t === "int4" || t === "int8" || t === "float4" || t === "float8" || t === "numeric") {
4349
- return opts.numericMode === "number" ? "number" : "string";
4391
+ if (opts.numericMode === "number")
4392
+ return "number";
4393
+ if (opts.numericMode === "string")
4394
+ return "string";
4395
+ return t === "int2" || t === "int4" || t === "float4" || t === "float8" ? "number" : "string";
4350
4396
  }
4351
4397
  if (t === "date" || t.startsWith("timestamp"))
4352
4398
  return "string";
@@ -6551,10 +6597,11 @@ async function generate(configPath) {
6551
6597
  console.log(`[Index] About to process ${Object.keys(model.tables || {}).length} tables for generation`);
6552
6598
  }
6553
6599
  for (const table of Object.values(model.tables)) {
6554
- const typesSrc = emitTypes(table, { numericMode: "string" }, model.enums);
6600
+ const numericMode = cfg.numericMode ?? "auto";
6601
+ const typesSrc = emitTypes(table, { numericMode }, model.enums);
6555
6602
  files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
6556
6603
  files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
6557
- const zodSrc = emitZod(table, { numericMode: "string" }, model.enums);
6604
+ const zodSrc = emitZod(table, { numericMode }, model.enums);
6558
6605
  files.push({ path: join(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
6559
6606
  files.push({ path: join(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
6560
6607
  const paramsZodSrc = emitParamsZod(table, graph);
@@ -1,4 +1,4 @@
1
1
  import type { Table } from "./introspect";
2
2
  export declare function emitTypes(table: Table, opts: {
3
- numericMode: "string" | "number";
3
+ numericMode: "string" | "number" | "auto";
4
4
  }, enums: Record<string, string[]>): string;
@@ -1,4 +1,4 @@
1
1
  import type { Table } from "./introspect";
2
2
  export declare function emitZod(table: Table, opts: {
3
- numericMode: "string" | "number";
3
+ numericMode: "string" | "number" | "auto";
4
4
  }, enums: Record<string, string[]>): string;
package/dist/index.js CHANGED
@@ -1978,10 +1978,20 @@ function emitZod(table, opts, enums) {
1978
1978
  return `z.string()`;
1979
1979
  if (t === "bool" || t === "boolean")
1980
1980
  return `z.boolean()`;
1981
- if (t === "int2" || t === "int4" || t === "int8")
1982
- return opts.numericMode === "number" ? `z.number()` : `z.string()`;
1983
- if (t === "numeric" || t === "float4" || t === "float8")
1984
- return opts.numericMode === "number" ? `z.number()` : `z.string()`;
1981
+ if (t === "int2" || t === "int4" || t === "int8") {
1982
+ if (opts.numericMode === "number")
1983
+ return `z.number()`;
1984
+ if (opts.numericMode === "string")
1985
+ return `z.string()`;
1986
+ return t === "int2" || t === "int4" ? `z.number()` : `z.string()`;
1987
+ }
1988
+ if (t === "numeric" || t === "float4" || t === "float8") {
1989
+ if (opts.numericMode === "number")
1990
+ return `z.number()`;
1991
+ if (opts.numericMode === "string")
1992
+ return `z.string()`;
1993
+ return t === "float4" || t === "float8" ? `z.number()` : `z.string()`;
1994
+ }
1985
1995
  if (t === "jsonb" || t === "json")
1986
1996
  return `z.unknown()`;
1987
1997
  if (t === "date" || t.startsWith("timestamp"))
@@ -2596,7 +2606,7 @@ ${hasJsonbColumns ? ` /**
2596
2606
  * const user = await client.create<{ metadata: Metadata }>({ name: 'Alice', metadata: { tags: [], prefs: { theme: 'light' } } });
2597
2607
  */
2598
2608
  async create<TJsonb extends Partial<Select${Type}> = {}>(
2599
- data: Insert${Type}<TJsonb>
2609
+ data: NoInfer<Insert${Type}<TJsonb>>
2600
2610
  ): Promise<Select${Type}<TJsonb>> {
2601
2611
  return this.post<Select${Type}<TJsonb>>(this.resource, data);
2602
2612
  }` : ` /**
@@ -2699,7 +2709,7 @@ ${hasJsonbColumns ? ` /**
2699
2709
  */
2700
2710
  async update<TJsonb extends Partial<Select${Type}> = {}>(
2701
2711
  pk: ${pkType},
2702
- patch: Update${Type}<TJsonb>
2712
+ patch: NoInfer<Update${Type}<TJsonb>>
2703
2713
  ): Promise<Select${Type}<TJsonb> | null> {
2704
2714
  const path = ${pkPathExpr};
2705
2715
  return this.patch<Select${Type}<TJsonb> | null>(\`\${this.resource}/\${path}\`, patch);
@@ -2911,18 +2921,18 @@ export abstract class BaseClient {
2911
2921
  /**
2912
2922
  * Make a POST request
2913
2923
  */
2914
- protected async post<T>(path: string, body?: any): Promise<T> {
2924
+ protected async post<T>(path: string, body?: unknown): Promise<T> {
2915
2925
  const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
2916
2926
  method: "POST",
2917
2927
  headers: await this.headers(true),
2918
2928
  body: JSON.stringify(body),
2919
2929
  });
2920
-
2930
+
2921
2931
  // Handle 404 specially for operations that might return null
2922
2932
  if (res.status === 404) {
2923
2933
  return null as T;
2924
2934
  }
2925
-
2935
+
2926
2936
  await this.okOrThrow(res, "POST", path);
2927
2937
  return (await res.json()) as T;
2928
2938
  }
@@ -2946,17 +2956,17 @@ export abstract class BaseClient {
2946
2956
  /**
2947
2957
  * Make a PATCH request
2948
2958
  */
2949
- protected async patch<T>(path: string, body?: any): Promise<T> {
2959
+ protected async patch<T>(path: string, body?: unknown): Promise<T> {
2950
2960
  const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
2951
2961
  method: "PATCH",
2952
2962
  headers: await this.headers(true),
2953
2963
  body: JSON.stringify(body),
2954
2964
  });
2955
-
2965
+
2956
2966
  if (res.status === 404) {
2957
2967
  return null as T;
2958
2968
  }
2959
-
2969
+
2960
2970
  await this.okOrThrow(res, "PATCH", path);
2961
2971
  return (await res.json()) as T;
2962
2972
  }
@@ -3445,7 +3455,11 @@ function tsTypeFor(pgType, opts, enums) {
3445
3455
  if (t === "bool" || t === "boolean")
3446
3456
  return "boolean";
3447
3457
  if (t === "int2" || t === "int4" || t === "int8" || t === "float4" || t === "float8" || t === "numeric") {
3448
- return opts.numericMode === "number" ? "number" : "string";
3458
+ if (opts.numericMode === "number")
3459
+ return "number";
3460
+ if (opts.numericMode === "string")
3461
+ return "string";
3462
+ return t === "int2" || t === "int4" || t === "float4" || t === "float8" ? "number" : "string";
3449
3463
  }
3450
3464
  if (t === "date" || t.startsWith("timestamp"))
3451
3465
  return "string";
@@ -5650,10 +5664,11 @@ async function generate(configPath) {
5650
5664
  console.log(`[Index] About to process ${Object.keys(model.tables || {}).length} tables for generation`);
5651
5665
  }
5652
5666
  for (const table of Object.values(model.tables)) {
5653
- const typesSrc = emitTypes(table, { numericMode: "string" }, model.enums);
5667
+ const numericMode = cfg.numericMode ?? "auto";
5668
+ const typesSrc = emitTypes(table, { numericMode }, model.enums);
5654
5669
  files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
5655
5670
  files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
5656
- const zodSrc = emitZod(table, { numericMode: "string" }, model.enums);
5671
+ const zodSrc = emitZod(table, { numericMode }, model.enums);
5657
5672
  files.push({ path: join(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
5658
5673
  files.push({ path: join(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
5659
5674
  const paramsZodSrc = emitParamsZod(table, graph);
package/dist/types.d.ts CHANGED
@@ -24,6 +24,7 @@ export interface Config {
24
24
  };
25
25
  softDeleteColumn?: string | null;
26
26
  dateType?: "date" | "string";
27
+ numericMode?: "string" | "number" | "auto";
27
28
  includeMethodsDepth?: number;
28
29
  skipJunctionTables?: boolean;
29
30
  serverFramework?: "hono" | "express" | "fastify";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.16.12",
3
+ "version": "0.16.14",
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: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",
25
+ "test": "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",
26
26
  "test:init": "bun test/test-init.ts",
27
27
  "test:gen": "bun test/test-gen.ts",
28
28
  "test:gen-with-tests": "bun test/test-gen-with-tests.ts",