postgresdk 0.16.9 → 0.16.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -584,6 +584,58 @@ const nestedResult = await sdk.authors.list({
584
584
  const authorsWithBooksAndTags = nestedResult.data;
585
585
  ```
586
586
 
587
+ **Typed Include Methods:**
588
+
589
+ For convenience and better type safety, the SDK generates `listWith*` and `getByPkWith*` methods for common include patterns:
590
+
591
+ ```typescript
592
+ // Typed methods provide full autocomplete and type safety
593
+ const result = await sdk.authors.listWithBooks();
594
+ // result.data[0].books is typed as SelectBooks[]
595
+
596
+ // Control included relations with options
597
+ const topAuthors = await sdk.authors.listWithBooks({
598
+ limit: 10,
599
+ booksInclude: {
600
+ orderBy: 'published_at',
601
+ order: 'desc',
602
+ limit: 5 // Only get top 5 books per author
603
+ }
604
+ });
605
+
606
+ // Parallel includes (multiple relations at once)
607
+ const result2 = await sdk.books.listWithAuthorAndTags({
608
+ tagsInclude: {
609
+ orderBy: 'name',
610
+ limit: 3
611
+ }
612
+ // author is included automatically (one-to-one)
613
+ });
614
+
615
+ // Nested includes with control at each level
616
+ const result3 = await sdk.authors.listWithBooksAndTags({
617
+ booksInclude: {
618
+ orderBy: 'title',
619
+ limit: 10,
620
+ include: {
621
+ tags: {
622
+ orderBy: 'name',
623
+ order: 'asc',
624
+ limit: 5
625
+ }
626
+ }
627
+ }
628
+ });
629
+
630
+ // Works with getByPk too
631
+ const author = await sdk.authors.getByPkWithBooks('author-id', {
632
+ booksInclude: {
633
+ orderBy: 'published_at',
634
+ limit: 3
635
+ }
636
+ });
637
+ ```
638
+
587
639
  #### Filtering & Pagination
588
640
 
589
641
  All `list()` methods return pagination metadata:
package/dist/cli.js CHANGED
@@ -2810,7 +2810,7 @@ function emitIncludeSpec(graph) {
2810
2810
  `;
2811
2811
  for (const [relKey, edge] of entries) {
2812
2812
  if (edge.kind === "many") {
2813
- out += ` ${relKey}?: boolean | { include?: ${toPascal(edge.target)}IncludeSpec; limit?: number; offset?: number; };
2813
+ out += ` ${relKey}?: boolean | { include?: ${toPascal(edge.target)}IncludeSpec; limit?: number; offset?: number; orderBy?: string; order?: "asc" | "desc"; };
2814
2814
  `;
2815
2815
  } else {
2816
2816
  out += ` ${relKey}?: boolean | ${toPascal(edge.target)}IncludeSpec;
@@ -3255,6 +3255,24 @@ function isJsonbType(pgType) {
3255
3255
  const t = pgType.toLowerCase();
3256
3256
  return t === "json" || t === "jsonb";
3257
3257
  }
3258
+ function toIncludeParamName(relationKey) {
3259
+ return `${relationKey}Include`;
3260
+ }
3261
+ function analyzeIncludeSpec(includeSpec) {
3262
+ const keys = Object.keys(includeSpec);
3263
+ if (keys.length > 1) {
3264
+ return { type: "parallel", keys };
3265
+ }
3266
+ const key = keys[0];
3267
+ if (!key) {
3268
+ return { type: "single", keys: [] };
3269
+ }
3270
+ const value = includeSpec[key];
3271
+ if (typeof value === "object" && value !== null) {
3272
+ return { type: "nested", keys: [key], nestedKey: key, nestedValue: value };
3273
+ }
3274
+ return { type: "single", keys: [key] };
3275
+ }
3258
3276
  function emitClient(table, graph, opts, model) {
3259
3277
  const Type = pascal(table.name);
3260
3278
  const ext = opts.useJsExtensions ? ".js" : "";
@@ -3293,39 +3311,157 @@ function emitClient(table, graph, opts, model) {
3293
3311
  let includeMethodsCode = "";
3294
3312
  for (const method of includeMethods) {
3295
3313
  const isGetByPk = method.name.startsWith("getByPk");
3296
- const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[]; }, "include">`;
3314
+ const pattern = analyzeIncludeSpec(method.includeSpec);
3297
3315
  const relationshipDesc = method.path.map((p, i) => {
3298
3316
  const isLast = i === method.path.length - 1;
3299
3317
  const relation = method.isMany[i] ? "many" : "one";
3300
3318
  return isLast ? p : `${p} -> `;
3301
3319
  }).join("");
3320
+ let paramsType = "";
3321
+ const includeParamNames = [];
3322
+ if (pattern.type === "single") {
3323
+ const key = pattern.keys[0];
3324
+ if (key) {
3325
+ const paramName = toIncludeParamName(key);
3326
+ includeParamNames.push(paramName);
3327
+ paramsType = `{
3328
+ limit?: number;
3329
+ offset?: number;
3330
+ where?: Where<Select${Type}>;
3331
+ orderBy?: string | string[];
3332
+ order?: "asc" | "desc" | ("asc" | "desc")[];
3333
+ ${paramName}?: {
3334
+ orderBy?: string | string[];
3335
+ order?: "asc" | "desc";
3336
+ limit?: number;
3337
+ offset?: number;
3338
+ };
3339
+ }`;
3340
+ }
3341
+ } else if (pattern.type === "parallel") {
3342
+ const includeParams = pattern.keys.map((key) => {
3343
+ const paramName = toIncludeParamName(key);
3344
+ includeParamNames.push(paramName);
3345
+ return `${paramName}?: {
3346
+ orderBy?: string | string[];
3347
+ order?: "asc" | "desc";
3348
+ limit?: number;
3349
+ offset?: number;
3350
+ }`;
3351
+ }).join(`;
3352
+ `);
3353
+ paramsType = `{
3354
+ limit?: number;
3355
+ offset?: number;
3356
+ where?: Where<Select${Type}>;
3357
+ orderBy?: string | string[];
3358
+ order?: "asc" | "desc" | ("asc" | "desc")[];
3359
+ ${includeParams};
3360
+ }`;
3361
+ } else if (pattern.type === "nested" && pattern.nestedKey) {
3362
+ const paramName = toIncludeParamName(pattern.nestedKey);
3363
+ includeParamNames.push(paramName);
3364
+ paramsType = `{
3365
+ limit?: number;
3366
+ offset?: number;
3367
+ where?: Where<Select${Type}>;
3368
+ orderBy?: string | string[];
3369
+ order?: "asc" | "desc" | ("asc" | "desc")[];
3370
+ ${paramName}?: {
3371
+ orderBy?: string | string[];
3372
+ order?: "asc" | "desc";
3373
+ limit?: number;
3374
+ offset?: number;
3375
+ include?: any;
3376
+ };
3377
+ }`;
3378
+ }
3302
3379
  if (isGetByPk) {
3303
3380
  const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
3304
3381
  const baseReturnType = method.returnType.replace(" | null", "");
3382
+ let transformCode = "";
3383
+ if (includeParamNames.length > 0) {
3384
+ const destructure = includeParamNames.map((name) => name).join(", ");
3385
+ if (pattern.type === "single") {
3386
+ const key = pattern.keys[0];
3387
+ const paramName = includeParamNames[0];
3388
+ transformCode = `
3389
+ const { ${destructure} } = params ?? {};
3390
+ const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};`;
3391
+ } else if (pattern.type === "parallel") {
3392
+ const includeSpecCode = pattern.keys.map((key, idx) => {
3393
+ const paramName = includeParamNames[idx];
3394
+ return `${key}: ${paramName} ?? true`;
3395
+ }).join(", ");
3396
+ transformCode = `
3397
+ const { ${destructure} } = params ?? {};
3398
+ const includeSpec = { ${includeSpecCode} };`;
3399
+ } else if (pattern.type === "nested" && pattern.nestedKey) {
3400
+ const key = pattern.nestedKey;
3401
+ const paramName = includeParamNames[0];
3402
+ transformCode = `
3403
+ const { ${destructure} } = params ?? {};
3404
+ const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};`;
3405
+ }
3406
+ } else {
3407
+ transformCode = `
3408
+ const includeSpec = ${JSON.stringify(method.includeSpec)};`;
3409
+ }
3305
3410
  includeMethodsCode += `
3306
3411
  /**
3307
3412
  * Get a ${table.name} record by primary key with included related ${relationshipDesc}
3308
3413
  * @param pk - The primary key value${hasCompositePk ? "s" : ""}
3414
+ * @param params - Optional include options
3309
3415
  * @returns The record with nested ${method.path.join(" and ")} if found, null otherwise
3310
3416
  */
3311
- async ${method.name}(pk: ${pkType}): Promise<${method.returnType}> {
3417
+ async ${method.name}(pk: ${pkType}, params?: ${paramsType}): Promise<${method.returnType}> {${transformCode}
3312
3418
  const results = await this.post<PaginatedResponse<${baseReturnType}>>(\`\${this.resource}/list\`, {
3313
3419
  where: ${pkWhere},
3314
- include: ${JSON.stringify(method.includeSpec)},
3420
+ include: includeSpec,
3315
3421
  limit: 1
3316
3422
  });
3317
3423
  return (results.data[0] as ${baseReturnType}) ?? null;
3318
3424
  }
3319
3425
  `;
3320
3426
  } else {
3427
+ let transformCode = "";
3428
+ if (includeParamNames.length > 0) {
3429
+ const destructure = includeParamNames.map((name) => name).join(", ");
3430
+ if (pattern.type === "single") {
3431
+ const key = pattern.keys[0];
3432
+ const paramName = includeParamNames[0];
3433
+ transformCode = `
3434
+ const { ${destructure}, ...baseParams } = params ?? {};
3435
+ const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};
3436
+ return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...baseParams, include: includeSpec });`;
3437
+ } else if (pattern.type === "parallel") {
3438
+ const includeSpecCode = pattern.keys.map((key, idx) => {
3439
+ const paramName = includeParamNames[idx];
3440
+ return `${key}: ${paramName} ?? true`;
3441
+ }).join(", ");
3442
+ transformCode = `
3443
+ const { ${destructure}, ...baseParams } = params ?? {};
3444
+ const includeSpec = { ${includeSpecCode} };
3445
+ return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...baseParams, include: includeSpec });`;
3446
+ } else if (pattern.type === "nested" && pattern.nestedKey) {
3447
+ const key = pattern.nestedKey;
3448
+ const paramName = includeParamNames[0];
3449
+ transformCode = `
3450
+ const { ${destructure}, ...baseParams } = params ?? {};
3451
+ const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};
3452
+ return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...baseParams, include: includeSpec });`;
3453
+ }
3454
+ } else {
3455
+ transformCode = `
3456
+ return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...params, include: ${JSON.stringify(method.includeSpec)} });`;
3457
+ }
3321
3458
  includeMethodsCode += `
3322
3459
  /**
3323
3460
  * List ${table.name} records with included related ${relationshipDesc}
3324
- * @param params - Query parameters (where, orderBy, order, limit, offset)
3461
+ * @param params - Query parameters (where, orderBy, order, limit, offset) and include options
3325
3462
  * @returns Paginated results with nested ${method.path.join(" and ")} included
3326
3463
  */
3327
- async ${method.name}(${baseParams}): Promise<${method.returnType}> {
3328
- return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...params, include: ${JSON.stringify(method.includeSpec)} });
3464
+ async ${method.name}(params?: ${paramsType}): Promise<${method.returnType}> {${transformCode}
3329
3465
  }
3330
3466
  `;
3331
3467
  }
@@ -3402,7 +3538,7 @@ ${hasJsonbColumns ? ` /**
3402
3538
  * @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
3403
3539
  * @param params.orderBy - Column(s) to sort by
3404
3540
  * @param params.order - Sort direction(s): "asc" or "desc"
3405
- * @param params.limit - Maximum number of records to return (default: 50, max: 100)
3541
+ * @param params.limit - Maximum number of records to return (default: 50, max: 1000)
3406
3542
  * @param params.offset - Number of records to skip for pagination
3407
3543
  * @param params.include - Related records to include (see listWith* methods for typed includes)
3408
3544
  * @returns Paginated results with data, total count, and hasMore flag
@@ -3431,7 +3567,7 @@ ${hasJsonbColumns ? ` /**
3431
3567
  * @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
3432
3568
  * @param params.orderBy - Column(s) to sort by
3433
3569
  * @param params.order - Sort direction(s): "asc" or "desc"
3434
- * @param params.limit - Maximum number of records to return (default: 50, max: 100)
3570
+ * @param params.limit - Maximum number of records to return (default: 50, max: 1000)
3435
3571
  * @param params.offset - Number of records to skip for pagination
3436
3572
  * @param params.include - Related records to include (see listWith* methods for typed includes)
3437
3573
  * @returns Paginated results with data, total count, and hasMore flag
@@ -3768,6 +3904,13 @@ type Graph = typeof RELATION_GRAPH;
3768
3904
  type TableName = keyof Graph;
3769
3905
  type IncludeSpec = any;
3770
3906
 
3907
+ type RelationOptions = {
3908
+ limit?: number;
3909
+ offset?: number;
3910
+ orderBy?: string;
3911
+ order?: "asc" | "desc";
3912
+ };
3913
+
3771
3914
  // Debug helpers (enabled with SDK_DEBUG=1)
3772
3915
  const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
3773
3916
  const log = {
@@ -3869,14 +4012,43 @@ export async function loadIncludes(
3869
4012
  // Safely run each loader; never let one bad edge 500 the route
3870
4013
  if (rel.via) {
3871
4014
  // M:N via junction
4015
+ const specValue = s[key];
4016
+ const options: RelationOptions = {};
4017
+ let childSpec: any = undefined;
4018
+
4019
+ if (specValue && typeof specValue === "object" && specValue !== true) {
4020
+ // Extract options
4021
+ if (specValue.limit !== undefined) options.limit = specValue.limit;
4022
+ if (specValue.offset !== undefined) options.offset = specValue.offset;
4023
+ if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
4024
+ if (specValue.order !== undefined) options.order = specValue.order;
4025
+
4026
+ // Extract nested spec - support both formats:
4027
+ // New: { limit: 3, include: { tags: true } }
4028
+ // Old: { tags: true } (backward compatibility)
4029
+ if (specValue.include !== undefined) {
4030
+ childSpec = specValue.include;
4031
+ } else {
4032
+ // Build childSpec from non-option keys
4033
+ const nonOptionKeys = Object.keys(specValue).filter(
4034
+ k => k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
4035
+ );
4036
+ if (nonOptionKeys.length > 0) {
4037
+ childSpec = {};
4038
+ for (const k of nonOptionKeys) {
4039
+ childSpec[k] = specValue[k];
4040
+ }
4041
+ }
4042
+ }
4043
+ }
4044
+
3872
4045
  try {
3873
- await loadManyToMany(table, target, rel.via as string, rows, key);
4046
+ await loadManyToMany(table, target, rel.via as string, rows, key, options);
3874
4047
  } catch (e: any) {
3875
4048
  log.error("loadManyToMany failed", { table, key, via: rel.via, target }, e?.message ?? e);
3876
4049
  for (const r of rows) r[key] = [];
3877
4050
  }
3878
- // Recurse if nested include specified
3879
- const childSpec = s[key] && typeof s[key] === "object" ? s[key] : undefined;
4051
+
3880
4052
  if (childSpec) {
3881
4053
  const children = rows.flatMap(r => (r[key] ?? []));
3882
4054
  try {
@@ -3890,13 +4062,43 @@ export async function loadIncludes(
3890
4062
 
3891
4063
  if (rel.kind === "many") {
3892
4064
  // 1:N target has FK to current
4065
+ const specValue = s[key];
4066
+ const options: RelationOptions = {};
4067
+ let childSpec: any = undefined;
4068
+
4069
+ if (specValue && typeof specValue === "object" && specValue !== true) {
4070
+ // Extract options
4071
+ if (specValue.limit !== undefined) options.limit = specValue.limit;
4072
+ if (specValue.offset !== undefined) options.offset = specValue.offset;
4073
+ if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
4074
+ if (specValue.order !== undefined) options.order = specValue.order;
4075
+
4076
+ // Extract nested spec - support both formats:
4077
+ // New: { limit: 3, include: { tags: true } }
4078
+ // Old: { tags: true } (backward compatibility)
4079
+ if (specValue.include !== undefined) {
4080
+ childSpec = specValue.include;
4081
+ } else {
4082
+ // Build childSpec from non-option keys
4083
+ const nonOptionKeys = Object.keys(specValue).filter(
4084
+ k => k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
4085
+ );
4086
+ if (nonOptionKeys.length > 0) {
4087
+ childSpec = {};
4088
+ for (const k of nonOptionKeys) {
4089
+ childSpec[k] = specValue[k];
4090
+ }
4091
+ }
4092
+ }
4093
+ }
4094
+
3893
4095
  try {
3894
- await loadOneToMany(table, target, rows, key);
4096
+ await loadOneToMany(table, target, rows, key, options);
3895
4097
  } catch (e: any) {
3896
4098
  log.error("loadOneToMany failed", { table, key, target }, e?.message ?? e);
3897
4099
  for (const r of rows) r[key] = [];
3898
4100
  }
3899
- const childSpec = s[key] && typeof s[key] === "object" ? s[key] : undefined;
4101
+
3900
4102
  if (childSpec) {
3901
4103
  const children = rows.flatMap(r => (r[key] ?? []));
3902
4104
  try {
@@ -3984,7 +4186,7 @@ export async function loadIncludes(
3984
4186
  }
3985
4187
  }
3986
4188
 
3987
- async function loadOneToMany(curr: TableName, target: TableName, rows: any[], key: string) {
4189
+ async function loadOneToMany(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
3988
4190
  // target has FK cols referencing current PK
3989
4191
  const fk = (FK_INDEX as any)[target].find((f: any) => f.toTable === curr);
3990
4192
  if (!fk) { for (const r of rows) r[key] = []; return; }
@@ -3995,18 +4197,50 @@ export async function loadIncludes(
3995
4197
 
3996
4198
  const where = buildOrAndPredicate(fk.from, tuples.length, 1);
3997
4199
  const params = tuples.flat();
3998
- const sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
3999
- log.debug("oneToMany SQL", { curr, target, key, sql, paramsCount: params.length });
4200
+
4201
+ // Build SQL with optional ORDER BY, LIMIT, OFFSET
4202
+ let sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
4203
+
4204
+ // If limit/offset are needed, use window functions to limit per parent
4205
+ if (options.limit !== undefined || options.offset !== undefined) {
4206
+ const orderByClause = options.orderBy
4207
+ ? \`ORDER BY "\${options.orderBy}" \${options.order === 'desc' ? 'DESC' : 'ASC'}\`
4208
+ : 'ORDER BY (SELECT NULL)';
4209
+
4210
+ const partitionCols = fk.from.map((c: string) => \`"\${c}"\`).join(', ');
4211
+ const offset = options.offset ?? 0;
4212
+ const limit = options.limit ?? 999999999;
4213
+
4214
+ sql = \`
4215
+ SELECT * FROM (
4216
+ SELECT *, ROW_NUMBER() OVER (PARTITION BY \${partitionCols} \${orderByClause}) as __rn
4217
+ FROM "\${target}"
4218
+ WHERE \${where}
4219
+ ) __sub
4220
+ WHERE __rn > \${offset} AND __rn <= \${offset + limit}
4221
+ \`;
4222
+ } else if (options.orderBy) {
4223
+ // Just ORDER BY without limit/offset
4224
+ sql += \` ORDER BY "\${options.orderBy}" \${options.order === 'desc' ? 'DESC' : 'ASC'}\`;
4225
+ }
4226
+
4227
+ log.debug("oneToMany SQL", { curr, target, key, sql, paramsCount: params.length, options });
4000
4228
  const { rows: children } = await pg.query(sql, params);
4001
4229
 
4002
- const groups = groupByTuple(children, fk.from);
4230
+ // Remove __rn column if it exists
4231
+ const cleanChildren = children.map((row: any) => {
4232
+ const { __rn, ...rest } = row;
4233
+ return rest;
4234
+ });
4235
+
4236
+ const groups = groupByTuple(cleanChildren, fk.from);
4003
4237
  for (const r of rows) {
4004
4238
  const keyStr = JSON.stringify(pkCols.map((c: string) => r[c]));
4005
4239
  r[key] = groups.get(keyStr) ?? [];
4006
4240
  }
4007
4241
  }
4008
4242
 
4009
- async function loadManyToMany(curr: TableName, target: TableName, via: string, rows: any[], key: string) {
4243
+ async function loadManyToMany(curr: TableName, target: TableName, via: string, rows: any[], key: string, options: RelationOptions = {}) {
4010
4244
  // via has two FKs: one to curr, one to target
4011
4245
  const toCurr = (FK_INDEX as any)[via].find((f: any) => f.toTable === curr);
4012
4246
  const toTarget = (FK_INDEX as any)[via].find((f: any) => f.toTable === target);
@@ -4016,31 +4250,83 @@ export async function loadIncludes(
4016
4250
  const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
4017
4251
  if (!tuples.length) { for (const r of rows) r[key] = []; return; }
4018
4252
 
4019
- // 1) Load junction rows for current parents
4020
4253
  const whereVia = buildOrAndPredicate(toCurr.from, tuples.length, 1);
4021
- const sqlVia = \`SELECT * FROM "\${via}" WHERE \${whereVia}\`;
4022
- const paramsVia = tuples.flat();
4023
- log.debug("manyToMany junction SQL", { curr, target, via, key, sql: sqlVia, paramsCount: paramsVia.length });
4024
- const { rows: jrows } = await pg.query(sqlVia, paramsVia);
4025
-
4026
- if (!jrows.length) { for (const r of rows) r[key] = []; return; }
4254
+ const params = tuples.flat();
4027
4255
 
4028
- // 2) Load targets by distinct target fk tuples in junction
4029
- const tTuples = distinctTuples(jrows, toTarget.from);
4030
- const whereT = buildOrAndPredicate((PKS as any)[target], tTuples.length, 1);
4031
- const sqlT = \`SELECT * FROM "\${target}" WHERE \${whereT}\`;
4032
- const paramsT = tTuples.flat();
4033
- log.debug("manyToMany target SQL", { curr, target, via, key, sql: sqlT, paramsCount: paramsT.length });
4034
- const { rows: targets } = await pg.query(sqlT, paramsT);
4256
+ // If we have limit/offset/orderBy, use a JOIN with window functions
4257
+ if (options.limit !== undefined || options.offset !== undefined || options.orderBy) {
4258
+ const orderByClause = options.orderBy
4259
+ ? \`ORDER BY t."\${options.orderBy}" \${options.order === 'desc' ? 'DESC' : 'ASC'}\`
4260
+ : 'ORDER BY (SELECT NULL)';
4261
+
4262
+ const partitionCols = toCurr.from.map((c: string) => \`j."\${c}"\`).join(', ');
4263
+ const offset = options.offset ?? 0;
4264
+ const limit = options.limit ?? 999999999;
4265
+
4266
+ const targetPkCols = (PKS as any)[target] as string[];
4267
+ const joinConditions = toTarget.from.map((jCol: string, i: number) => {
4268
+ return \`j."\${jCol}" = t."\${targetPkCols[i]}"\`;
4269
+ }).join(' AND ');
4270
+
4271
+ const sql = \`
4272
+ SELECT __numbered.*
4273
+ FROM (
4274
+ SELECT t.*,
4275
+ ROW_NUMBER() OVER (PARTITION BY \${partitionCols} \${orderByClause}) as __rn,
4276
+ \${toCurr.from.map((c: string) => \`j."\${c}"\`).join(' || \\',\\' || ')} as __parent_fk
4277
+ FROM "\${via}" j
4278
+ INNER JOIN "\${target}" t ON \${joinConditions}
4279
+ WHERE \${whereVia}
4280
+ ) __numbered
4281
+ WHERE __numbered.__rn > \${offset} AND __numbered.__rn <= \${offset + limit}
4282
+ \`;
4283
+
4284
+ log.debug("manyToMany SQL with options", { curr, target, via, key, sql, paramsCount: params.length, options });
4285
+ const { rows: results } = await pg.query(sql, params);
4286
+
4287
+ // Clean and group results
4288
+ const cleanResults = results.map((row: any) => {
4289
+ const { __rn, __parent_fk, ...rest } = row;
4290
+ return { ...rest, __parent_fk };
4291
+ });
4035
4292
 
4036
- const tIdx = indexByTuple(targets, (PKS as any)[target]);
4293
+ const grouped = new Map<string, any[]>();
4294
+ for (const row of cleanResults) {
4295
+ const { __parent_fk, ...cleanRow } = row;
4296
+ const arr = grouped.get(__parent_fk) ?? [];
4297
+ arr.push(cleanRow);
4298
+ grouped.set(__parent_fk, arr);
4299
+ }
4037
4300
 
4038
- // 3) Group junction rows by current pk tuple, map to target rows
4039
- const byCurr = groupByTuple(jrows, toCurr.from);
4040
- for (const r of rows) {
4041
- const currKey = JSON.stringify(pkCols.map((c: string) => r[c]));
4042
- const j = byCurr.get(currKey) ?? [];
4043
- r[key] = j.map(jr => tIdx.get(JSON.stringify(toTarget.from.map((c: string) => jr[c])))).filter(Boolean);
4301
+ for (const r of rows) {
4302
+ const currKey = pkCols.map((c: string) => r[c]).join(',');
4303
+ r[key] = grouped.get(currKey) ?? [];
4304
+ }
4305
+ } else {
4306
+ // Original logic without options
4307
+ const sqlVia = \`SELECT * FROM "\${via}" WHERE \${whereVia}\`;
4308
+ log.debug("manyToMany junction SQL", { curr, target, via, key, sql: sqlVia, paramsCount: params.length });
4309
+ const { rows: jrows } = await pg.query(sqlVia, params);
4310
+
4311
+ if (!jrows.length) { for (const r of rows) r[key] = []; return; }
4312
+
4313
+ // 2) Load targets by distinct target fk tuples in junction
4314
+ const tTuples = distinctTuples(jrows, toTarget.from);
4315
+ const whereT = buildOrAndPredicate((PKS as any)[target], tTuples.length, 1);
4316
+ const sqlT = \`SELECT * FROM "\${target}" WHERE \${whereT}\`;
4317
+ const paramsT = tTuples.flat();
4318
+ log.debug("manyToMany target SQL", { curr, target, via, key, sql: sqlT, paramsCount: paramsT.length });
4319
+ const { rows: targets } = await pg.query(sqlT, paramsT);
4320
+
4321
+ const tIdx = indexByTuple(targets, (PKS as any)[target]);
4322
+
4323
+ // 3) Group junction rows by current pk tuple, map to target rows
4324
+ const byCurr = groupByTuple(jrows, toCurr.from);
4325
+ for (const r of rows) {
4326
+ const currKey = JSON.stringify(pkCols.map((c: string) => r[c]));
4327
+ const j = byCurr.get(currKey) ?? [];
4328
+ r[key] = j.map(jr => tIdx.get(JSON.stringify(toTarget.from.map((c: string) => jr[c])))).filter(Boolean);
4329
+ }
4044
4330
  }
4045
4331
  }
4046
4332
  }
@@ -4119,7 +4405,9 @@ ${baseSelectType}
4119
4405
  * @example Insert${Type}<{ metadata: MyMetadataType }>
4120
4406
  */
4121
4407
  export type Insert${Type}<TJsonb extends Partial<_Insert${Type}Base> = {}> =
4122
- Omit<_Insert${Type}Base, keyof TJsonb> & TJsonb;
4408
+ {} extends TJsonb
4409
+ ? _Insert${Type}Base
4410
+ : Omit<_Insert${Type}Base, keyof TJsonb> & TJsonb;
4123
4411
 
4124
4412
  /**
4125
4413
  * Type for updating an existing ${table.name} record.
@@ -4129,7 +4417,9 @@ export type Insert${Type}<TJsonb extends Partial<_Insert${Type}Base> = {}> =
4129
4417
  * @example Update${Type}<{ metadata: MyMetadataType }>
4130
4418
  */
4131
4419
  export type Update${Type}<TJsonb extends Partial<_Select${Type}Base> = {}> =
4132
- Partial<Omit<_Insert${Type}Base, keyof TJsonb> & TJsonb>;
4420
+ {} extends TJsonb
4421
+ ? Partial<_Insert${Type}Base>
4422
+ : Partial<Omit<_Insert${Type}Base, keyof TJsonb> & TJsonb>;
4133
4423
 
4134
4424
  /**
4135
4425
  * Type representing a ${table.name} record from the database.
@@ -4139,7 +4429,9 @@ export type Update${Type}<TJsonb extends Partial<_Select${Type}Base> = {}> =
4139
4429
  * @example Select${Type}<{ metadata: MyMetadataType }>
4140
4430
  */
4141
4431
  export type Select${Type}<TJsonb extends Partial<_Select${Type}Base> = {}> =
4142
- Omit<_Select${Type}Base, keyof TJsonb> & TJsonb;
4432
+ {} extends TJsonb
4433
+ ? _Select${Type}Base
4434
+ : Omit<_Select${Type}Base, keyof TJsonb> & TJsonb;
4143
4435
  `;
4144
4436
  }
4145
4437
  return `/**
package/dist/index.js CHANGED
@@ -1909,7 +1909,7 @@ function emitIncludeSpec(graph) {
1909
1909
  `;
1910
1910
  for (const [relKey, edge] of entries) {
1911
1911
  if (edge.kind === "many") {
1912
- out += ` ${relKey}?: boolean | { include?: ${toPascal(edge.target)}IncludeSpec; limit?: number; offset?: number; };
1912
+ out += ` ${relKey}?: boolean | { include?: ${toPascal(edge.target)}IncludeSpec; limit?: number; offset?: number; orderBy?: string; order?: "asc" | "desc"; };
1913
1913
  `;
1914
1914
  } else {
1915
1915
  out += ` ${relKey}?: boolean | ${toPascal(edge.target)}IncludeSpec;
@@ -2354,6 +2354,24 @@ function isJsonbType(pgType) {
2354
2354
  const t = pgType.toLowerCase();
2355
2355
  return t === "json" || t === "jsonb";
2356
2356
  }
2357
+ function toIncludeParamName(relationKey) {
2358
+ return `${relationKey}Include`;
2359
+ }
2360
+ function analyzeIncludeSpec(includeSpec) {
2361
+ const keys = Object.keys(includeSpec);
2362
+ if (keys.length > 1) {
2363
+ return { type: "parallel", keys };
2364
+ }
2365
+ const key = keys[0];
2366
+ if (!key) {
2367
+ return { type: "single", keys: [] };
2368
+ }
2369
+ const value = includeSpec[key];
2370
+ if (typeof value === "object" && value !== null) {
2371
+ return { type: "nested", keys: [key], nestedKey: key, nestedValue: value };
2372
+ }
2373
+ return { type: "single", keys: [key] };
2374
+ }
2357
2375
  function emitClient(table, graph, opts, model) {
2358
2376
  const Type = pascal(table.name);
2359
2377
  const ext = opts.useJsExtensions ? ".js" : "";
@@ -2392,39 +2410,157 @@ function emitClient(table, graph, opts, model) {
2392
2410
  let includeMethodsCode = "";
2393
2411
  for (const method of includeMethods) {
2394
2412
  const isGetByPk = method.name.startsWith("getByPk");
2395
- const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[]; }, "include">`;
2413
+ const pattern = analyzeIncludeSpec(method.includeSpec);
2396
2414
  const relationshipDesc = method.path.map((p, i) => {
2397
2415
  const isLast = i === method.path.length - 1;
2398
2416
  const relation = method.isMany[i] ? "many" : "one";
2399
2417
  return isLast ? p : `${p} -> `;
2400
2418
  }).join("");
2419
+ let paramsType = "";
2420
+ const includeParamNames = [];
2421
+ if (pattern.type === "single") {
2422
+ const key = pattern.keys[0];
2423
+ if (key) {
2424
+ const paramName = toIncludeParamName(key);
2425
+ includeParamNames.push(paramName);
2426
+ paramsType = `{
2427
+ limit?: number;
2428
+ offset?: number;
2429
+ where?: Where<Select${Type}>;
2430
+ orderBy?: string | string[];
2431
+ order?: "asc" | "desc" | ("asc" | "desc")[];
2432
+ ${paramName}?: {
2433
+ orderBy?: string | string[];
2434
+ order?: "asc" | "desc";
2435
+ limit?: number;
2436
+ offset?: number;
2437
+ };
2438
+ }`;
2439
+ }
2440
+ } else if (pattern.type === "parallel") {
2441
+ const includeParams = pattern.keys.map((key) => {
2442
+ const paramName = toIncludeParamName(key);
2443
+ includeParamNames.push(paramName);
2444
+ return `${paramName}?: {
2445
+ orderBy?: string | string[];
2446
+ order?: "asc" | "desc";
2447
+ limit?: number;
2448
+ offset?: number;
2449
+ }`;
2450
+ }).join(`;
2451
+ `);
2452
+ paramsType = `{
2453
+ limit?: number;
2454
+ offset?: number;
2455
+ where?: Where<Select${Type}>;
2456
+ orderBy?: string | string[];
2457
+ order?: "asc" | "desc" | ("asc" | "desc")[];
2458
+ ${includeParams};
2459
+ }`;
2460
+ } else if (pattern.type === "nested" && pattern.nestedKey) {
2461
+ const paramName = toIncludeParamName(pattern.nestedKey);
2462
+ includeParamNames.push(paramName);
2463
+ paramsType = `{
2464
+ limit?: number;
2465
+ offset?: number;
2466
+ where?: Where<Select${Type}>;
2467
+ orderBy?: string | string[];
2468
+ order?: "asc" | "desc" | ("asc" | "desc")[];
2469
+ ${paramName}?: {
2470
+ orderBy?: string | string[];
2471
+ order?: "asc" | "desc";
2472
+ limit?: number;
2473
+ offset?: number;
2474
+ include?: any;
2475
+ };
2476
+ }`;
2477
+ }
2401
2478
  if (isGetByPk) {
2402
2479
  const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
2403
2480
  const baseReturnType = method.returnType.replace(" | null", "");
2481
+ let transformCode = "";
2482
+ if (includeParamNames.length > 0) {
2483
+ const destructure = includeParamNames.map((name) => name).join(", ");
2484
+ if (pattern.type === "single") {
2485
+ const key = pattern.keys[0];
2486
+ const paramName = includeParamNames[0];
2487
+ transformCode = `
2488
+ const { ${destructure} } = params ?? {};
2489
+ const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};`;
2490
+ } else if (pattern.type === "parallel") {
2491
+ const includeSpecCode = pattern.keys.map((key, idx) => {
2492
+ const paramName = includeParamNames[idx];
2493
+ return `${key}: ${paramName} ?? true`;
2494
+ }).join(", ");
2495
+ transformCode = `
2496
+ const { ${destructure} } = params ?? {};
2497
+ const includeSpec = { ${includeSpecCode} };`;
2498
+ } else if (pattern.type === "nested" && pattern.nestedKey) {
2499
+ const key = pattern.nestedKey;
2500
+ const paramName = includeParamNames[0];
2501
+ transformCode = `
2502
+ const { ${destructure} } = params ?? {};
2503
+ const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};`;
2504
+ }
2505
+ } else {
2506
+ transformCode = `
2507
+ const includeSpec = ${JSON.stringify(method.includeSpec)};`;
2508
+ }
2404
2509
  includeMethodsCode += `
2405
2510
  /**
2406
2511
  * Get a ${table.name} record by primary key with included related ${relationshipDesc}
2407
2512
  * @param pk - The primary key value${hasCompositePk ? "s" : ""}
2513
+ * @param params - Optional include options
2408
2514
  * @returns The record with nested ${method.path.join(" and ")} if found, null otherwise
2409
2515
  */
2410
- async ${method.name}(pk: ${pkType}): Promise<${method.returnType}> {
2516
+ async ${method.name}(pk: ${pkType}, params?: ${paramsType}): Promise<${method.returnType}> {${transformCode}
2411
2517
  const results = await this.post<PaginatedResponse<${baseReturnType}>>(\`\${this.resource}/list\`, {
2412
2518
  where: ${pkWhere},
2413
- include: ${JSON.stringify(method.includeSpec)},
2519
+ include: includeSpec,
2414
2520
  limit: 1
2415
2521
  });
2416
2522
  return (results.data[0] as ${baseReturnType}) ?? null;
2417
2523
  }
2418
2524
  `;
2419
2525
  } else {
2526
+ let transformCode = "";
2527
+ if (includeParamNames.length > 0) {
2528
+ const destructure = includeParamNames.map((name) => name).join(", ");
2529
+ if (pattern.type === "single") {
2530
+ const key = pattern.keys[0];
2531
+ const paramName = includeParamNames[0];
2532
+ transformCode = `
2533
+ const { ${destructure}, ...baseParams } = params ?? {};
2534
+ const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};
2535
+ return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...baseParams, include: includeSpec });`;
2536
+ } else if (pattern.type === "parallel") {
2537
+ const includeSpecCode = pattern.keys.map((key, idx) => {
2538
+ const paramName = includeParamNames[idx];
2539
+ return `${key}: ${paramName} ?? true`;
2540
+ }).join(", ");
2541
+ transformCode = `
2542
+ const { ${destructure}, ...baseParams } = params ?? {};
2543
+ const includeSpec = { ${includeSpecCode} };
2544
+ return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...baseParams, include: includeSpec });`;
2545
+ } else if (pattern.type === "nested" && pattern.nestedKey) {
2546
+ const key = pattern.nestedKey;
2547
+ const paramName = includeParamNames[0];
2548
+ transformCode = `
2549
+ const { ${destructure}, ...baseParams } = params ?? {};
2550
+ const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};
2551
+ return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...baseParams, include: includeSpec });`;
2552
+ }
2553
+ } else {
2554
+ transformCode = `
2555
+ return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...params, include: ${JSON.stringify(method.includeSpec)} });`;
2556
+ }
2420
2557
  includeMethodsCode += `
2421
2558
  /**
2422
2559
  * List ${table.name} records with included related ${relationshipDesc}
2423
- * @param params - Query parameters (where, orderBy, order, limit, offset)
2560
+ * @param params - Query parameters (where, orderBy, order, limit, offset) and include options
2424
2561
  * @returns Paginated results with nested ${method.path.join(" and ")} included
2425
2562
  */
2426
- async ${method.name}(${baseParams}): Promise<${method.returnType}> {
2427
- return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...params, include: ${JSON.stringify(method.includeSpec)} });
2563
+ async ${method.name}(params?: ${paramsType}): Promise<${method.returnType}> {${transformCode}
2428
2564
  }
2429
2565
  `;
2430
2566
  }
@@ -2501,7 +2637,7 @@ ${hasJsonbColumns ? ` /**
2501
2637
  * @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
2502
2638
  * @param params.orderBy - Column(s) to sort by
2503
2639
  * @param params.order - Sort direction(s): "asc" or "desc"
2504
- * @param params.limit - Maximum number of records to return (default: 50, max: 100)
2640
+ * @param params.limit - Maximum number of records to return (default: 50, max: 1000)
2505
2641
  * @param params.offset - Number of records to skip for pagination
2506
2642
  * @param params.include - Related records to include (see listWith* methods for typed includes)
2507
2643
  * @returns Paginated results with data, total count, and hasMore flag
@@ -2530,7 +2666,7 @@ ${hasJsonbColumns ? ` /**
2530
2666
  * @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
2531
2667
  * @param params.orderBy - Column(s) to sort by
2532
2668
  * @param params.order - Sort direction(s): "asc" or "desc"
2533
- * @param params.limit - Maximum number of records to return (default: 50, max: 100)
2669
+ * @param params.limit - Maximum number of records to return (default: 50, max: 1000)
2534
2670
  * @param params.offset - Number of records to skip for pagination
2535
2671
  * @param params.include - Related records to include (see listWith* methods for typed includes)
2536
2672
  * @returns Paginated results with data, total count, and hasMore flag
@@ -2867,6 +3003,13 @@ type Graph = typeof RELATION_GRAPH;
2867
3003
  type TableName = keyof Graph;
2868
3004
  type IncludeSpec = any;
2869
3005
 
3006
+ type RelationOptions = {
3007
+ limit?: number;
3008
+ offset?: number;
3009
+ orderBy?: string;
3010
+ order?: "asc" | "desc";
3011
+ };
3012
+
2870
3013
  // Debug helpers (enabled with SDK_DEBUG=1)
2871
3014
  const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
2872
3015
  const log = {
@@ -2968,14 +3111,43 @@ export async function loadIncludes(
2968
3111
  // Safely run each loader; never let one bad edge 500 the route
2969
3112
  if (rel.via) {
2970
3113
  // M:N via junction
3114
+ const specValue = s[key];
3115
+ const options: RelationOptions = {};
3116
+ let childSpec: any = undefined;
3117
+
3118
+ if (specValue && typeof specValue === "object" && specValue !== true) {
3119
+ // Extract options
3120
+ if (specValue.limit !== undefined) options.limit = specValue.limit;
3121
+ if (specValue.offset !== undefined) options.offset = specValue.offset;
3122
+ if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
3123
+ if (specValue.order !== undefined) options.order = specValue.order;
3124
+
3125
+ // Extract nested spec - support both formats:
3126
+ // New: { limit: 3, include: { tags: true } }
3127
+ // Old: { tags: true } (backward compatibility)
3128
+ if (specValue.include !== undefined) {
3129
+ childSpec = specValue.include;
3130
+ } else {
3131
+ // Build childSpec from non-option keys
3132
+ const nonOptionKeys = Object.keys(specValue).filter(
3133
+ k => k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
3134
+ );
3135
+ if (nonOptionKeys.length > 0) {
3136
+ childSpec = {};
3137
+ for (const k of nonOptionKeys) {
3138
+ childSpec[k] = specValue[k];
3139
+ }
3140
+ }
3141
+ }
3142
+ }
3143
+
2971
3144
  try {
2972
- await loadManyToMany(table, target, rel.via as string, rows, key);
3145
+ await loadManyToMany(table, target, rel.via as string, rows, key, options);
2973
3146
  } catch (e: any) {
2974
3147
  log.error("loadManyToMany failed", { table, key, via: rel.via, target }, e?.message ?? e);
2975
3148
  for (const r of rows) r[key] = [];
2976
3149
  }
2977
- // Recurse if nested include specified
2978
- const childSpec = s[key] && typeof s[key] === "object" ? s[key] : undefined;
3150
+
2979
3151
  if (childSpec) {
2980
3152
  const children = rows.flatMap(r => (r[key] ?? []));
2981
3153
  try {
@@ -2989,13 +3161,43 @@ export async function loadIncludes(
2989
3161
 
2990
3162
  if (rel.kind === "many") {
2991
3163
  // 1:N target has FK to current
3164
+ const specValue = s[key];
3165
+ const options: RelationOptions = {};
3166
+ let childSpec: any = undefined;
3167
+
3168
+ if (specValue && typeof specValue === "object" && specValue !== true) {
3169
+ // Extract options
3170
+ if (specValue.limit !== undefined) options.limit = specValue.limit;
3171
+ if (specValue.offset !== undefined) options.offset = specValue.offset;
3172
+ if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
3173
+ if (specValue.order !== undefined) options.order = specValue.order;
3174
+
3175
+ // Extract nested spec - support both formats:
3176
+ // New: { limit: 3, include: { tags: true } }
3177
+ // Old: { tags: true } (backward compatibility)
3178
+ if (specValue.include !== undefined) {
3179
+ childSpec = specValue.include;
3180
+ } else {
3181
+ // Build childSpec from non-option keys
3182
+ const nonOptionKeys = Object.keys(specValue).filter(
3183
+ k => k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
3184
+ );
3185
+ if (nonOptionKeys.length > 0) {
3186
+ childSpec = {};
3187
+ for (const k of nonOptionKeys) {
3188
+ childSpec[k] = specValue[k];
3189
+ }
3190
+ }
3191
+ }
3192
+ }
3193
+
2992
3194
  try {
2993
- await loadOneToMany(table, target, rows, key);
3195
+ await loadOneToMany(table, target, rows, key, options);
2994
3196
  } catch (e: any) {
2995
3197
  log.error("loadOneToMany failed", { table, key, target }, e?.message ?? e);
2996
3198
  for (const r of rows) r[key] = [];
2997
3199
  }
2998
- const childSpec = s[key] && typeof s[key] === "object" ? s[key] : undefined;
3200
+
2999
3201
  if (childSpec) {
3000
3202
  const children = rows.flatMap(r => (r[key] ?? []));
3001
3203
  try {
@@ -3083,7 +3285,7 @@ export async function loadIncludes(
3083
3285
  }
3084
3286
  }
3085
3287
 
3086
- async function loadOneToMany(curr: TableName, target: TableName, rows: any[], key: string) {
3288
+ async function loadOneToMany(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
3087
3289
  // target has FK cols referencing current PK
3088
3290
  const fk = (FK_INDEX as any)[target].find((f: any) => f.toTable === curr);
3089
3291
  if (!fk) { for (const r of rows) r[key] = []; return; }
@@ -3094,18 +3296,50 @@ export async function loadIncludes(
3094
3296
 
3095
3297
  const where = buildOrAndPredicate(fk.from, tuples.length, 1);
3096
3298
  const params = tuples.flat();
3097
- const sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
3098
- log.debug("oneToMany SQL", { curr, target, key, sql, paramsCount: params.length });
3299
+
3300
+ // Build SQL with optional ORDER BY, LIMIT, OFFSET
3301
+ let sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
3302
+
3303
+ // If limit/offset are needed, use window functions to limit per parent
3304
+ if (options.limit !== undefined || options.offset !== undefined) {
3305
+ const orderByClause = options.orderBy
3306
+ ? \`ORDER BY "\${options.orderBy}" \${options.order === 'desc' ? 'DESC' : 'ASC'}\`
3307
+ : 'ORDER BY (SELECT NULL)';
3308
+
3309
+ const partitionCols = fk.from.map((c: string) => \`"\${c}"\`).join(', ');
3310
+ const offset = options.offset ?? 0;
3311
+ const limit = options.limit ?? 999999999;
3312
+
3313
+ sql = \`
3314
+ SELECT * FROM (
3315
+ SELECT *, ROW_NUMBER() OVER (PARTITION BY \${partitionCols} \${orderByClause}) as __rn
3316
+ FROM "\${target}"
3317
+ WHERE \${where}
3318
+ ) __sub
3319
+ WHERE __rn > \${offset} AND __rn <= \${offset + limit}
3320
+ \`;
3321
+ } else if (options.orderBy) {
3322
+ // Just ORDER BY without limit/offset
3323
+ sql += \` ORDER BY "\${options.orderBy}" \${options.order === 'desc' ? 'DESC' : 'ASC'}\`;
3324
+ }
3325
+
3326
+ log.debug("oneToMany SQL", { curr, target, key, sql, paramsCount: params.length, options });
3099
3327
  const { rows: children } = await pg.query(sql, params);
3100
3328
 
3101
- const groups = groupByTuple(children, fk.from);
3329
+ // Remove __rn column if it exists
3330
+ const cleanChildren = children.map((row: any) => {
3331
+ const { __rn, ...rest } = row;
3332
+ return rest;
3333
+ });
3334
+
3335
+ const groups = groupByTuple(cleanChildren, fk.from);
3102
3336
  for (const r of rows) {
3103
3337
  const keyStr = JSON.stringify(pkCols.map((c: string) => r[c]));
3104
3338
  r[key] = groups.get(keyStr) ?? [];
3105
3339
  }
3106
3340
  }
3107
3341
 
3108
- async function loadManyToMany(curr: TableName, target: TableName, via: string, rows: any[], key: string) {
3342
+ async function loadManyToMany(curr: TableName, target: TableName, via: string, rows: any[], key: string, options: RelationOptions = {}) {
3109
3343
  // via has two FKs: one to curr, one to target
3110
3344
  const toCurr = (FK_INDEX as any)[via].find((f: any) => f.toTable === curr);
3111
3345
  const toTarget = (FK_INDEX as any)[via].find((f: any) => f.toTable === target);
@@ -3115,31 +3349,83 @@ export async function loadIncludes(
3115
3349
  const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
3116
3350
  if (!tuples.length) { for (const r of rows) r[key] = []; return; }
3117
3351
 
3118
- // 1) Load junction rows for current parents
3119
3352
  const whereVia = buildOrAndPredicate(toCurr.from, tuples.length, 1);
3120
- const sqlVia = \`SELECT * FROM "\${via}" WHERE \${whereVia}\`;
3121
- const paramsVia = tuples.flat();
3122
- log.debug("manyToMany junction SQL", { curr, target, via, key, sql: sqlVia, paramsCount: paramsVia.length });
3123
- const { rows: jrows } = await pg.query(sqlVia, paramsVia);
3124
-
3125
- if (!jrows.length) { for (const r of rows) r[key] = []; return; }
3353
+ const params = tuples.flat();
3126
3354
 
3127
- // 2) Load targets by distinct target fk tuples in junction
3128
- const tTuples = distinctTuples(jrows, toTarget.from);
3129
- const whereT = buildOrAndPredicate((PKS as any)[target], tTuples.length, 1);
3130
- const sqlT = \`SELECT * FROM "\${target}" WHERE \${whereT}\`;
3131
- const paramsT = tTuples.flat();
3132
- log.debug("manyToMany target SQL", { curr, target, via, key, sql: sqlT, paramsCount: paramsT.length });
3133
- const { rows: targets } = await pg.query(sqlT, paramsT);
3355
+ // If we have limit/offset/orderBy, use a JOIN with window functions
3356
+ if (options.limit !== undefined || options.offset !== undefined || options.orderBy) {
3357
+ const orderByClause = options.orderBy
3358
+ ? \`ORDER BY t."\${options.orderBy}" \${options.order === 'desc' ? 'DESC' : 'ASC'}\`
3359
+ : 'ORDER BY (SELECT NULL)';
3360
+
3361
+ const partitionCols = toCurr.from.map((c: string) => \`j."\${c}"\`).join(', ');
3362
+ const offset = options.offset ?? 0;
3363
+ const limit = options.limit ?? 999999999;
3364
+
3365
+ const targetPkCols = (PKS as any)[target] as string[];
3366
+ const joinConditions = toTarget.from.map((jCol: string, i: number) => {
3367
+ return \`j."\${jCol}" = t."\${targetPkCols[i]}"\`;
3368
+ }).join(' AND ');
3369
+
3370
+ const sql = \`
3371
+ SELECT __numbered.*
3372
+ FROM (
3373
+ SELECT t.*,
3374
+ ROW_NUMBER() OVER (PARTITION BY \${partitionCols} \${orderByClause}) as __rn,
3375
+ \${toCurr.from.map((c: string) => \`j."\${c}"\`).join(' || \\',\\' || ')} as __parent_fk
3376
+ FROM "\${via}" j
3377
+ INNER JOIN "\${target}" t ON \${joinConditions}
3378
+ WHERE \${whereVia}
3379
+ ) __numbered
3380
+ WHERE __numbered.__rn > \${offset} AND __numbered.__rn <= \${offset + limit}
3381
+ \`;
3382
+
3383
+ log.debug("manyToMany SQL with options", { curr, target, via, key, sql, paramsCount: params.length, options });
3384
+ const { rows: results } = await pg.query(sql, params);
3385
+
3386
+ // Clean and group results
3387
+ const cleanResults = results.map((row: any) => {
3388
+ const { __rn, __parent_fk, ...rest } = row;
3389
+ return { ...rest, __parent_fk };
3390
+ });
3134
3391
 
3135
- const tIdx = indexByTuple(targets, (PKS as any)[target]);
3392
+ const grouped = new Map<string, any[]>();
3393
+ for (const row of cleanResults) {
3394
+ const { __parent_fk, ...cleanRow } = row;
3395
+ const arr = grouped.get(__parent_fk) ?? [];
3396
+ arr.push(cleanRow);
3397
+ grouped.set(__parent_fk, arr);
3398
+ }
3136
3399
 
3137
- // 3) Group junction rows by current pk tuple, map to target rows
3138
- const byCurr = groupByTuple(jrows, toCurr.from);
3139
- for (const r of rows) {
3140
- const currKey = JSON.stringify(pkCols.map((c: string) => r[c]));
3141
- const j = byCurr.get(currKey) ?? [];
3142
- r[key] = j.map(jr => tIdx.get(JSON.stringify(toTarget.from.map((c: string) => jr[c])))).filter(Boolean);
3400
+ for (const r of rows) {
3401
+ const currKey = pkCols.map((c: string) => r[c]).join(',');
3402
+ r[key] = grouped.get(currKey) ?? [];
3403
+ }
3404
+ } else {
3405
+ // Original logic without options
3406
+ const sqlVia = \`SELECT * FROM "\${via}" WHERE \${whereVia}\`;
3407
+ log.debug("manyToMany junction SQL", { curr, target, via, key, sql: sqlVia, paramsCount: params.length });
3408
+ const { rows: jrows } = await pg.query(sqlVia, params);
3409
+
3410
+ if (!jrows.length) { for (const r of rows) r[key] = []; return; }
3411
+
3412
+ // 2) Load targets by distinct target fk tuples in junction
3413
+ const tTuples = distinctTuples(jrows, toTarget.from);
3414
+ const whereT = buildOrAndPredicate((PKS as any)[target], tTuples.length, 1);
3415
+ const sqlT = \`SELECT * FROM "\${target}" WHERE \${whereT}\`;
3416
+ const paramsT = tTuples.flat();
3417
+ log.debug("manyToMany target SQL", { curr, target, via, key, sql: sqlT, paramsCount: paramsT.length });
3418
+ const { rows: targets } = await pg.query(sqlT, paramsT);
3419
+
3420
+ const tIdx = indexByTuple(targets, (PKS as any)[target]);
3421
+
3422
+ // 3) Group junction rows by current pk tuple, map to target rows
3423
+ const byCurr = groupByTuple(jrows, toCurr.from);
3424
+ for (const r of rows) {
3425
+ const currKey = JSON.stringify(pkCols.map((c: string) => r[c]));
3426
+ const j = byCurr.get(currKey) ?? [];
3427
+ r[key] = j.map(jr => tIdx.get(JSON.stringify(toTarget.from.map((c: string) => jr[c])))).filter(Boolean);
3428
+ }
3143
3429
  }
3144
3430
  }
3145
3431
  }
@@ -3218,7 +3504,9 @@ ${baseSelectType}
3218
3504
  * @example Insert${Type}<{ metadata: MyMetadataType }>
3219
3505
  */
3220
3506
  export type Insert${Type}<TJsonb extends Partial<_Insert${Type}Base> = {}> =
3221
- Omit<_Insert${Type}Base, keyof TJsonb> & TJsonb;
3507
+ {} extends TJsonb
3508
+ ? _Insert${Type}Base
3509
+ : Omit<_Insert${Type}Base, keyof TJsonb> & TJsonb;
3222
3510
 
3223
3511
  /**
3224
3512
  * Type for updating an existing ${table.name} record.
@@ -3228,7 +3516,9 @@ export type Insert${Type}<TJsonb extends Partial<_Insert${Type}Base> = {}> =
3228
3516
  * @example Update${Type}<{ metadata: MyMetadataType }>
3229
3517
  */
3230
3518
  export type Update${Type}<TJsonb extends Partial<_Select${Type}Base> = {}> =
3231
- Partial<Omit<_Insert${Type}Base, keyof TJsonb> & TJsonb>;
3519
+ {} extends TJsonb
3520
+ ? Partial<_Insert${Type}Base>
3521
+ : Partial<Omit<_Insert${Type}Base, keyof TJsonb> & TJsonb>;
3232
3522
 
3233
3523
  /**
3234
3524
  * Type representing a ${table.name} record from the database.
@@ -3238,7 +3528,9 @@ export type Update${Type}<TJsonb extends Partial<_Select${Type}Base> = {}> =
3238
3528
  * @example Select${Type}<{ metadata: MyMetadataType }>
3239
3529
  */
3240
3530
  export type Select${Type}<TJsonb extends Partial<_Select${Type}Base> = {}> =
3241
- Omit<_Select${Type}Base, keyof TJsonb> & TJsonb;
3531
+ {} extends TJsonb
3532
+ ? _Select${Type}Base
3533
+ : Omit<_Select${Type}Base, keyof TJsonb> & TJsonb;
3242
3534
  `;
3243
3535
  }
3244
3536
  return `/**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.16.9",
3
+ "version": "0.16.12",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "scripts": {
24
24
  "build": "bun build src/cli.ts src/index.ts --outdir dist --target node --format esm --external=pg --external=zod --external=hono --external=prompts --external=node:* && tsc -p tsconfig.build.json --emitDeclarationOnly",
25
- "test": "bun test:init && bun test:gen && bun test test/test-where-clause.test.ts && bun test test/test-where-or-and.test.ts && bun test:gen-with-tests && bun test:pull && bun test:enums && bun test:typecheck && bun test:drizzle-e2e",
25
+ "test": "bun test:init && bun test:gen && bun test test/test-where-clause.test.ts && bun test test/test-where-or-and.test.ts && bun test test/test-nested-include-options.test.ts && bun test test/test-include-methods-with-options.test.ts && bun test:gen-with-tests && bun test:pull && bun test:enums && bun test:typecheck && bun test:drizzle-e2e",
26
26
  "test:init": "bun test/test-init.ts",
27
27
  "test:gen": "bun test/test-gen.ts",
28
28
  "test:gen-with-tests": "bun test/test-gen-with-tests.ts",