postgresdk 0.16.6 → 0.16.8

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
@@ -163,6 +163,9 @@ export default {
163
163
  }
164
164
  },
165
165
 
166
+ // SDK endpoint protection (optional)
167
+ pullToken: "env:POSTGRESDK_PULL_TOKEN", // Protect /_psdk/* endpoints (if not set, public)
168
+
166
169
  // Test generation (optional)
167
170
  tests: {
168
171
  generate: true, // Generate test files
@@ -515,7 +518,7 @@ export default {
515
518
  pull: {
516
519
  from: "https://api.myapp.com",
517
520
  output: "./src/sdk",
518
- token: process.env.API_TOKEN // Optional auth for protected APIs
521
+ pullToken: "env:POSTGRESDK_PULL_TOKEN" // Optional: if server has pullToken set
519
522
  }
520
523
  };
521
524
  ```
package/dist/cli.js CHANGED
@@ -1805,6 +1805,15 @@ function extractConfigFields(configContent) {
1805
1805
  isCommented: pullBlock.isCommented
1806
1806
  });
1807
1807
  }
1808
+ const pullTokenMatch = configContent.match(/^\s*(\/\/)?\s*pullToken:\s*(.+),?$/m);
1809
+ if (pullTokenMatch) {
1810
+ fields.push({
1811
+ key: "pullToken",
1812
+ value: pullTokenMatch[2]?.trim().replace(/,$/, ""),
1813
+ description: "Token for protecting /_psdk/* endpoints",
1814
+ isCommented: !!pullTokenMatch[1]
1815
+ });
1816
+ }
1808
1817
  return fields;
1809
1818
  }
1810
1819
  function extractComplexBlock(configContent, blockName) {
@@ -1947,21 +1956,36 @@ export default {
1947
1956
  ${getComplexBlockLine("tests", existingFields, mergeStrategy, userChoices)}
1948
1957
 
1949
1958
  // ========== AUTHENTICATION ==========
1950
-
1959
+
1951
1960
  /**
1952
1961
  * Authentication configuration for your API
1953
- *
1962
+ *
1954
1963
  * Simple syntax examples:
1955
1964
  * auth: { apiKey: process.env.API_KEY }
1956
1965
  * auth: { jwt: process.env.JWT_SECRET }
1957
- *
1966
+ *
1958
1967
  * Multiple API keys:
1959
1968
  * auth: { apiKeys: [process.env.KEY1, process.env.KEY2] }
1960
- *
1969
+ *
1961
1970
  * Full syntax for advanced options:
1962
1971
  */
1963
1972
  ${getComplexBlockLine("auth", existingFields, mergeStrategy, userChoices)}
1964
-
1973
+
1974
+ // ========== SDK ENDPOINT PROTECTION ==========
1975
+
1976
+ /**
1977
+ * Token for protecting /_psdk/* endpoints (SDK distribution and contract endpoints)
1978
+ *
1979
+ * When set, clients must provide this token via Authorization header when pulling SDK.
1980
+ * If not set, /_psdk/* endpoints are publicly accessible.
1981
+ *
1982
+ * This is separate from the main auth strategy (JWT/API key) used for CRUD operations.
1983
+ *
1984
+ * Use "env:" prefix to read from environment variables:
1985
+ * pullToken: "env:POSTGRESDK_PULL_TOKEN"
1986
+ */
1987
+ ${getFieldLine("pullToken", existingFields, mergeStrategy, '"env:POSTGRESDK_PULL_TOKEN"', userChoices)}
1988
+
1965
1989
  // ========== SDK DISTRIBUTION (Pull Configuration) ==========
1966
1990
 
1967
1991
  /**
@@ -2050,9 +2074,9 @@ function getDefaultComplexBlock(key) {
2050
2074
  // },`;
2051
2075
  case "pull":
2052
2076
  return `// pull: {
2053
- // from: "https://api.myapp.com", // API URL to pull SDK from
2054
- // output: "./src/sdk", // Local directory for pulled SDK
2055
- // token: process.env.API_TOKEN, // Optional authentication token
2077
+ // from: "https://api.myapp.com", // API URL to pull SDK from
2078
+ // output: "./src/sdk", // Local directory for pulled SDK
2079
+ // pullToken: "env:POSTGRESDK_PULL_TOKEN", // Optional: if server has pullToken set
2056
2080
  // },`;
2057
2081
  default:
2058
2082
  return `// ${key}: {},`;
@@ -2154,6 +2178,7 @@ async function initCommand(args) {
2154
2178
  const newOptions = [
2155
2179
  { key: "tests", description: "Enable test generation" },
2156
2180
  { key: "auth", description: "Add authentication" },
2181
+ { key: "pullToken", description: "Add SDK endpoint protection" },
2157
2182
  { key: "pull", description: "Configure SDK distribution" }
2158
2183
  ];
2159
2184
  const existingKeys = new Set(existingFields.map((f) => f.key));
@@ -2242,13 +2267,14 @@ async function initCommand(args) {
2242
2267
  console.log(" DATABASE_URL=postgres://user:pass@localhost:5432/mydb");
2243
2268
  console.log(" API_KEY=your-secret-key");
2244
2269
  console.log(" JWT_SECRET=your-jwt-secret");
2270
+ console.log(" POSTGRESDK_PULL_TOKEN=your-pull-token");
2245
2271
  }
2246
2272
  console.log(" 3. Run 'postgresdk generate' to create your SDK");
2247
2273
  } else {
2248
2274
  console.log(" 1. Edit postgresdk.config.ts with your API URL in pull.from");
2249
2275
  if (!hasEnv) {
2250
2276
  console.log(" 2. Consider creating a .env file if you need authentication:");
2251
- console.log(" API_TOKEN=your-api-token");
2277
+ console.log(" POSTGRESDK_PULL_TOKEN=your-pull-token");
2252
2278
  }
2253
2279
  console.log(" 3. Run 'postgresdk pull' to fetch your SDK");
2254
2280
  }
@@ -2395,6 +2421,21 @@ export default {
2395
2421
  // audience: "my-api", // Optional: validate 'aud' claim
2396
2422
  // }
2397
2423
  // },
2424
+
2425
+ // ========== SDK ENDPOINT PROTECTION ==========
2426
+
2427
+ /**
2428
+ * Token for protecting /_psdk/* endpoints (SDK distribution and contract endpoints)
2429
+ *
2430
+ * When set, clients must provide this token via Authorization header when pulling SDK.
2431
+ * If not set, /_psdk/* endpoints are publicly accessible.
2432
+ *
2433
+ * This is separate from the main auth strategy (JWT/API key) used for CRUD operations.
2434
+ *
2435
+ * Use "env:" prefix to read from environment variables:
2436
+ * pullToken: "env:POSTGRESDK_PULL_TOKEN"
2437
+ */
2438
+ // pullToken: "env:POSTGRESDK_PULL_TOKEN",
2398
2439
  };
2399
2440
  `, CONFIG_TEMPLATE_SDK = `/**
2400
2441
  * PostgreSDK Configuration (SDK-Side)
@@ -2420,9 +2461,17 @@ export default {
2420
2461
  * Configuration for pulling SDK from a remote API
2421
2462
  */
2422
2463
  pull: {
2423
- from: "https://api.myapp.com", // API URL to pull SDK from
2424
- output: "./src/sdk", // Local directory for pulled SDK
2425
- // token: process.env.API_TOKEN, // Optional authentication token
2464
+ from: "https://api.myapp.com", // API URL to pull SDK from
2465
+ output: "./src/sdk", // Local directory for pulled SDK
2466
+
2467
+ /**
2468
+ * Authentication token for protected /_psdk/* endpoints
2469
+ * Should match the server's pullToken configuration
2470
+ *
2471
+ * Use "env:" prefix to read from environment variables:
2472
+ * pullToken: "env:POSTGRESDK_PULL_TOKEN"
2473
+ */
2474
+ // pullToken: "env:POSTGRESDK_PULL_TOKEN",
2426
2475
  },
2427
2476
  };
