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 +4 -1
- package/dist/cli.js +178 -34
- package/dist/emit-router-hono.d.ts +1 -1
- package/dist/index.js +90 -18
- package/dist/types.d.ts +2 -1
- package/package.json +1 -1
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
|
-
|
|
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",
|
|
2054
|
-
// output: "./src/sdk",
|
|
2055
|
-
//
|
|
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("
|
|
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",
|
|
2424
|
-
output: "./src/sdk",
|
|
2425
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
45
|
+
pullToken?: string;
|
|
45
46
|
}
|
|
46
47
|
export declare function normalizeAuthConfig(input: AuthConfigInput | undefined): AuthConfig | undefined;
|