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 +52 -0
- package/dist/cli.js +335 -43
- package/dist/index.js +335 -43
- package/package.json +2 -2
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
|
|
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:
|
|
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}(${
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3999
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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}(${
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3098
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|