2428
2477
  `;
@@ -2463,7 +2512,7 @@ async function pullCommand(args) {
2463
2512
  const cliConfig = {
2464
2513
  from: args.find((a) => a.startsWith("--from="))?.split("=")[1],
2465
2514
  output: args.find((a) => a.startsWith("--output="))?.split("=")[1],
2466
- token: args.find((a) => a.startsWith("--token="))?.split("=")[1]
2515
+ pullToken: args.find((a) => a.startsWith("--pullToken="))?.split("=")[1]
2467
2516
  };
2468
2517
  const config = {
2469
2518
  output: "./src/sdk",
@@ -2479,13 +2528,29 @@ Options:`);
2479
2528
  console.error(" (then edit postgresdk.config.ts and run 'postgresdk pull')");
2480
2529
  process.exit(1);
2481
2530
  }
2531
+ let resolvedToken = config.pullToken;
2532
+ if (resolvedToken?.startsWith("env:")) {
2533
+ const envVarName = resolvedToken.slice(4);
2534
+ resolvedToken = process.env[envVarName];
2535
+ if (!resolvedToken) {
2536
+ console.error(`❌ Environment variable "${envVarName}" not set (referenced in pullToken config)`);
2537
+ process.exit(1);
2538
+ }
2539
+ }
2482
2540
  console.log(`\uD83D\uDD04 Pulling SDK from ${config.from}`);
