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