postgresdk 0.19.3 → 0.19.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/dist/cli-install-skill.d.ts +5 -0
- package/dist/cli.js +89 -28
- package/dist/emit-client.d.ts +1 -0
- package/dist/emit-params-zod.d.ts +3 -1
- package/dist/emit-routes-hono.d.ts +1 -0
- package/dist/emit-routes.d.ts +1 -0
- package/dist/emit-shared-params-zod.d.ts +3 -1
- package/dist/index.js +37 -22
- package/dist/types.d.ts +2 -0
- package/package.json +3 -2
- package/skills/postgresdk/SKILL.md +394 -0
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:
|
|
723
|
+
// Note: Omitting `limit` returns all matching records. Max limit is controlled by `maxLimit` config (default: 1000).
|
|
723
724
|
|
|
724
|
-
// Calculate pagination info
|
|
725
|
-
const totalPages = Math.ceil(result.total / result.limit);
|
|
726
|
-
const currentPage = Math.floor(result.offset / result.limit) + 1;
|
|
725
|
+
// Calculate pagination info (when using explicit limit)
|
|
726
|
+
const totalPages = result.limit ? Math.ceil(result.total / result.limit) : 1;
|
|
727
|
+
const currentPage = result.limit ? Math.floor(result.offset / result.limit) + 1 : 1;
|
|
727
728
|
|
|
728
729
|
// Multi-column sorting
|
|
729
730
|
const sorted = await sdk.users.list({
|
|
@@ -1062,12 +1063,13 @@ Commands:
|
|
|
1062
1063
|
init Create a postgresdk.config.ts file
|
|
1063
1064
|
generate Generate SDK from database
|
|
1064
1065
|
pull Pull SDK from API endpoint
|
|
1066
|
+
install-skill Install Claude Code skill for PostgreSDK
|
|
1065
1067
|
version Show version
|
|
1066
1068
|
help Show help
|
|
1067
1069
|
|
|
1068
1070
|
Options:
|
|
1069
1071
|
-c, --config <path> Path to config file (default: postgresdk.config.ts)
|
|
1070
|
-
--force, -y Delete stale files without prompting (generate & pull)
|
|
1072
|
+
--force, -y Delete stale files without prompting (generate & pull); overwrite existing skill (install-skill)
|
|
1071
1073
|
|
|
1072
1074
|
Init subcommands/flags:
|
|
1073
1075
|
init pull Generate pull-only config (alias for --sdk)
|
|
@@ -1083,6 +1085,7 @@ Examples:
|
|
|
1083
1085
|
npx postgresdk@latest generate --force # Skip stale file prompts
|
|
1084
1086
|
npx postgresdk@latest pull --from=https://api.com --output=./src/sdk
|
|
1085
1087
|
npx postgresdk@latest pull --from=https://api.com --output=./src/sdk --force
|
|
1088
|
+
npx postgresdk@latest install-skill # Install Claude Code skill
|
|
1086
1089
|
```
|
|
1087
1090
|
|
|
1088
1091
|
### Generated Tests
|
package/dist/cli.js
CHANGED
|
@@ -1221,7 +1221,7 @@ function generateExampleValue(column) {
|
|
|
1221
1221
|
}
|
|
1222
1222
|
function generateQueryParams(table, enums) {
|
|
1223
1223
|
const params = {
|
|
1224
|
-
limit: "number - Max records to return (
|
|
1224
|
+
limit: "number - Max records to return (omit for all)",
|
|
1225
1225
|
offset: "number - Records to skip",
|
|
1226
1226
|
orderBy: "string | string[] - Field(s) to sort by",
|
|
1227
1227
|
order: "'asc' | 'desc' | ('asc' | 'desc')[] - Sort direction(s)"
|
|
@@ -2483,6 +2483,44 @@ var init_cli_pull = __esm(() => {
|
|
|
2483
2483
|
init_cli_utils();
|
|
2484
2484
|
});
|
|
2485
2485
|
|
|
2486
|
+
// src/cli-install-skill.ts
|
|
2487
|
+
var exports_cli_install_skill = {};
|
|
2488
|
+
__export(exports_cli_install_skill, {
|
|
2489
|
+
installSkillCommand: () => installSkillCommand
|
|
2490
|
+
});
|
|
2491
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync5 } from "node:fs";
|
|
2492
|
+
import { join as join4, dirname as dirname4 } from "node:path";
|
|
2493
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2494
|
+
async function installSkillCommand(args) {
|
|
2495
|
+
const force = parseForceFlag(args);
|
|
2496
|
+
const destDir = join4(process.cwd(), ".claude", "skills", "postgresdk");
|
|
2497
|
+
const destPath = join4(destDir, "SKILL.md");
|
|
2498
|
+
if (existsSync5(destPath) && !force) {
|
|
2499
|
+
console.log("⚠️ Skill already exists at .claude/skills/postgresdk/SKILL.md");
|
|
2500
|
+
console.log(" Use --force to overwrite.");
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
const srcPath = join4(__dirname3, "..", "skills", "postgresdk", "SKILL.md");
|
|
2504
|
+
if (!existsSync5(srcPath)) {
|
|
2505
|
+
console.error("❌ Could not find bundled skill file. This is a bug — please report it.");
|
|
2506
|
+
process.exit(1);
|
|
2507
|
+
}
|
|
2508
|
+
const content = readFileSync3(srcPath, "utf-8");
|
|
2509
|
+
mkdirSync(destDir, { recursive: true });
|
|
2510
|
+
writeFileSync2(destPath, content, "utf-8");
|
|
2511
|
+
console.log("✅ Installed PostgreSDK skill to .claude/skills/postgresdk/SKILL.md");
|
|
2512
|
+
console.log("");
|
|
2513
|
+
console.log(" Claude Code will now use this skill when you ask about your");
|
|
2514
|
+
console.log(" generated API or SDK. Try asking it to help with queries,");
|
|
2515
|
+
console.log(" filtering, includes, auth setup, or transactions.");
|
|
2516
|
+
}
|
|
2517
|
+
var __filename3, __dirname3;
|
|
2518
|
+
var init_cli_install_skill = __esm(() => {
|
|
2519
|
+
init_cli_utils();
|
|
2520
|
+
__filename3 = fileURLToPath2(import.meta.url);
|
|
2521
|
+
__dirname3 = dirname4(__filename3);
|
|
2522
|
+
});
|
|
2523
|
+
|
|
2486
2524
|
// src/index.ts
|
|
2487
2525
|
var import_config = __toESM(require_config(), 1);
|
|
2488
2526
|
import { join as join2, relative, dirname as dirname2 } from "node:path";
|
|
@@ -2920,7 +2958,7 @@ export const Upsert${Type}Schema = z.object({
|
|
|
2920
2958
|
|
|
2921
2959
|
// src/emit-params-zod.ts
|
|
2922
2960
|
init_utils();
|
|
2923
|
-
function emitParamsZod(table, graph) {
|
|
2961
|
+
function emitParamsZod(table, graph, opts) {
|
|
2924
2962
|
const Type = pascal(table.name);
|
|
2925
2963
|
const columnNames = table.columns.map((c) => `"${c.name}"`).join(", ");
|
|
2926
2964
|
const pkCols = Array.isArray(table.pk) ? table.pk : table.pk ? [table.pk] : [];
|
|
@@ -2937,7 +2975,7 @@ export const ${Type}PkSchema = ${pkSchema};
|
|
|
2937
2975
|
// Schema for list query parameters
|
|
2938
2976
|
export const ${Type}ListParamsSchema = z.object({
|
|
2939
2977
|
include: ${includeSpecSchema}.optional(),
|
|
2940
|
-
limit: z.number().int().positive().max(
|
|
2978
|
+
limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
|
|
2941
2979
|
offset: z.number().int().nonnegative().optional(),
|
|
2942
2980
|
where: z.any().optional(),
|
|
2943
2981
|
vector: VectorSearchParamsSchema.optional(),
|
|
@@ -2959,12 +2997,12 @@ export type ${Type}OrderParams = z.infer<typeof ${Type}OrderParamsSchema>;
|
|
|
2959
2997
|
}
|
|
2960
2998
|
|
|
2961
2999
|
// src/emit-shared-params-zod.ts
|
|
2962
|
-
function emitSharedParamsZod() {
|
|
3000
|
+
function emitSharedParamsZod(opts) {
|
|
2963
3001
|
return `import { z } from "zod";
|
|
2964
3002
|
|
|
2965
3003
|
// Shared pagination schema (used across all tables)
|
|
2966
3004
|
export const PaginationParamsSchema = z.object({
|
|
2967
|
-
limit: z.number().int().positive().max(
|
|
3005
|
+
limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
|
|
2968
3006
|
offset: z.number().int().nonnegative().optional()
|
|
2969
3007
|
}).strict();
|
|
2970
3008
|
|
|
@@ -2996,8 +3034,8 @@ export interface PaginatedResponse<T> {
|
|
|
2996
3034
|
data: T[];
|
|
2997
3035
|
/** Total number of records matching the query (across all pages) */
|
|
2998
3036
|
total: number;
|
|
2999
|
-
/** Maximum number of records per page */
|
|
3000
|
-
limit
|
|
3037
|
+
/** Maximum number of records per page (absent when no limit was specified) */
|
|
3038
|
+
limit?: number;
|
|
3001
3039
|
/** Number of records skipped (for pagination) */
|
|
3002
3040
|
offset: number;
|
|
3003
3041
|
/** Whether there are more records available after this page */
|
|
@@ -3072,7 +3110,7 @@ const listSchema = z.object({
|
|
|
3072
3110
|
include: z.any().optional(),
|
|
3073
3111
|
select: z.array(z.string()).min(1).optional(),
|
|
3074
3112
|
exclude: z.array(z.string()).min(1).optional(),
|
|
3075
|
-
limit: z.number().int().positive().max(
|
|
3113
|
+
limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
|
|
3076
3114
|
offset: z.number().int().min(0).optional(),
|
|
3077
3115
|
orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
|
|
3078
3116
|
order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional(),
|
|
@@ -3985,7 +4023,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
3985
4023
|
* @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
|
|
3986
4024
|
* @param params.orderBy - Column(s) to sort by
|
|
3987
4025
|
* @param params.order - Sort direction(s): "asc" or "desc"
|
|
3988
|
-
* @param params.limit - Maximum number of records to return (
|
|
4026
|
+
* @param params.limit - Maximum number of records to return${opts.maxLimit > 0 ? ` (max: ${opts.maxLimit})` : ""}. Omit to return all matching records.
|
|
3989
4027
|
* @param params.offset - Number of records to skip for pagination
|
|
3990
4028
|
* @param params.include - Related records to include (return type automatically infers included relations)
|
|
3991
4029
|
* @returns Paginated results with all fields (and included relations if specified)
|
|
@@ -4085,7 +4123,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
4085
4123
|
* @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
|
|
4086
4124
|
* @param params.orderBy - Column(s) to sort by
|
|
4087
4125
|
* @param params.order - Sort direction(s): "asc" or "desc"
|
|
4088
|
-
* @param params.limit - Maximum number of records to return (
|
|
4126
|
+
* @param params.limit - Maximum number of records to return${opts.maxLimit > 0 ? ` (max: ${opts.maxLimit})` : ""}. Omit to return all matching records.
|
|
4089
4127
|
* @param params.offset - Number of records to skip for pagination
|
|
4090
4128
|
* @param params.include - Related records to include (return type automatically infers included relations)
|
|
4091
4129
|
* @returns Paginated results with all fields (and included relations if specified)
|
|
@@ -6579,7 +6617,7 @@ export async function listRecords(
|
|
|
6579
6617
|
}
|
|
6580
6618
|
): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
6581
6619
|
try {
|
|
6582
|
-
const { where: whereClause, limit
|
|
6620
|
+
const { where: whereClause, limit, offset = 0, include, orderBy, order, vector, trigram, distinctOn, includeSoftDeleted } = params;
|
|
6583
6621
|
|
|
6584
6622
|
// DISTINCT ON support
|
|
6585
6623
|
const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
|
|
@@ -6715,10 +6753,22 @@ export async function listRecords(
|
|
|
6715
6753
|
orderBySQL = buildOrderBySQL(userOrderCols, userDirs);
|
|
6716
6754
|
}
|
|
6717
6755
|
|
|
6718
|
-
// Add limit and offset params
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6756
|
+
// Add limit and offset params (conditional — omit LIMIT clause when limit is undefined)
|
|
6757
|
+
let limitOffsetSQL: string;
|
|
6758
|
+
let allParams: any[];
|
|
6759
|
+
if (limit !== undefined) {
|
|
6760
|
+
const limitParam = \`$\${paramIndex}\`;
|
|
6761
|
+
const offsetParam = \`$\${paramIndex + 1}\`;
|
|
6762
|
+
limitOffsetSQL = \`LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
6763
|
+
allParams = [...queryParams, ...whereParams, limit, offset];
|
|
6764
|
+
} else if (offset > 0) {
|
|
6765
|
+
const offsetParam = \`$\${paramIndex}\`;
|
|
6766
|
+
limitOffsetSQL = \`OFFSET \${offsetParam}\`;
|
|
6767
|
+
allParams = [...queryParams, ...whereParams, offset];
|
|
6768
|
+
} else {
|
|
6769
|
+
limitOffsetSQL = '';
|
|
6770
|
+
allParams = [...queryParams, ...whereParams];
|
|
6771
|
+
}
|
|
6722
6772
|
|
|
6723
6773
|
// Get total count for pagination
|
|
6724
6774
|
const countText = distinctCols
|
|
@@ -6734,9 +6784,9 @@ export async function listRecords(
|
|
|
6734
6784
|
// Inner query: DISTINCT ON with only the distinctCols ORDER BY prefix (PG requirement).
|
|
6735
6785
|
// Outer query: free ORDER BY from the user's full orderBy list, plus LIMIT/OFFSET.
|
|
6736
6786
|
const innerQuery = \`SELECT DISTINCT ON (\${_distinctOnColsSQL}) \${baseColumns} FROM "\${ctx.table}" \${whereSQL} ORDER BY \${_distinctOnColsSQL}\`;
|
|
6737
|
-
text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL}
|
|
6787
|
+
text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL} \${limitOffsetSQL}\`.trim();
|
|
6738
6788
|
} else {
|
|
6739
|
-
text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL}
|
|
6789
|
+
text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} \${limitOffsetSQL}\`.trim();
|
|
6740
6790
|
}
|
|
6741
6791
|
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
|
|
6742
6792
|
|
|
@@ -6744,7 +6794,7 @@ export async function listRecords(
|
|
|
6744
6794
|
const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
|
|
6745
6795
|
|
|
6746
6796
|
// Calculate hasMore
|
|
6747
|
-
const hasMore = offset + limit < total;
|
|
6797
|
+
const hasMore = limit !== undefined ? offset + limit < total : false;
|
|
6748
6798
|
|
|
6749
6799
|
const metadata = {
|
|
6750
6800
|
data: parsedRows,
|
|
@@ -7784,6 +7834,7 @@ async function generate(configPath, options) {
|
|
|
7784
7834
|
clientDir = join2(originalClientDir, "sdk");
|
|
7785
7835
|
}
|
|
7786
7836
|
const serverFramework = cfg.serverFramework || "hono";
|
|
7837
|
+
const maxLimit = cfg.maxLimit ?? 1000;
|
|
7787
7838
|
const generateTests = cfg.tests?.generate ?? false;
|
|
7788
7839
|
const originalTestDir = cfg.tests?.output || "./api/tests";
|
|
7789
7840
|
let testDir = originalTestDir;
|
|
@@ -7812,7 +7863,7 @@ async function generate(configPath, options) {
|
|
|
7812
7863
|
files.push({ path: join2(clientDir, "include-spec.ts"), content: includeSpec });
|
|
7813
7864
|
const includeResolver = emitIncludeResolver(graph, cfg.useJsExtensions);
|
|
7814
7865
|
files.push({ path: join2(clientDir, "include-resolver.ts"), content: includeResolver });
|
|
7815
|
-
files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
|
|
7866
|
+
files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod({ maxLimit }) });
|
|
7816
7867
|
files.push({ path: join2(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
|
|
7817
7868
|
files.push({ path: join2(clientDir, "base-client.ts"), content: emitBaseClient() });
|
|
7818
7869
|
files.push({ path: join2(clientDir, "where-types.ts"), content: emitWhereTypes() });
|
|
@@ -7849,7 +7900,7 @@ async function generate(configPath, options) {
|
|
|
7849
7900
|
const zodSrc = emitZod(table, { numericMode }, model.enums);
|
|
7850
7901
|
files.push({ path: join2(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
7851
7902
|
files.push({ path: join2(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
7852
|
-
const paramsZodSrc = emitParamsZod(table, graph);
|
|
7903
|
+
const paramsZodSrc = emitParamsZod(table, graph, { maxLimit });
|
|
7853
7904
|
files.push({ path: join2(clientDir, "params", `${table.name}.ts`), content: paramsZodSrc });
|
|
7854
7905
|
let routeContent;
|
|
7855
7906
|
if (serverFramework === "hono") {
|
|
@@ -7859,7 +7910,8 @@ async function generate(configPath, options) {
|
|
|
7859
7910
|
includeMethodsDepth: cfg.includeMethodsDepth || 2,
|
|
7860
7911
|
authStrategy: getAuthStrategy(normalizedAuth),
|
|
7861
7912
|
useJsExtensions: cfg.useJsExtensions,
|
|
7862
|
-
apiPathPrefix: cfg.apiPathPrefix || "/v1"
|
|
7913
|
+
apiPathPrefix: cfg.apiPathPrefix || "/v1",
|
|
7914
|
+
maxLimit
|
|
7863
7915
|
});
|
|
7864
7916
|
} else {
|
|
7865
7917
|
throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
|
|
@@ -7875,7 +7927,8 @@ async function generate(configPath, options) {
|
|
|
7875
7927
|
exposeHardDelete,
|
|
7876
7928
|
useJsExtensions: cfg.useJsExtensionsClient,
|
|
7877
7929
|
includeMethodsDepth: cfg.includeMethodsDepth ?? 2,
|
|
7878
|
-
skipJunctionTables: cfg.skipJunctionTables ?? true
|
|
7930
|
+
skipJunctionTables: cfg.skipJunctionTables ?? true,
|
|
7931
|
+
maxLimit
|
|
7879
7932
|
}, model)
|
|
7880
7933
|
});
|
|
7881
7934
|
}
|
|
@@ -8020,13 +8073,13 @@ async function generate(configPath, options) {
|
|
|
8020
8073
|
// src/cli.ts
|
|
8021
8074
|
var import_config2 = __toESM(require_config(), 1);
|
|
8022
8075
|
import { resolve as resolve3 } from "node:path";
|
|
8023
|
-
import { readFileSync as
|
|
8024
|
-
import { fileURLToPath as
|
|
8025
|
-
import { dirname as
|
|
8076
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
8077
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
8078
|
+
import { dirname as dirname5, join as join5 } from "node:path";
|
|
8026
8079
|
init_cli_utils();
|
|
8027
|
-
var
|
|
8028
|
-
var
|
|
8029
|
-
var packageJson = JSON.parse(
|
|
8080
|
+
var __filename4 = fileURLToPath3(import.meta.url);
|
|
8081
|
+
var __dirname4 = dirname5(__filename4);
|
|
8082
|
+
var packageJson = JSON.parse(readFileSync4(join5(__dirname4, "../package.json"), "utf-8"));
|
|
8030
8083
|
var VERSION = packageJson.version;
|
|
8031
8084
|
var args = process.argv.slice(2);
|
|
8032
8085
|
var command = args[0];
|
|
@@ -8045,6 +8098,7 @@ Commands:
|
|
|
8045
8098
|
init Create a postgresdk.config.ts file
|
|
8046
8099
|
generate, gen Generate SDK from database
|
|
8047
8100
|
pull Pull SDK from API endpoint
|
|
8101
|
+
install-skill Install Claude Code skill for PostgreSDK
|
|
8048
8102
|
version Show version
|
|
8049
8103
|
help Show help
|
|
8050
8104
|
|
|
@@ -8062,6 +8116,9 @@ Pull Options:
|
|
|
8062
8116
|
--force, -y Delete stale files without prompting
|
|
8063
8117
|
-c, --config <path> Path to config file with pull settings
|
|
8064
8118
|
|
|
8119
|
+
Install-skill Options:
|
|
8120
|
+
--force, -y Overwrite existing skill
|
|
8121
|
+
|
|
8065
8122
|
Examples:
|
|
8066
8123
|
postgresdk init # Create config file
|
|
8067
8124
|
postgresdk generate # Generate using postgresdk.config.ts
|
|
@@ -8069,6 +8126,7 @@ Examples:
|
|
|
8069
8126
|
postgresdk generate -c custom.config.ts
|
|
8070
8127
|
postgresdk pull --from=https://api.com --output=./src/sdk
|
|
8071
8128
|
postgresdk pull # Pull using config file
|
|
8129
|
+
postgresdk install-skill # Install Claude Code skill
|
|
8072
8130
|
`);
|
|
8073
8131
|
process.exit(0);
|
|
8074
8132
|
}
|
|
@@ -8091,6 +8149,9 @@ if (command === "init") {
|
|
|
8091
8149
|
} else if (command === "pull") {
|
|
8092
8150
|
const { pullCommand: pullCommand2 } = await Promise.resolve().then(() => (init_cli_pull(), exports_cli_pull));
|
|
8093
8151
|
await pullCommand2(args.slice(1));
|
|
8152
|
+
} else if (command === "install-skill") {
|
|
8153
|
+
const { installSkillCommand: installSkillCommand2 } = await Promise.resolve().then(() => (init_cli_install_skill(), exports_cli_install_skill));
|
|
8154
|
+
await installSkillCommand2(args.slice(1));
|
|
8094
8155
|
} else {
|
|
8095
8156
|
console.error(`❌ Unknown command: ${command}`);
|
|
8096
8157
|
console.error(`Run 'postgresdk help' for usage information`);
|
package/dist/emit-client.d.ts
CHANGED
|
@@ -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
|
|
3
|
+
export declare function emitParamsZod(table: Table, graph: Graph, opts: {
|
|
4
|
+
maxLimit: number;
|
|
5
|
+
}): string;
|
package/dist/emit-routes.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1220,7 +1220,7 @@ function generateExampleValue(column) {
|
|
|
1220
1220
|
}
|
|
1221
1221
|
function generateQueryParams(table, enums) {
|
|
1222
1222
|
const params = {
|
|
1223
|
-
limit: "number - Max records to return (
|
|
1223
|
+
limit: "number - Max records to return (omit for all)",
|
|
1224
1224
|
offset: "number - Records to skip",
|
|
1225
1225
|
orderBy: "string | string[] - Field(s) to sort by",
|
|
1226
1226
|
order: "'asc' | 'desc' | ('asc' | 'desc')[] - Sort direction(s)"
|
|
@@ -1960,7 +1960,7 @@ export const Upsert${Type}Schema = z.object({
|
|
|
1960
1960
|
|
|
1961
1961
|
// src/emit-params-zod.ts
|
|
1962
1962
|
init_utils();
|
|
1963
|
-
function emitParamsZod(table, graph) {
|
|
1963
|
+
function emitParamsZod(table, graph, opts) {
|
|
1964
1964
|
const Type = pascal(table.name);
|
|
1965
1965
|
const columnNames = table.columns.map((c) => `"${c.name}"`).join(", ");
|
|
1966
1966
|
const pkCols = Array.isArray(table.pk) ? table.pk : table.pk ? [table.pk] : [];
|
|
@@ -1977,7 +1977,7 @@ export const ${Type}PkSchema = ${pkSchema};
|
|
|
1977
1977
|
// Schema for list query parameters
|
|
1978
1978
|
export const ${Type}ListParamsSchema = z.object({
|
|
1979
1979
|
include: ${includeSpecSchema}.optional(),
|
|
1980
|
-
limit: z.number().int().positive().max(
|
|
1980
|
+
limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
|
|
1981
1981
|
offset: z.number().int().nonnegative().optional(),
|
|
1982
1982
|
where: z.any().optional(),
|
|
1983
1983
|
vector: VectorSearchParamsSchema.optional(),
|
|
@@ -1999,12 +1999,12 @@ export type ${Type}OrderParams = z.infer<typeof ${Type}OrderParamsSchema>;
|
|
|
1999
1999
|
}
|
|
2000
2000
|
|
|
2001
2001
|
// src/emit-shared-params-zod.ts
|
|
2002
|
-
function emitSharedParamsZod() {
|
|
2002
|
+
function emitSharedParamsZod(opts) {
|
|
2003
2003
|
return `import { z } from "zod";
|
|
2004
2004
|
|
|
2005
2005
|
// Shared pagination schema (used across all tables)
|
|
2006
2006
|
export const PaginationParamsSchema = z.object({
|
|
2007
|
-
limit: z.number().int().positive().max(
|
|
2007
|
+
limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
|
|
2008
2008
|
offset: z.number().int().nonnegative().optional()
|
|
2009
2009
|
}).strict();
|
|
2010
2010
|
|
|
@@ -2036,8 +2036,8 @@ export interface PaginatedResponse<T> {
|
|
|
2036
2036
|
data: T[];
|
|
2037
2037
|
/** Total number of records matching the query (across all pages) */
|
|
2038
2038
|
total: number;
|
|
2039
|
-
/** Maximum number of records per page */
|
|
2040
|
-
limit
|
|
2039
|
+
/** Maximum number of records per page (absent when no limit was specified) */
|
|
2040
|
+
limit?: number;
|
|
2041
2041
|
/** Number of records skipped (for pagination) */
|
|
2042
2042
|
offset: number;
|
|
2043
2043
|
/** Whether there are more records available after this page */
|
|
@@ -2112,7 +2112,7 @@ const listSchema = z.object({
|
|
|
2112
2112
|
include: z.any().optional(),
|
|
2113
2113
|
select: z.array(z.string()).min(1).optional(),
|
|
2114
2114
|
exclude: z.array(z.string()).min(1).optional(),
|
|
2115
|
-
limit: z.number().int().positive().max(
|
|
2115
|
+
limit: z.number().int().positive()${opts.maxLimit > 0 ? `.max(${opts.maxLimit})` : ""}.optional(),
|
|
2116
2116
|
offset: z.number().int().min(0).optional(),
|
|
2117
2117
|
orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
|
|
2118
2118
|
order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional(),
|
|
@@ -3025,7 +3025,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
3025
3025
|
* @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
|
|
3026
3026
|
* @param params.orderBy - Column(s) to sort by
|
|
3027
3027
|
* @param params.order - Sort direction(s): "asc" or "desc"
|
|
3028
|
-
* @param params.limit - Maximum number of records to return (
|
|
3028
|
+
* @param params.limit - Maximum number of records to return${opts.maxLimit > 0 ? ` (max: ${opts.maxLimit})` : ""}. Omit to return all matching records.
|
|
3029
3029
|
* @param params.offset - Number of records to skip for pagination
|
|
3030
3030
|
* @param params.include - Related records to include (return type automatically infers included relations)
|
|
3031
3031
|
* @returns Paginated results with all fields (and included relations if specified)
|
|
@@ -3125,7 +3125,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
3125
3125
|
* @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
|
|
3126
3126
|
* @param params.orderBy - Column(s) to sort by
|
|
3127
3127
|
* @param params.order - Sort direction(s): "asc" or "desc"
|
|
3128
|
-
* @param params.limit - Maximum number of records to return (
|
|
3128
|
+
* @param params.limit - Maximum number of records to return${opts.maxLimit > 0 ? ` (max: ${opts.maxLimit})` : ""}. Omit to return all matching records.
|
|
3129
3129
|
* @param params.offset - Number of records to skip for pagination
|
|
3130
3130
|
* @param params.include - Related records to include (return type automatically infers included relations)
|
|
3131
3131
|
* @returns Paginated results with all fields (and included relations if specified)
|
|
@@ -5619,7 +5619,7 @@ export async function listRecords(
|
|
|
5619
5619
|
}
|
|
5620
5620
|
): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
5621
5621
|
try {
|
|
5622
|
-
const { where: whereClause, limit
|
|
5622
|
+
const { where: whereClause, limit, offset = 0, include, orderBy, order, vector, trigram, distinctOn, includeSoftDeleted } = params;
|
|
5623
5623
|
|
|
5624
5624
|
// DISTINCT ON support
|
|
5625
5625
|
const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
|
|
@@ -5755,10 +5755,22 @@ export async function listRecords(
|
|
|
5755
5755
|
orderBySQL = buildOrderBySQL(userOrderCols, userDirs);
|
|
5756
5756
|
}
|
|
5757
5757
|
|
|
5758
|
-
// Add limit and offset params
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5758
|
+
// Add limit and offset params (conditional — omit LIMIT clause when limit is undefined)
|
|
5759
|
+
let limitOffsetSQL: string;
|
|
5760
|
+
let allParams: any[];
|
|
5761
|
+
if (limit !== undefined) {
|
|
5762
|
+
const limitParam = \`$\${paramIndex}\`;
|
|
5763
|
+
const offsetParam = \`$\${paramIndex + 1}\`;
|
|
5764
|
+
limitOffsetSQL = \`LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
5765
|
+
allParams = [...queryParams, ...whereParams, limit, offset];
|
|
5766
|
+
} else if (offset > 0) {
|
|
5767
|
+
const offsetParam = \`$\${paramIndex}\`;
|
|
5768
|
+
limitOffsetSQL = \`OFFSET \${offsetParam}\`;
|
|
5769
|
+
allParams = [...queryParams, ...whereParams, offset];
|
|
5770
|
+
} else {
|
|
5771
|
+
limitOffsetSQL = '';
|
|
5772
|
+
allParams = [...queryParams, ...whereParams];
|
|
5773
|
+
}
|
|
5762
5774
|
|
|
5763
5775
|
// Get total count for pagination
|
|
5764
5776
|
const countText = distinctCols
|
|
@@ -5774,9 +5786,9 @@ export async function listRecords(
|
|
|
5774
5786
|
// Inner query: DISTINCT ON with only the distinctCols ORDER BY prefix (PG requirement).
|
|
5775
5787
|
// Outer query: free ORDER BY from the user's full orderBy list, plus LIMIT/OFFSET.
|
|
5776
5788
|
const innerQuery = \`SELECT DISTINCT ON (\${_distinctOnColsSQL}) \${baseColumns} FROM "\${ctx.table}" \${whereSQL} ORDER BY \${_distinctOnColsSQL}\`;
|
|
5777
|
-
text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL}
|
|
5789
|
+
text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL} \${limitOffsetSQL}\`.trim();
|
|
5778
5790
|
} else {
|
|
5779
|
-
text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL}
|
|
5791
|
+
text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} \${limitOffsetSQL}\`.trim();
|
|
5780
5792
|
}
|
|
5781
5793
|
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
|
|
5782
5794
|
|
|
@@ -5784,7 +5796,7 @@ export async function listRecords(
|
|
|
5784
5796
|
const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
|
|
5785
5797
|
|
|
5786
5798
|
// Calculate hasMore
|
|
5787
|
-
const hasMore = offset + limit < total;
|
|
5799
|
+
const hasMore = limit !== undefined ? offset + limit < total : false;
|
|
5788
5800
|
|
|
5789
5801
|
const metadata = {
|
|
5790
5802
|
data: parsedRows,
|
|
@@ -6824,6 +6836,7 @@ async function generate(configPath, options) {
|
|
|
6824
6836
|
clientDir = join2(originalClientDir, "sdk");
|
|
6825
6837
|
}
|
|
6826
6838
|
const serverFramework = cfg.serverFramework || "hono";
|
|
6839
|
+
const maxLimit = cfg.maxLimit ?? 1000;
|
|
6827
6840
|
const generateTests = cfg.tests?.generate ?? false;
|
|
6828
6841
|
const originalTestDir = cfg.tests?.output || "./api/tests";
|
|
6829
6842
|
let testDir = originalTestDir;
|
|
@@ -6852,7 +6865,7 @@ async function generate(configPath, options) {
|
|
|
6852
6865
|
files.push({ path: join2(clientDir, "include-spec.ts"), content: includeSpec });
|
|
6853
6866
|
const includeResolver = emitIncludeResolver(graph, cfg.useJsExtensions);
|
|
6854
6867
|
files.push({ path: join2(clientDir, "include-resolver.ts"), content: includeResolver });
|
|
6855
|
-
files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
|
|
6868
|
+
files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod({ maxLimit }) });
|
|
6856
6869
|
files.push({ path: join2(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
|
|
6857
6870
|
files.push({ path: join2(clientDir, "base-client.ts"), content: emitBaseClient() });
|
|
6858
6871
|
files.push({ path: join2(clientDir, "where-types.ts"), content: emitWhereTypes() });
|
|
@@ -6889,7 +6902,7 @@ async function generate(configPath, options) {
|
|
|
6889
6902
|
const zodSrc = emitZod(table, { numericMode }, model.enums);
|
|
6890
6903
|
files.push({ path: join2(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
6891
6904
|
files.push({ path: join2(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
6892
|
-
const paramsZodSrc = emitParamsZod(table, graph);
|
|
6905
|
+
const paramsZodSrc = emitParamsZod(table, graph, { maxLimit });
|
|
6893
6906
|
files.push({ path: join2(clientDir, "params", `${table.name}.ts`), content: paramsZodSrc });
|
|
6894
6907
|
let routeContent;
|
|
6895
6908
|
if (serverFramework === "hono") {
|
|
@@ -6899,7 +6912,8 @@ async function generate(configPath, options) {
|
|
|
6899
6912
|
includeMethodsDepth: cfg.includeMethodsDepth || 2,
|
|
6900
6913
|
authStrategy: getAuthStrategy(normalizedAuth),
|
|
6901
6914
|
useJsExtensions: cfg.useJsExtensions,
|
|
6902
|
-
apiPathPrefix: cfg.apiPathPrefix || "/v1"
|
|
6915
|
+
apiPathPrefix: cfg.apiPathPrefix || "/v1",
|
|
6916
|
+
maxLimit
|
|
6903
6917
|
});
|
|
6904
6918
|
} else {
|
|
6905
6919
|
throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
|
|
@@ -6915,7 +6929,8 @@ async function generate(configPath, options) {
|
|
|
6915
6929
|
exposeHardDelete,
|
|
6916
6930
|
useJsExtensions: cfg.useJsExtensionsClient,
|
|
6917
6931
|
includeMethodsDepth: cfg.includeMethodsDepth ?? 2,
|
|
6918
|
-
skipJunctionTables: cfg.skipJunctionTables ?? true
|
|
6932
|
+
skipJunctionTables: cfg.skipJunctionTables ?? true,
|
|
6933
|
+
maxLimit
|
|
6919
6934
|
}, model)
|
|
6920
6935
|
});
|
|
6921
6936
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface Config {
|
|
|
38
38
|
skipJunctionTables?: boolean;
|
|
39
39
|
serverFramework?: "hono" | "express" | "fastify";
|
|
40
40
|
apiPathPrefix?: string;
|
|
41
|
+
/** Maximum allowed value for the `limit` parameter in list operations (default: 1000). Set to 0 to disable. */
|
|
42
|
+
maxLimit?: number;
|
|
41
43
|
auth?: AuthConfigInput;
|
|
42
44
|
pullToken?: string;
|
|
43
45
|
pull?: PullConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postgresdk",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.5",
|
|
4
4
|
"description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
|
+
"skills",
|
|
17
18
|
"README.md",
|
|
18
19
|
"LICENSE"
|
|
19
20
|
],
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
},
|
|
23
24
|
"scripts": {
|
|
24
25
|
"build": "bun build src/cli.ts src/index.ts --outdir dist --target node --format esm --external=pg --external=zod --external=hono --external=prompts --external=node:* && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
25
|
-
"test": "bun test:write-files && bun test:init && bun test:gen && bun test test/test-where-clause.test.ts && bun test test/test-where-or-and.test.ts && bun test test/test-nested-include-options.test.ts && bun test test/test-include-methods-with-options.test.ts && bun test:gen-with-tests && bun test:pull && bun test:enums && bun test:typecheck && bun test:drizzle-e2e && bun test test/test-numeric-mode-integration.test.ts && bun test test/test-jsonb-array-serialization.test.ts && bun test test/test-trigram-search.test.ts && bun test test/test-soft-delete-config.test.ts && bun test test/test-soft-delete-include-loader.test.ts && bun test test/test-soft-delete-nested-include.test.ts && bun test test/test-transaction.test.ts && bun test test/test-nullable-belongs-to.test.ts",
|
|
26
|
+
"test": "bun test:write-files && bun test:init && bun test:gen && bun test test/test-where-clause.test.ts && bun test test/test-where-or-and.test.ts && bun test test/test-nested-include-options.test.ts && bun test test/test-include-methods-with-options.test.ts && bun test:gen-with-tests && bun test:pull && bun test:enums && bun test:typecheck && bun test:drizzle-e2e && bun test test/test-numeric-mode-integration.test.ts && bun test test/test-jsonb-array-serialization.test.ts && bun test test/test-trigram-search.test.ts && bun test test/test-soft-delete-config.test.ts && bun test test/test-soft-delete-include-loader.test.ts && bun test test/test-soft-delete-nested-include.test.ts && bun test test/test-transaction.test.ts && bun test test/test-nullable-belongs-to.test.ts && bun test test/test-no-default-limit.test.ts",
|
|
26
27
|
"test:write-files": "bun test/test-write-files-if-changed.ts",
|
|
27
28
|
"test:init": "bun test/test-init.ts",
|
|
28
29
|
"test:gen": "bun test/test-gen.ts",
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: postgresdk
|
|
3
|
+
description: "How to use a PostgreSDK-generated API and client SDK. Use this skill whenever the user is working with code generated by PostgreSDK — including the typed client SDK (e.g., `sdk.users.list()`, `sdk.books.create()`), the generated Hono API server (router, routes, auth middleware), or the CONTRACT.md/contract.ts reference files. Trigger on: SDK method calls like `.list()`, `.getByPk()`, `.create()`, `.update()`, `.upsert()`, `.$transaction()`, `.listWith*()`, WHERE clause operators like `$ilike`, `$in`, `$gte`, `$jsonbContains`, mentions of `postgresdk`, `CONTRACT.md`, `createRouter`, `BaseClient`, `PaginatedResponse`, `InsertX`/`SelectX`/`UpdateX` types, `postgresdk.config.ts`, or any questions about filtering, includes, pagination, vector search, or trigram search in the context of a generated API."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PostgreSDK Assistant
|
|
7
|
+
|
|
8
|
+
You are helping a developer who is using code generated by [PostgreSDK](https://github.com/nickreese/postgresdk) — a tool that generates a fully typed Hono API server and TypeScript client SDK from a PostgreSQL database schema.
|
|
9
|
+
|
|
10
|
+
## First: Discover the Generated Code
|
|
11
|
+
|
|
12
|
+
The output directory is fully configurable via `postgresdk.config.ts` — don't assume paths like `./api/server` or `./api/client`. Discover where the generated code actually lives:
|
|
13
|
+
|
|
14
|
+
1. **Search for `postgresdk.config.ts`** — if present, read the `outDir` value. It can be a string (e.g. `"./generated"`) or an object (`{ client: "./sdk", server: "./backend" }`). This tells you where the generated directories are.
|
|
15
|
+
2. **Search for generated marker files** — if no config, glob for `**/router.ts`, `**/base-client.ts`, or `**/CONTRACT.md` and look for the `AUTO-GENERATED FILE` header to find the generated output directories.
|
|
16
|
+
|
|
17
|
+
Once you've found the generated directories, identify which side the user is working on:
|
|
18
|
+
|
|
19
|
+
**API-side** (runs the server):
|
|
20
|
+
- Has a directory containing `router.ts`, `routes/*.ts`, `contract.ts`, `sdk-bundle.ts`
|
|
21
|
+
- Has `postgresdk.config.ts` with `connectionString`
|
|
22
|
+
- Asks about `createRouter`, `onRequest`, auth config, deployment, database drivers
|
|
23
|
+
|
|
24
|
+
**SDK-side** (consumes the API):
|
|
25
|
+
- Has a directory containing `base-client.ts`, table client files (e.g. `users.ts`), `CONTRACT.md`
|
|
26
|
+
- May have `postgresdk.config.ts` with a `pull` section instead of `connectionString`
|
|
27
|
+
- Asks about `sdk.tableName.method()`, filtering, includes, transactions
|
|
28
|
+
|
|
29
|
+
**Both** — common when a single `outDir` generates server and client subdirectories.
|
|
30
|
+
|
|
31
|
+
## Second: Read the Contract
|
|
32
|
+
|
|
33
|
+
The generated `CONTRACT.md` is the single source of truth for the user's specific schema. It contains every table, field, type, method, endpoint, and relationship. Read it before answering schema-specific questions.
|
|
34
|
+
|
|
35
|
+
Search with `**/CONTRACT.md` and look for the one with the `AUTO-GENERATED` header. It exists in both the server and client output directories — either copy works, they're identical.
|
|
36
|
+
|
|
37
|
+
If you can't find it, ask the user where their generated code lives.
|
|
38
|
+
|
|
39
|
+
## SDK Reference
|
|
40
|
+
|
|
41
|
+
### Initialization
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// Import path depends on outDir config — find the generated client directory
|
|
45
|
+
import { SDK } from '<client-dir>';
|
|
46
|
+
|
|
47
|
+
const sdk = new SDK({
|
|
48
|
+
baseUrl: 'http://localhost:3000',
|
|
49
|
+
auth: {
|
|
50
|
+
apiKey: 'your-key', // API key auth
|
|
51
|
+
// OR
|
|
52
|
+
jwt: 'your-token', // Static JWT
|
|
53
|
+
// OR
|
|
54
|
+
jwt: async () => getToken(), // Async JWT provider
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### CRUD Operations
|
|
60
|
+
|
|
61
|
+
Every table gets these methods (assuming single primary key):
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// Create
|
|
65
|
+
const item = await sdk.users.create({ name: 'Alice', email: 'alice@example.com' });
|
|
66
|
+
|
|
67
|
+
// Read
|
|
68
|
+
const user = await sdk.users.getByPk('id-123'); // single record or null
|
|
69
|
+
const result = await sdk.users.list({ limit: 20 }); // paginated
|
|
70
|
+
|
|
71
|
+
// Update
|
|
72
|
+
const updated = await sdk.users.update('id-123', { name: 'Bob' });
|
|
73
|
+
|
|
74
|
+
// Upsert (insert or update on conflict)
|
|
75
|
+
const upserted = await sdk.users.upsert({
|
|
76
|
+
where: { email: 'alice@example.com' }, // unique constraint columns
|
|
77
|
+
create: { email: 'alice@example.com', name: 'Alice' },
|
|
78
|
+
update: { name: 'Alice Updated' },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Delete
|
|
82
|
+
await sdk.users.hardDelete('id-123'); // permanent
|
|
83
|
+
await sdk.users.softDelete('id-123'); // sets soft-delete column (if configured)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Paginated Response Shape
|
|
87
|
+
|
|
88
|
+
All `list()` calls return:
|
|
89
|
+
```typescript
|
|
90
|
+
{
|
|
91
|
+
data: T[]; // array of records
|
|
92
|
+
total: number; // total matching records (respects WHERE)
|
|
93
|
+
limit?: number; // page size (absent when no limit)
|
|
94
|
+
offset: number; // current offset
|
|
95
|
+
hasMore: boolean; // more pages available
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### WHERE Filtering
|
|
100
|
+
|
|
101
|
+
Root-level keys are AND'd. Use `$or`/`$and` for logic (2 nesting levels max).
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
await sdk.users.list({
|
|
105
|
+
where: {
|
|
106
|
+
age: { $gte: 18, $lt: 65 },
|
|
107
|
+
email: { $ilike: '%@company.com' },
|
|
108
|
+
status: { $in: ['active', 'pending'] },
|
|
109
|
+
deleted_at: { $is: null },
|
|
110
|
+
$or: [{ role: 'admin' }, { role: 'mod' }]
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Available operators:**
|
|
116
|
+
|
|
117
|
+
| Operator | SQL | Applies to |
|
|
118
|
+
|----------|-----|------------|
|
|
119
|
+
| `$eq`, `$ne` | `=`, `!=` | All types |
|
|
120
|
+
| `$gt`, `$gte`, `$lt`, `$lte` | `>`, `>=`, `<`, `<=` | Number, Date |
|
|
121
|
+
| `$in`, `$nin` | `IN`, `NOT IN` | All types |
|
|
122
|
+
| `$like`, `$ilike` | `LIKE`, `ILIKE` | Strings |
|
|
123
|
+
| `$is`, `$isNot` | `IS NULL`, `IS NOT NULL` | Nullable fields |
|
|
124
|
+
| `$similarity`, `$wordSimilarity`, `$strictWordSimilarity` | pg_trgm operators | Strings (requires pg_trgm) |
|
|
125
|
+
| `$jsonbContains` | `@>` | JSONB |
|
|
126
|
+
| `$jsonbContainedBy` | `<@` | JSONB |
|
|
127
|
+
| `$jsonbHasKey` | `?` | JSONB |
|
|
128
|
+
| `$jsonbHasAnyKeys` | `?\|` | JSONB |
|
|
129
|
+
| `$jsonbHasAllKeys` | `?&` | JSONB |
|
|
130
|
+
| `$jsonbPath` | Path-based query | JSONB |
|
|
131
|
+
| `$or`, `$and` | `OR`, `AND` | Logical combinators |
|
|
132
|
+
|
|
133
|
+
### Sorting
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// Single column
|
|
137
|
+
await sdk.users.list({ orderBy: 'created_at', order: 'desc' });
|
|
138
|
+
|
|
139
|
+
// Multi-column with per-column direction
|
|
140
|
+
await sdk.users.list({
|
|
141
|
+
orderBy: ['status', 'created_at'],
|
|
142
|
+
order: ['asc', 'desc']
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### DISTINCT ON
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const latestPerUser = await sdk.events.list({
|
|
150
|
+
distinctOn: 'user_id',
|
|
151
|
+
orderBy: 'created_at',
|
|
152
|
+
order: 'desc'
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Field Selection
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// Only return specific fields
|
|
160
|
+
await sdk.users.list({ select: ['id', 'email', 'name'] });
|
|
161
|
+
|
|
162
|
+
// Return all fields except these
|
|
163
|
+
await sdk.users.list({ exclude: ['password_hash', 'secret_token'] });
|
|
164
|
+
|
|
165
|
+
// Works on single-record operations too
|
|
166
|
+
await sdk.users.create(data, { select: ['id', 'email'] });
|
|
167
|
+
await sdk.users.update(id, patch, { exclude: ['updated_at'] });
|
|
168
|
+
await sdk.users.getByPk(id, { select: ['id', 'name'] });
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Relationships & Includes
|
|
172
|
+
|
|
173
|
+
**Generic include:**
|
|
174
|
+
```typescript
|
|
175
|
+
const result = await sdk.authors.list({
|
|
176
|
+
include: { books: true }
|
|
177
|
+
});
|
|
178
|
+
// result.data[0].books is SelectBooks[]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Typed convenience methods** (generated per-table based on relationships):
|
|
182
|
+
```typescript
|
|
183
|
+
// listWith* and getByPkWith* — check CONTRACT.md for what's available
|
|
184
|
+
const result = await sdk.authors.listWithBooks({ limit: 10 });
|
|
185
|
+
const author = await sdk.authors.getByPkWithBooksAndTags('id');
|
|
186
|
+
|
|
187
|
+
// Control included relations
|
|
188
|
+
await sdk.authors.listWithBooks({
|
|
189
|
+
booksInclude: { orderBy: 'published_at', order: 'desc', limit: 5 }
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Nested includes:**
|
|
194
|
+
```typescript
|
|
195
|
+
const result = await sdk.authors.list({
|
|
196
|
+
include: { books: { tags: true } }
|
|
197
|
+
});
|
|
198
|
+
// result.data[0].books[0].tags is SelectTags[]
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Select/exclude on includes:**
|
|
202
|
+
```typescript
|
|
203
|
+
await sdk.authors.list({
|
|
204
|
+
select: ['id', 'name'],
|
|
205
|
+
include: {
|
|
206
|
+
books: { select: ['id', 'title'], orderBy: 'published_at', limit: 5 }
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Atomic Transactions
|
|
212
|
+
|
|
213
|
+
Use `$`-prefixed lazy builders inside `$transaction`:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
const [order, updatedUser] = await sdk.$transaction([
|
|
217
|
+
sdk.orders.$create({ user_id: user.id, total: 99 }),
|
|
218
|
+
sdk.users.$update(user.id, { last_order_at: new Date().toISOString() }),
|
|
219
|
+
]);
|
|
220
|
+
// TypeScript infers: [SelectOrders, SelectUsers | null]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Available builders: `$create`, `$update`, `$softDelete`, `$hardDelete`, `$upsert`.
|
|
224
|
+
|
|
225
|
+
All operations are Zod-validated before `BEGIN`. On failure, the entire transaction rolls back and throws with a `.failedAt` index.
|
|
226
|
+
|
|
227
|
+
### Soft Deletes
|
|
228
|
+
|
|
229
|
+
When `softDeleteColumn` is configured in `postgresdk.config.ts`:
|
|
230
|
+
- `softDelete(id)` — sets the column (e.g. `deleted_at = NOW()`)
|
|
231
|
+
- `hardDelete(id)` — permanent `DELETE` (unless `exposeHardDelete: false`)
|
|
232
|
+
- Soft-deleted rows are hidden from `list`/`getByPk` by default
|
|
233
|
+
- Pass `includeSoftDeleted: true` to see them
|
|
234
|
+
|
|
235
|
+
### Vector Search (pgvector)
|
|
236
|
+
|
|
237
|
+
For tables with `vector` columns. Results auto-include `_distance`.
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const results = await sdk.video_sections.list({
|
|
241
|
+
vector: {
|
|
242
|
+
field: 'vision_embedding',
|
|
243
|
+
query: embeddingArray, // number[]
|
|
244
|
+
metric: 'cosine', // 'cosine' | 'l2' | 'inner'
|
|
245
|
+
maxDistance: 0.5 // optional threshold
|
|
246
|
+
},
|
|
247
|
+
where: { status: 'published' }, // combine with regular filters
|
|
248
|
+
limit: 10
|
|
249
|
+
});
|
|
250
|
+
// results.data[0]._distance
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Trigram Search (pg_trgm)
|
|
254
|
+
|
|
255
|
+
Fuzzy text search. Results auto-include `_similarity`.
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
const results = await sdk.books.list({
|
|
259
|
+
trigram: {
|
|
260
|
+
field: 'title',
|
|
261
|
+
query: 'postgrs', // typo-tolerant
|
|
262
|
+
metric: 'similarity', // 'similarity' | 'wordSimilarity' | 'strictWordSimilarity'
|
|
263
|
+
threshold: 0.3 // min score 0–1
|
|
264
|
+
},
|
|
265
|
+
limit: 10
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Multi-field:** `fields: ['name', 'url']` with `strategy: 'greatest' | 'concat'`, or weighted: `fields: [{ field: 'name', weight: 2 }, { field: 'url', weight: 1 }]`.
|
|
270
|
+
|
|
271
|
+
Note: `trigram` and `vector` are mutually exclusive on a single `list()` call.
|
|
272
|
+
|
|
273
|
+
### Type Imports
|
|
274
|
+
|
|
275
|
+
Import paths below use `<client>` as a placeholder — substitute the actual path to the generated client directory (determined by the `outDir` config).
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
import { SDK } from '<client>';
|
|
279
|
+
import type { SelectUsers, InsertUsers, UpdateUsers } from '<client>/types/users';
|
|
280
|
+
import type { PaginatedResponse } from '<client>/types/shared';
|
|
281
|
+
|
|
282
|
+
// Zod schemas (for form validation, etc.)
|
|
283
|
+
import { InsertUsersSchema, UpdateUsersSchema } from '<client>/zod/users';
|
|
284
|
+
|
|
285
|
+
// Params schemas
|
|
286
|
+
import { UsersListParamsSchema, UsersPkSchema } from '<client>/params/users';
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## API Server Reference
|
|
290
|
+
|
|
291
|
+
### Basic Setup
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
import { Hono } from 'hono';
|
|
295
|
+
import { serve } from '@hono/node-server';
|
|
296
|
+
import { Client } from 'pg';
|
|
297
|
+
import { createRouter } from '<server>/router'; // path depends on outDir config
|
|
298
|
+
|
|
299
|
+
const app = new Hono();
|
|
300
|
+
const pg = new Client({ connectionString: process.env.DATABASE_URL });
|
|
301
|
+
await pg.connect();
|
|
302
|
+
|
|
303
|
+
const apiRouter = createRouter({ pg });
|
|
304
|
+
app.route('/', apiRouter);
|
|
305
|
+
|
|
306
|
+
serve({ fetch: app.fetch, port: 3000 });
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Auth Configuration (in postgresdk.config.ts)
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
export default {
|
|
313
|
+
connectionString: process.env.DATABASE_URL,
|
|
314
|
+
auth: {
|
|
315
|
+
// Option A: API key
|
|
316
|
+
apiKey: process.env.API_KEY,
|
|
317
|
+
|
|
318
|
+
// Option B: JWT (supports multiple services)
|
|
319
|
+
jwt: {
|
|
320
|
+
services: [
|
|
321
|
+
{ issuer: 'web-app', secret: 'env:WEB_SECRET' },
|
|
322
|
+
{ issuer: 'mobile', secret: 'env:MOBILE_SECRET' },
|
|
323
|
+
],
|
|
324
|
+
audience: 'my-api' // optional
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### onRequest Hook
|
|
331
|
+
|
|
332
|
+
Runs before every endpoint. Use for RLS, audit logging, authorization:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
const apiRouter = createRouter({
|
|
336
|
+
pg,
|
|
337
|
+
onRequest: async (c, pg) => {
|
|
338
|
+
const auth = c.get('auth');
|
|
339
|
+
|
|
340
|
+
// Set PostgreSQL session variable for RLS
|
|
341
|
+
if (auth?.kind === 'jwt' && auth.claims?.sub) {
|
|
342
|
+
await pg.query(`SET LOCAL app.user_id = '${auth.claims.sub}'`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Scope-based authorization
|
|
346
|
+
const scopes = auth?.claims?.scopes || [];
|
|
347
|
+
const table = c.req.path.split('/')[2];
|
|
348
|
+
if (!hasPermission(scopes, table, c.req.method)) {
|
|
349
|
+
throw new Error('Forbidden');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Deployment Patterns
|
|
356
|
+
|
|
357
|
+
- **Serverless** (Vercel, Cloudflare): Use `Pool` with `max: 1` — each instance is ephemeral
|
|
358
|
+
- **Traditional** (Railway, VPS): Use `Pool` with `max: 10` — reuse connections across requests
|
|
359
|
+
- **Edge**: Use `@neondatabase/serverless` Pool, set `useJsExtensions: true` in config
|
|
360
|
+
|
|
361
|
+
### SDK Distribution
|
|
362
|
+
|
|
363
|
+
The generated server auto-serves the client SDK:
|
|
364
|
+
- `GET /_psdk/sdk/manifest` — file listing
|
|
365
|
+
- `GET /_psdk/sdk/download` — complete bundle
|
|
366
|
+
- `GET /_psdk/contract.md` — markdown contract
|
|
367
|
+
- `GET /_psdk/contract.json` — JSON contract
|
|
368
|
+
|
|
369
|
+
Clients pull with: `npx postgresdk@latest pull --from=https://api.example.com --output=./src/sdk`
|
|
370
|
+
|
|
371
|
+
Protect with `pullToken` in config if needed.
|
|
372
|
+
|
|
373
|
+
## Key Configuration Options
|
|
374
|
+
|
|
375
|
+
| Option | Default | Description |
|
|
376
|
+
|--------|---------|-------------|
|
|
377
|
+
| `schema` | `"public"` | Database schema to introspect |
|
|
378
|
+
| `outDir` | `"./api"` | Output dir (or `{ client, server }`) |
|
|
379
|
+
| `numericMode` | `"auto"` | `"auto"` / `"number"` / `"string"` |
|
|
380
|
+
| `maxLimit` | `1000` | Max allowed `limit` (0 = no cap) |
|
|
381
|
+
| `includeMethodsDepth` | `2` | Max depth for `listWith*` methods |
|
|
382
|
+
| `dateType` | `"date"` | `"date"` / `"string"` |
|
|
383
|
+
| `useJsExtensions` | `false` | Add `.js` to imports (Edge/Deno) |
|
|
384
|
+
| `delete.softDeleteColumn` | — | Column name for soft deletes |
|
|
385
|
+
| `delete.exposeHardDelete` | `true` | Also expose `hardDelete()` |
|
|
386
|
+
|
|
387
|
+
## Common Gotchas
|
|
388
|
+
|
|
389
|
+
- **list() uses POST** — the `list` method sends a POST to `/v1/{table}/list` (not GET) because complex WHERE/include payloads don't fit in query strings. The simple GET endpoint exists too but only supports basic query params.
|
|
390
|
+
- **Composite PKs** — tables with composite primary keys don't get `getByPk`, `update`, `delete`, or `upsert` methods. Use `list` with WHERE filters instead.
|
|
391
|
+
- **Junction tables** — M:N junction tables (like `book_tags`) are detected automatically. The SDK generates include methods that skip the junction table: `sdk.books.listWithTags()` gives you tags directly, not book_tags.
|
|
392
|
+
- **Import paths** — depend entirely on the `outDir` config in `postgresdk.config.ts`. Always discover the actual generated directory before suggesting imports.
|
|
393
|
+
- **Types naming** — `Select{PascalTable}`, `Insert{PascalTable}`, `Update{PascalTable}` (e.g., `SelectUsers`, `InsertUsers`).
|
|
394
|
+
- **Regeneration** — all generated files have `AUTO-GENERATED FILE - DO NOT EDIT` headers. Changes are overwritten on `generate` or `pull`. Extend behavior via `onRequest` hooks, not by editing generated code.
|