2483
2541
  console.log(`\uD83D\uDCC1 Output directory: ${config.output}`);
2484
2542
  try {
2485
- const headers = config.token ? { Authorization: `Bearer ${config.token}` } : {};
2543
+ const headers = resolvedToken ? { Authorization: `Bearer ${resolvedToken}` } : {};
2486
2544
  const manifestRes = await fetch(`${config.from}/_psdk/sdk/manifest`, { headers });
2487
2545
  if (!manifestRes.ok) {
2488
- throw new Error(`Failed to fetch SDK manifest: ${manifestRes.status} ${manifestRes.statusText}`);
2546
+ let errorMsg = `${manifestRes.status} ${manifestRes.statusText}`;
2547
+ try {
2548
+ const errorBody = await manifestRes.json();
2549
+ if (errorBody.error) {
2550
+ errorMsg = errorBody.error;
2551
+ }
2552
+ } catch {}
2553
+ throw new Error(`Failed to fetch SDK manifest: ${errorMsg}`);
2489
2554
  }
2490
2555
  const manifest = await manifestRes.json();
2491
2556
  console.log(`\uD83D\uDCE6 SDK version: ${manifest.version}`);
@@ -2493,7 +2558,14 @@ Options:`);
2493
2558
  console.log(`\uD83D\uDCC4 Files: ${manifest.files.length}`);
2494
2559
  const sdkRes = await fetch(`${config.from}/_psdk/sdk/download`, { headers });
2495
2560
  if (!sdkRes.ok) {
2496
- throw new Error(`Failed to download SDK: ${sdkRes.status} ${sdkRes.statusText}`);
2561
+ let errorMsg = `${sdkRes.status} ${sdkRes.statusText}`;
2562
+ try {
2563
+ const errorBody = await sdkRes.json();
2564
+ if (errorBody.error) {
2565
+ errorMsg = errorBody.error;
2566
+ }
2567
+ } catch {}
2568
+ throw new Error(`Failed to download SDK: ${errorMsg}`);
2497
2569
  }
2498
2570
  const sdk = await sdkRes.json();
2499
2571
  for (const [path, content] of Object.entries(sdk.files)) {
@@ -2815,6 +2887,8 @@ function emitZod(table, opts, enums) {
2815
2887
  return `z.unknown()`;
2816
2888
  if (t === "date" || t.startsWith("timestamp"))
2817
2889
  return `z.string()`;
2890
+ if (t === "vector" || t === "halfvec" || t === "sparsevec" || t === "bit")
2891
+ return `z.array(z.number())`;
2818
2892
  if (t.startsWith("_"))
2819
2893
  return `z.array(${zFor(t.slice(1))})`;
2820
2894
  return `z.string()`;
@@ -2948,6 +3022,7 @@ function emitHonoRoutes(table, _graph, opts) {
2948
3022
  const fileTableName = table.name;
2949
3023
  const Type = pascal(table.name);
2950
3024
  const hasVectorColumns = table.columns.some((c) => isVectorType(c.pgType));
3025
+ const vectorColumns = table.columns.filter((c) => isVectorType(c.pgType)).map((c) => c.name);
2951
3026
  const rawPk = table.pk;
2952
3027
  const pkCols = Array.isArray(rawPk) ? rawPk : rawPk ? [rawPk] : [];
2953
3028
  const safePkCols = pkCols.length ? pkCols : ["id"];
@@ -3008,7 +3083,8 @@ export function register${Type}Routes(app: Hono, deps: { pg: { query: (text: str
3008
3083
  table: "${fileTableName}",
3009
3084
  pkColumns: ${JSON.stringify(safePkCols)},
3010
3085
  softDeleteColumn: ${softDel ? `"${softDel}"` : "null"},
3011
- includeMethodsDepth: ${opts.includeMethodsDepth}
3086
+ includeMethodsDepth: ${opts.includeMethodsDepth}${vectorColumns.length > 0 ? `,
3087
+ vectorColumns: ${JSON.stringify(vectorColumns)}` : ""}
3012
3088
  };
3013
3089
  ${hasAuth ? `
3014
3090
  // \uD83D\uDD10 Auth: protect all routes for this table
@@ -3990,6 +4066,8 @@ function tsTypeFor(pgType, opts, enums) {
3990
4066
  return "string";
3991
4067
  if (t === "json" || t === "jsonb")
3992
4068
  return "unknown";
4069
+ if (t === "vector" || t === "halfvec" || t === "sparsevec" || t === "bit")
4070
+ return "number[]";
3993
4071
  return "string";
3994
4072
  }
3995
4073
  function isJsonbType2(pgType) {
@@ -4438,9 +4516,20 @@ export async function authMiddleware(c: Context, next: Next) {
4438
4516
 
4439
4517
  // src/emit-router-hono.ts
4440
4518
  init_utils();
4441
- function emitHonoRouter(tables, hasAuth, useJsExtensions) {
4519
+ function emitHonoRouter(tables, hasAuth, useJsExtensions, pullToken) {
4442
4520
  const tableNames = tables.map((t) => t.name).sort();
4443
4521
  const ext = useJsExtensions ? ".js" : "";
4522
+ let resolvedPullToken;
4523
+ let pullTokenEnvVar;
4524
+ if (pullToken) {
4525
+ if (pullToken.startsWith("env:")) {
4526
+ const envVarName = pullToken.slice(4);
4527
+ resolvedPullToken = `process.env.${envVarName}`;
4528
+ pullTokenEnvVar = envVarName;
4529
+ } else {
4530
+ resolvedPullToken = JSON.stringify(pullToken);
4531
+ }
4532
+ }
4444
4533
  const imports = tableNames.map((name) => {
4445
4534
  const Type = pascal(name);
4446
4535
  return `import { register${Type}Routes } from "./routes/${name}${ext}";`;
@@ -4527,10 +4616,35 @@ export function createRouter(
4527
4616
  }
4528
4617
  ): Hono {
4529
4618
  const router = new Hono();
4530
-
4619
+
4531
4620
  // Register table routes
4532
4621
  ${registrations}
4622
+ ${pullToken ? `
4623
+ // \uD83D\uDD10 Protect /_psdk/* endpoints with pullToken
4624
+ router.use("/_psdk/*", async (c, next) => {
4625
+ const authHeader = c.req.header("Authorization");
4626
+ const expectedToken = ${resolvedPullToken};
4627
+
4628
+ if (!expectedToken) {
4629
+ // Token not configured in environment - reject request
4630
+ return c.json({
4631
+ error: "SDK endpoints are protected but pullToken environment variable not set. ${pullTokenEnvVar ? `Set ${pullTokenEnvVar} in your environment or remove pullToken from config.` : "Set the pullToken environment variable or remove pullToken from config."}"
4632
+ }, 500);
4633
+ }
4634
+
4635
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
4636
+ return c.json({ error: "Missing or invalid Authorization header" }, 401);
4637
+ }
4638
+
4639
+ const providedToken = authHeader.slice(7); // Remove "Bearer " prefix
4640
+
4641
+ if (providedToken !== expectedToken) {
4642
+ return c.json({ error: "Invalid pull token" }, 401);
4643
+ }
4533
4644
 
4645
+ await next();
4646
+ });
4647
+ ` : ""}
4534
4648
  // SDK distribution endpoints
4535
4649
  router.get("/_psdk/sdk/manifest", (c) => {
4536
4650
  return c.json({
@@ -4674,6 +4788,7 @@ export interface OperationContext {
4674
4788
  pkColumns: string[];
4675
4789
  softDeleteColumn?: string | null;
4676
4790
  includeMethodsDepth: number;
4791
+ vectorColumns?: string[];
4677
4792
  }
4678
4793
 
4679
4794
  const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
@@ -4698,6 +4813,30 @@ function prepareParams(params: any[]): any[] {
4698
4813
  });
4699
4814
  }
4700
4815
 
4816
+ /**
4817
+ * Parse vector columns in retrieved rows.
4818
+ * pgvector returns vectors as strings (e.g., "[1.5,2.5,3.5]") which need to be
4819
+ * parsed back to number[] to match TypeScript types.
4820
+ */
4821
+ function parseVectorColumns(rows: any[], vectorColumns?: string[]): any[] {
4822
+ if (!vectorColumns || vectorColumns.length === 0) return rows;
4823
+
4824
+ return rows.map(row => {
4825
+ const parsed = { ...row };
4826
+ for (const col of vectorColumns) {
4827
+ if (parsed[col] !== null && parsed[col] !== undefined && typeof parsed[col] === 'string') {
4828
+ try {
4829
+ parsed[col] = JSON.parse(parsed[col]);
4830
+ } catch (e) {
4831
+ // If parsing fails, leave as string (shouldn't happen with valid vectors)
4832
+ log.error(\`Failed to parse vector column "\${col}":, e\`);
4833
+ }
4834
+ }
4835
+ }
4836
+ return parsed;
4837
+ });
4838
+ }
4839
+
4701
4840
  /**
4702
4841
  * CREATE operation - Insert a new record
4703
4842
  */
@@ -4720,8 +4859,9 @@ export async function createRecord(
4720
4859
 
4721
4860
  log.debug("SQL:", text, "vals:", vals);
4722
4861
  const { rows } = await ctx.pg.query(text, prepareParams(vals));
4862
+ const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
4723
4863
 
4724
- return { data: rows[0] ?? null, status: rows[0] ? 201 : 500 };
4864
+ return { data: parsedRows[0] ?? null, status: parsedRows[0] ? 201 : 500 };
4725
4865
  } catch (e: any) {
4726
4866
  // Enhanced logging for JSON validation errors
4727
4867
  const errorMsg = e?.message ?? "";
@@ -4760,12 +4900,13 @@ export async function getByPk(
4760
4900
  log.debug(\`GET \${ctx.table} by PK:\`, pkValues, "SQL:", text);
4761
4901
 
4762
4902
  const { rows } = await ctx.pg.query(text, prepareParams(pkValues));
4763
-
4764
- if (!rows[0]) {
4903
+ const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
4904
+
4905
+ if (!parsedRows[0]) {
4765
4906
  return { data: null, status: 404 };
4766
4907
  }
4767
-
4768
- return { data: rows[0], status: 200 };
4908
+
4909
+ return { data: parsedRows[0], status: 200 };
4769
4910
  } catch (e: any) {
4770
4911
  log.error(\`GET \${ctx.table} error:\`, e?.stack ?? e);
4771
4912
  return {
@@ -5151,12 +5292,13 @@ export async function listRecords(
5151
5292
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
5152
5293
 
5153
5294
  const { rows } = await ctx.pg.query(text, prepareParams(allParams));
5295
+ const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
5154
5296
 
5155
5297
  // Calculate hasMore
5156
5298
  const hasMore = offset + limit < total;
5157
5299
 
5158
5300
  const metadata = {
5159
- data: rows,
5301
+ data: parsedRows,
5160
5302
  total,
5161
5303
  limit,
5162
5304
  offset,
@@ -5221,12 +5363,13 @@ export async function updateRecord(
5221
5363
 
5222
5364
  log.debug(\`PATCH \${ctx.table} SQL:\`, text, "params:", params);
5223
5365
  const { rows } = await ctx.pg.query(text, prepareParams(params));
5224
-
5225
- if (!rows[0]) {
5366
+ const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
5367
+
5368
+ if (!parsedRows[0]) {
5226
5369
  return { data: null, status: 404 };
5227
5370
  }
5228
-
5229
- return { data: rows[0], status: 200 };
5371
+
5372
+ return { data: parsedRows[0], status: 200 };
5230
5373
  } catch (e: any) {
5231
5374
  // Enhanced logging for JSON validation errors
5232
5375
  const errorMsg = e?.message ?? "";
@@ -5270,12 +5413,13 @@ export async function deleteRecord(
5270
5413
 
5271
5414
  log.debug(\`DELETE \${ctx.softDeleteColumn ? '(soft)' : ''} \${ctx.table} SQL:\`, text, "pk:", pkValues);
5272
5415
  const { rows } = await ctx.pg.query(text, prepareParams(pkValues));
5273
-
5274
- if (!rows[0]) {
5416
+ const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
5417
+
5418
+ if (!parsedRows[0]) {
5275
5419
  return { data: null, status: 404 };
5276
5420
  }
5277
-
5278
- return { data: rows[0], status: 200 };
5421
+
5422
+ return { data: parsedRows[0], status: 200 };
5279
5423
  } catch (e: any) {
5280
5424
  log.error(\`DELETE \${ctx.table} error:\`, e?.stack ?? e);
5281
5425
  return {
@@ -6145,7 +6289,7 @@ async function generate(configPath) {
6145
6289
  if (serverFramework === "hono") {
6146
6290
  files.push({
6147
6291
  path: join(serverDir, "router.ts"),
6148
- content: emitHonoRouter(Object.values(model.tables), getAuthStrategy(normalizedAuth) !== "none", cfg.useJsExtensions)
6292
+ content: emitHonoRouter(Object.values(model.tables), getAuthStrategy(normalizedAuth) !== "none", cfg.useJsExtensions, cfg.pullToken)
6149
6293
  });
6150
6294
  }
6151
6295
  const { generateUnifiedContract: generateUnifiedContract2, generateUnifiedContractMarkdown: generateUnifiedContractMarkdown2 } = await Promise.resolve().then(() => (init_emit_sdk_contract(), exports_emit_sdk_contract));
@@ -2,4 +2,4 @@ import type { Table } from "./introspect";
2
2
  /**
3
3
  * Emits the Hono server router file that exports helper functions for route registration
4
4
  */
5
- export declare function emitHonoRouter(tables: Table[], hasAuth: boolean, useJsExtensions?: boolean): string;
5
+ export declare function emitHonoRouter(tables: Table[], hasAuth: boolean, useJsExtensions?: boolean, pullToken?: string): string;
package/dist/index.js CHANGED
@@ -1986,6 +1986,8 @@ function emitZod(table, opts, enums) {
1986
1986
  return `z.unknown()`;
1987
1987
  if (t === "date" || t.startsWith("timestamp"))
1988
1988
  return `z.string()`;
1989
+ if (t === "vector" || t === "halfvec" || t === "sparsevec" || t === "bit")
1990
+ return `z.array(z.number())`;
1989
1991
  if (t.startsWith("_"))
1990
1992
  return `z.array(${zFor(t.slice(1))})`;
1991
1993
  return `z.string()`;
@@ -2119,6 +2121,7 @@ function emitHonoRoutes(table, _graph, opts) {
2119
2121
  const fileTableName = table.name;
2120
2122
  const Type = pascal(table.name);
2121
2123
  const hasVectorColumns = table.columns.some((c) => isVectorType(c.pgType));
2124
+ const vectorColumns = table.columns.filter((c) => isVectorType(c.pgType)).map((c) => c.name);
2122
2125
  const rawPk = table.pk;
2123
2126
  const pkCols = Array.isArray(rawPk) ? rawPk : rawPk ? [rawPk] : [];
2124
2127
  const safePkCols = pkCols.length ? pkCols : ["id"];
@@ -2179,7 +2182,8 @@ export function register${Type}Routes(app: Hono, deps: { pg: { query: (text: str
2179
2182
  table: "${fileTableName}",
2180
2183
  pkColumns: ${JSON.stringify(safePkCols)},
2181
2184
  softDeleteColumn: ${softDel ? `"${softDel}"` : "null"},
2182
- includeMethodsDepth: ${opts.includeMethodsDepth}
2185
+ includeMethodsDepth: ${opts.includeMethodsDepth}${vectorColumns.length > 0 ? `,
2186
+ vectorColumns: ${JSON.stringify(vectorColumns)}` : ""}
2183
2187
  };
2184
2188
  ${hasAuth ? `
2185
2189
  // \uD83D\uDD10 Auth: protect all routes for this table
@@ -3161,6 +3165,8 @@ function tsTypeFor(pgType, opts, enums) {
3161
3165
  return "string";
3162
3166
  if (t === "json" || t === "jsonb")
3163
3167
  return "unknown";
3168
+ if (t === "vector" || t === "halfvec" || t === "sparsevec" || t === "bit")
3169
+ return "number[]";
3164
3170
  return "string";
3165
3171
  }
3166
3172
  function isJsonbType2(pgType) {
@@ -3609,9 +3615,20 @@ export async function authMiddleware(c: Context, next: Next) {
3609
3615
 
3610
3616
  // src/emit-router-hono.ts
3611
3617
  init_utils();
3612
- function emitHonoRouter(tables, hasAuth, useJsExtensions) {
3618
+ function emitHonoRouter(tables, hasAuth, useJsExtensions, pullToken) {
3613
3619
  const tableNames = tables.map((t) => t.name).sort();
3614
3620
  const ext = useJsExtensions ? ".js" : "";
3621
+ let resolvedPullToken;
3622
+ let pullTokenEnvVar;
3623
+ if (pullToken) {
3624
+ if (pullToken.startsWith("env:")) {
3625
+ const envVarName = pullToken.slice(4);
3626
+ resolvedPullToken = `process.env.${envVarName}`;
3627
+ pullTokenEnvVar = envVarName;
3628
+ } else {
3629
+ resolvedPullToken = JSON.stringify(pullToken);
3630
+ }
3631
+ }
3615
3632
  const imports = tableNames.map((name) => {
3616
3633
  const Type = pascal(name);
3617
3634
  return `import { register${Type}Routes } from "./routes/${name}${ext}";`;
@@ -3698,10 +3715,35 @@ export function createRouter(
3698
3715
  }
3699
3716
  ): Hono {
3700
3717
  const router = new Hono();
3701
-
3718
+
3702
3719
  // Register table routes
3703
3720
  ${registrations}
3721
+ ${pullToken ? `
3722
+ // \uD83D\uDD10 Protect /_psdk/* endpoints with pullToken
3723
+ router.use("/_psdk/*", async (c, next) => {
3724
+ const authHeader = c.req.header("Authorization");
3725
+ const expectedToken = ${resolvedPullToken};
3726
+
3727
+ if (!expectedToken) {
3728
+ // Token not configured in environment - reject request
3729
+ return c.json({
3730
+ error: "SDK endpoints are protected but pullToken environment variable not set. ${pullTokenEnvVar ? `Set ${pullTokenEnvVar} in your environment or remove pullToken from config.` : "Set the pullToken environment variable or remove pullToken from config."}"
3731
+ }, 500);
3732
+ }
3733
+
3734
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
3735
+ return c.json({ error: "Missing or invalid Authorization header" }, 401);
3736
+ }
3704
3737
 
3738
+ const providedToken = authHeader.slice(7); // Remove "Bearer " prefix
3739
+
3740
+ if (providedToken !== expectedToken) {
3741
+ return c.json({ error: "Invalid pull token" }, 401);
3742
+ }
3743
+
3744
+ await next();
3745
+ });
3746
+ ` : ""}
3705
3747
  // SDK distribution endpoints
3706
3748
  router.get("/_psdk/sdk/manifest", (c) => {
3707
3749
  return c.json({
@@ -3845,6 +3887,7 @@ export interface OperationContext {
3845
3887
  pkColumns: string[];
3846
3888
  softDeleteColumn?: string | null;
3847
3889
  includeMethodsDepth: number;
3890
+ vectorColumns?: string[];
3848
3891
  }
3849
3892
 
3850
3893
  const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
@@ -3869,6 +3912,30 @@ function prepareParams(params: any[]): any[] {
3869
3912
  });
3870
3913
  }
3871
3914
 
3915
+ /**
3916
+ * Parse vector columns in retrieved rows.
3917
+ * pgvector returns vectors as strings (e.g., "[1.5,2.5,3.5]") which need to be
3918
+ * parsed back to number[] to match TypeScript types.
3919
+ */
3920
+ function parseVectorColumns(rows: any[], vectorColumns?: string[]): any[] {
3921
+ if (!vectorColumns || vectorColumns.length === 0) return rows;
3922
+
3923
+ return rows.map(row => {
3924
+ const parsed = { ...row };
3925
+ for (const col of vectorColumns) {
3926
+ if (parsed[col] !== null && parsed[col] !== undefined && typeof parsed[col] === 'string') {
3927
+ try {
3928
+ parsed[col] = JSON.parse(parsed[col]);
3929
+ } catch (e) {
3930
+ // If parsing fails, leave as string (shouldn't happen with valid vectors)
3931
+ log.error(\`Failed to parse vector column "\${col}":, e\`);
3932
+ }
3933
+ }
3934
+ }
3935
+ return parsed;
3936
+ });
3937
+ }
3938
+
3872
3939
  /**
3873
3940
  * CREATE operation - Insert a new record
3874
3941
  */
@@ -3891,8 +3958,9 @@ export async function createRecord(
3891
3958
 
3892
3959
  log.debug("SQL:", text, "vals:", vals);
3893
3960
  const { rows } = await ctx.pg.query(text, prepareParams(vals));
3961
+ const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
3894
3962
 
3895
- return { data: rows[0] ?? null, status: rows[0] ? 201 : 500 };
3963
+ return { data: parsedRows[0] ?? null, status: parsedRows[0] ? 201 : 500 };
3896
3964
  } catch (e: any) {
3897
3965
  // Enhanced logging for JSON validation errors
3898
3966
  const errorMsg = e?.message ?? "";
@@ -3931,12 +3999,13 @@ export async function getByPk(
3931
3999
  log.debug(\`GET \${ctx.table} by PK:\`, pkValues, "SQL:", text);
3932
4000
 
3933
4001
  const { rows } = await ctx.pg.query(text, prepareParams(pkValues));
3934
-
3935
- if (!rows[0]) {
4002
+ const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
4003
+
4004
+ if (!parsedRows[0]) {
3936
4005
  return { data: null, status: 404 };
3937
4006
  }
3938
-
3939
- return { data: rows[0], status: 200 };
4007
+
4008
+ return { data: parsedRows[0], status: 200 };
3940
4009
  } catch (e: any) {
3941
4010
  log.error(\`GET \${ctx.table} error:\`, e?.stack ?? e);
3942
4011
  return {
@@ -4322,12 +4391,13 @@ export async function listRecords(
4322
4391
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
4323
4392
 
4324
4393
  const { rows } = await ctx.pg.query(text, prepareParams(allParams));
4394
+ const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
4325
4395
 
4326
4396
  // Calculate hasMore
4327
4397
  const hasMore = offset + limit < total;
4328
4398
 
4329
4399
  const metadata = {
4330
- data: rows,
4400
+ data: parsedRows,
4331
4401
  total,
4332
4402
  limit,
4333
4403
  offset,
@@ -4392,12 +4462,13 @@ export async function updateRecord(
4392
4462
 
4393
4463
  log.debug(\`PATCH \${ctx.table} SQL:\`, text, "params:", params);
4394
4464
  const { rows } = await ctx.pg.query(text, prepareParams(params));
4395
-
4396
- if (!rows[0]) {
4465
+ const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
4466
+
4467
+ if (!parsedRows[0]) {
4397
4468
  return { data: null, status: 404 };
4398
4469
  }
4399
-
4400
- return { data: rows[0], status: 200 };
4470
+
4471
+ return { data: parsedRows[0], status: 200 };
4401
4472
  } catch (e: any) {
4402
4473
  // Enhanced logging for JSON validation errors
4403
4474
  const errorMsg = e?.message ?? "";
@@ -4441,12 +4512,13 @@ export async function deleteRecord(
4441
4512
 
4442
4513
  log.debug(\`DELETE \${ctx.softDeleteColumn ? '(soft)' : ''} \${ctx.table} SQL:\`, text, "pk:", pkValues);
4443
4514
  const { rows } = await ctx.pg.query(text, prepareParams(pkValues));
4444
-
4445
- if (!rows[0]) {
4515
+ const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
4516
+
4517
+ if (!parsedRows[0]) {
4446
4518
  return { data: null, status: 404 };
4447
4519
  }
4448
-
4449
- return { data: rows[0], status: 200 };
4520
+
4521
+ return { data: parsedRows[0], status: 200 };
4450
4522
  } catch (e: any) {
4451
4523
  log.error(\`DELETE \${ctx.table} error:\`, e?.stack ?? e);
4452
4524
  return {
@@ -5316,7 +5388,7 @@ async function generate(configPath) {
5316
5388
  if (serverFramework === "hono") {
5317
5389
  files.push({
5318
5390
  path: join(serverDir, "router.ts"),
5319
- content: emitHonoRouter(Object.values(model.tables), getAuthStrategy(normalizedAuth) !== "none", cfg.useJsExtensions)
5391
+ content: emitHonoRouter(Object.values(model.tables), getAuthStrategy(normalizedAuth) !== "none", cfg.useJsExtensions, cfg.pullToken)
5320
5392
  });
5321
5393
  }
5322
5394
  const { generateUnifiedContract: generateUnifiedContract2, generateUnifiedContractMarkdown: generateUnifiedContractMarkdown2 } = await Promise.resolve().then(() => (init_emit_sdk_contract(), exports_emit_sdk_contract));
package/dist/types.d.ts CHANGED
@@ -29,6 +29,7 @@ export interface Config {
29
29
  serverFramework?: "hono" | "express" | "fastify";
30
30
  apiPathPrefix?: string;
31
31
  auth?: AuthConfigInput;
32
+ pullToken?: string;
32
33
  pull?: PullConfig;
33
34
  useJsExtensions?: boolean;
34
35
  useJsExtensionsClient?: boolean;
@@ -41,6 +42,6 @@ export interface Config {
41
42
  export interface PullConfig {
42
43
  from: string;
43
44
  output?: string;
44
- token?: string;
45
+ pullToken?: string;
45
46
  }
46
47
  export declare function normalizeAuthConfig(input: AuthConfigInput | undefined): AuthConfig | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.16.6",
3
+ "version": "0.16.8",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {