postgresdk 0.16.10 → 0.16.13
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 +61 -0
- package/dist/cli.js +381 -48
- package/dist/emit-types.d.ts +1 -1
- package/dist/emit-zod.d.ts +1 -1
- package/dist/index.js +348 -47
- package/dist/types.d.ts +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -146,6 +146,7 @@ export default {
|
|
|
146
146
|
schema: "public", // Database schema to introspect
|
|
147
147
|
outDir: "./api", // Output directory (or { client: "./sdk", server: "./api" })
|
|
148
148
|
softDeleteColumn: null, // Column name for soft deletes (e.g., "deleted_at")
|
|
149
|
+
numericMode: "auto", // "auto" | "number" | "string" - How to type numeric columns
|
|
149
150
|
includeMethodsDepth: 2, // Max depth for nested includes
|
|
150
151
|
dateType: "date", // "date" | "string" - How to handle timestamps
|
|
151
152
|
serverFramework: "hono", // Currently only hono is supported
|
|
@@ -175,6 +176,14 @@ export default {
|
|
|
175
176
|
};
|
|
176
177
|
```
|
|
177
178
|
|
|
179
|
+
#### Type Mapping (numericMode)
|
|
180
|
+
|
|
181
|
+
Controls how PostgreSQL numeric types map to TypeScript:
|
|
182
|
+
|
|
183
|
+
- **`"auto"` (default)**: `int2`/`int4`/floats → `number`, `int8`/`numeric` → `string`
|
|
184
|
+
- **`"number"`**: All numeric → `number` (⚠️ unsafe for bigint - JS can't handle values > 2^53)
|
|
185
|
+
- **`"string"`**: All numeric → `string` (safe but requires parsing)
|
|
186
|
+
|
|
178
187
|
### Database Drivers
|
|
179
188
|
|
|
180
189
|
The generated code works with any PostgreSQL client that implements a simple `query` interface:
|
|
@@ -584,6 +593,58 @@ const nestedResult = await sdk.authors.list({
|
|
|
584
593
|
const authorsWithBooksAndTags = nestedResult.data;
|
|
585
594
|
```
|
|
586
595
|
|
|
596
|
+
**Typed Include Methods:**
|
|
597
|
+
|
|
598
|
+
For convenience and better type safety, the SDK generates `listWith*` and `getByPkWith*` methods for common include patterns:
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
// Typed methods provide full autocomplete and type safety
|
|
602
|
+
const result = await sdk.authors.listWithBooks();
|
|
603
|
+
// result.data[0].books is typed as SelectBooks[]
|
|
604
|
+
|
|
605
|
+
// Control included relations with options
|
|
606
|
+
const topAuthors = await sdk.authors.listWithBooks({
|
|
607
|
+
limit: 10,
|
|
608
|
+
booksInclude: {
|
|
609
|
+
orderBy: 'published_at',
|
|
610
|
+
order: 'desc',
|
|
611
|
+
limit: 5 // Only get top 5 books per author
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// Parallel includes (multiple relations at once)
|
|
616
|
+
const result2 = await sdk.books.listWithAuthorAndTags({
|
|
617
|
+
tagsInclude: {
|
|
618
|
+
orderBy: 'name',
|
|
619
|
+
limit: 3
|
|
620
|
+
}
|
|
621
|
+
// author is included automatically (one-to-one)
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// Nested includes with control at each level
|
|
625
|
+
const result3 = await sdk.authors.listWithBooksAndTags({
|
|
626
|
+
booksInclude: {
|
|
627
|
+
orderBy: 'title',
|
|
628
|
+
limit: 10,
|
|
629
|
+
include: {
|
|
630
|
+
tags: {
|
|
631
|
+
orderBy: 'name',
|
|
632
|
+
order: 'asc',
|
|
633
|
+
limit: 5
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// Works with getByPk too
|
|
640
|
+
const author = await sdk.authors.getByPkWithBooks('author-id', {
|
|
641
|
+
booksInclude: {
|
|
642
|
+
orderBy: 'published_at',
|
|
643
|
+
limit: 3
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
```
|
|
647
|
+
|
|
587
648
|
#### Filtering & Pagination
|
|
588
649
|
|
|
589
650
|
All `list()` methods return pagination metadata:
|
package/dist/cli.js
CHANGED
|
@@ -1751,6 +1751,15 @@ function extractConfigFields(configContent) {
|
|
|
1751
1751
|
isCommented: !!depthMatch[1]
|
|
1752
1752
|
});
|
|
1753
1753
|
}
|
|
1754
|
+
const numericModeMatch = configContent.match(/^\s*(\/\/)?\s*numericMode:\s*"(.+)"/m);
|
|
1755
|
+
if (numericModeMatch) {
|
|
1756
|
+
fields.push({
|
|
1757
|
+
key: "numericMode",
|
|
1758
|
+
value: numericModeMatch[2],
|
|
1759
|
+
description: "How to type numeric columns in TypeScript",
|
|
1760
|
+
isCommented: !!numericModeMatch[1]
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1754
1763
|
const frameworkMatch = configContent.match(/^\s*(\/\/)?\s*serverFramework:\s*"(.+)"/m);
|
|
1755
1764
|
if (frameworkMatch) {
|
|
1756
1765
|
fields.push({
|
|
@@ -1918,7 +1927,16 @@ export default {
|
|
|
1918
1927
|
* @example "deleted_at"
|
|
1919
1928
|
*/
|
|
1920
1929
|
${getFieldLine("softDeleteColumn", existingFields, mergeStrategy, "null", userChoices)}
|
|
1921
|
-
|
|
1930
|
+
|
|
1931
|
+
/**
|
|
1932
|
+
* How to type numeric columns in TypeScript
|
|
1933
|
+
* - "auto": int2/int4/float → number, int8/numeric → string (recommended)
|
|
1934
|
+
* - "number": All numeric types become TypeScript number (unsafe for bigint)
|
|
1935
|
+
* - "string": All numeric types become TypeScript string (legacy)
|
|
1936
|
+
* @default "auto"
|
|
1937
|
+
*/
|
|
1938
|
+
${getFieldLine("numericMode", existingFields, mergeStrategy, '"auto"', userChoices)}
|
|
1939
|
+
|
|
1922
1940
|
/**
|
|
1923
1941
|
* Maximum depth for nested relationship includes to prevent infinite loops
|
|
1924
1942
|
* @default 2
|
|
@@ -2347,6 +2365,20 @@ export default {
|
|
|
2347
2365
|
*/
|
|
2348
2366
|
// softDeleteColumn: null,
|
|
2349
2367
|
|
|
2368
|
+
/**
|
|
2369
|
+
* How to type numeric columns in TypeScript
|
|
2370
|
+
* Options:
|
|
2371
|
+
* - "auto": int2/int4/float → number, int8/numeric → string (recommended, default)
|
|
2372
|
+
* - "number": All numeric types become TypeScript number (unsafe for bigint)
|
|
2373
|
+
* - "string": All numeric types become TypeScript string (legacy behavior)
|
|
2374
|
+
*
|
|
2375
|
+
* Auto mode is safest - keeps JavaScript-safe integers as numbers,
|
|
2376
|
+
* but preserves precision for bigint/numeric by using strings.
|
|
2377
|
+
*
|
|
2378
|
+
* Default: "auto"
|
|
2379
|
+
*/
|
|
2380
|
+
// numericMode: "auto",
|
|
2381
|
+
|
|
2350
2382
|
/**
|
|
2351
2383
|
* Maximum depth for nested relationship includes to prevent infinite loops
|
|
2352
2384
|
* Default: 2
|
|
@@ -2810,7 +2842,7 @@ function emitIncludeSpec(graph) {
|
|
|
2810
2842
|
`;
|
|
2811
2843
|
for (const [relKey, edge] of entries) {
|
|
2812
2844
|
if (edge.kind === "many") {
|
|
2813
|
-
out += ` ${relKey}?: boolean | { include?: ${toPascal(edge.target)}IncludeSpec; limit?: number; offset?: number; };
|
|
2845
|
+
out += ` ${relKey}?: boolean | { include?: ${toPascal(edge.target)}IncludeSpec; limit?: number; offset?: number; orderBy?: string; order?: "asc" | "desc"; };
|
|
2814
2846
|
`;
|
|
2815
2847
|
} else {
|
|
2816
2848
|
out += ` ${relKey}?: boolean | ${toPascal(edge.target)}IncludeSpec;
|
|
@@ -2879,10 +2911,20 @@ function emitZod(table, opts, enums) {
|
|
|
2879
2911
|
return `z.string()`;
|
|
2880
2912
|
if (t === "bool" || t === "boolean")
|
|
2881
2913
|
return `z.boolean()`;
|
|
2882
|
-
if (t === "int2" || t === "int4" || t === "int8")
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2914
|
+
if (t === "int2" || t === "int4" || t === "int8") {
|
|
2915
|
+
if (opts.numericMode === "number")
|
|
2916
|
+
return `z.number()`;
|
|
2917
|
+
if (opts.numericMode === "string")
|
|
2918
|
+
return `z.string()`;
|
|
2919
|
+
return t === "int2" || t === "int4" ? `z.number()` : `z.string()`;
|
|
2920
|
+
}
|
|
2921
|
+
if (t === "numeric" || t === "float4" || t === "float8") {
|
|
2922
|
+
if (opts.numericMode === "number")
|
|
2923
|
+
return `z.number()`;
|
|
2924
|
+
if (opts.numericMode === "string")
|
|
2925
|
+
return `z.string()`;
|
|
2926
|
+
return t === "float4" || t === "float8" ? `z.number()` : `z.string()`;
|
|
2927
|
+
}
|
|
2886
2928
|
if (t === "jsonb" || t === "json")
|
|
2887
2929
|
return `z.unknown()`;
|
|
2888
2930
|
if (t === "date" || t.startsWith("timestamp"))
|
|
@@ -3255,6 +3297,24 @@ function isJsonbType(pgType) {
|
|
|
3255
3297
|
const t = pgType.toLowerCase();
|
|
3256
3298
|
return t === "json" || t === "jsonb";
|
|
3257
3299
|
}
|
|
3300
|
+
function toIncludeParamName(relationKey) {
|
|
3301
|
+
return `${relationKey}Include`;
|
|
3302
|
+
}
|
|
3303
|
+
function analyzeIncludeSpec(includeSpec) {
|
|
3304
|
+
const keys = Object.keys(includeSpec);
|
|
3305
|
+
if (keys.length > 1) {
|
|
3306
|
+
return { type: "parallel", keys };
|
|
3307
|
+
}
|
|
3308
|
+
const key = keys[0];
|
|
3309
|
+
if (!key) {
|
|
3310
|
+
return { type: "single", keys: [] };
|
|
3311
|
+
}
|
|
3312
|
+
const value = includeSpec[key];
|
|
3313
|
+
if (typeof value === "object" && value !== null) {
|
|
3314
|
+
return { type: "nested", keys: [key], nestedKey: key, nestedValue: value };
|
|
3315
|
+
}
|
|
3316
|
+
return { type: "single", keys: [key] };
|
|
3317
|
+
}
|
|
3258
3318
|
function emitClient(table, graph, opts, model) {
|
|
3259
3319
|
const Type = pascal(table.name);
|
|
3260
3320
|
const ext = opts.useJsExtensions ? ".js" : "";
|
|
@@ -3293,39 +3353,157 @@ function emitClient(table, graph, opts, model) {
|
|
|
3293
3353
|
let includeMethodsCode = "";
|
|
3294
3354
|
for (const method of includeMethods) {
|
|
3295
3355
|
const isGetByPk = method.name.startsWith("getByPk");
|
|
3296
|
-
const
|
|
3356
|
+
const pattern = analyzeIncludeSpec(method.includeSpec);
|
|
3297
3357
|
const relationshipDesc = method.path.map((p, i) => {
|
|
3298
3358
|
const isLast = i === method.path.length - 1;
|
|
3299
3359
|
const relation = method.isMany[i] ? "many" : "one";
|
|
3300
3360
|
return isLast ? p : `${p} -> `;
|
|
3301
3361
|
}).join("");
|
|
3362
|
+
let paramsType = "";
|
|
3363
|
+
const includeParamNames = [];
|
|
3364
|
+
if (pattern.type === "single") {
|
|
3365
|
+
const key = pattern.keys[0];
|
|
3366
|
+
if (key) {
|
|
3367
|
+
const paramName = toIncludeParamName(key);
|
|
3368
|
+
includeParamNames.push(paramName);
|
|
3369
|
+
paramsType = `{
|
|
3370
|
+
limit?: number;
|
|
3371
|
+
offset?: number;
|
|
3372
|
+
where?: Where<Select${Type}>;
|
|
3373
|
+
orderBy?: string | string[];
|
|
3374
|
+
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3375
|
+
${paramName}?: {
|
|
3376
|
+
orderBy?: string | string[];
|
|
3377
|
+
order?: "asc" | "desc";
|
|
3378
|
+
limit?: number;
|
|
3379
|
+
offset?: number;
|
|
3380
|
+
};
|
|
3381
|
+
}`;
|
|
3382
|
+
}
|
|
3383
|
+
} else if (pattern.type === "parallel") {
|
|
3384
|
+
const includeParams = pattern.keys.map((key) => {
|
|
3385
|
+
const paramName = toIncludeParamName(key);
|
|
3386
|
+
includeParamNames.push(paramName);
|
|
3387
|
+
return `${paramName}?: {
|
|
3388
|
+
orderBy?: string | string[];
|
|
3389
|
+
order?: "asc" | "desc";
|
|
3390
|
+
limit?: number;
|
|
3391
|
+
offset?: number;
|
|
3392
|
+
}`;
|
|
3393
|
+
}).join(`;
|
|
3394
|
+
`);
|
|
3395
|
+
paramsType = `{
|
|
3396
|
+
limit?: number;
|
|
3397
|
+
offset?: number;
|
|
3398
|
+
where?: Where<Select${Type}>;
|
|
3399
|
+
orderBy?: string | string[];
|
|
3400
|
+
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3401
|
+
${includeParams};
|
|
3402
|
+
}`;
|
|
3403
|
+
} else if (pattern.type === "nested" && pattern.nestedKey) {
|
|
3404
|
+
const paramName = toIncludeParamName(pattern.nestedKey);
|
|
3405
|
+
includeParamNames.push(paramName);
|
|
3406
|
+
paramsType = `{
|
|
3407
|
+
limit?: number;
|
|
3408
|
+
offset?: number;
|
|
3409
|
+
where?: Where<Select${Type}>;
|
|
3410
|
+
orderBy?: string | string[];
|
|
3411
|
+
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3412
|
+
${paramName}?: {
|
|
3413
|
+
orderBy?: string | string[];
|
|
3414
|
+
order?: "asc" | "desc";
|
|
3415
|
+
limit?: number;
|
|
3416
|
+
offset?: number;
|
|
3417
|
+
include?: any;
|
|
3418
|
+
};
|
|
3419
|
+
}`;
|
|
3420
|
+
}
|
|
3302
3421
|
if (isGetByPk) {
|
|
3303
3422
|
const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
|
|
3304
3423
|
const baseReturnType = method.returnType.replace(" | null", "");
|
|
3424
|
+
let transformCode = "";
|
|
3425
|
+
if (includeParamNames.length > 0) {
|
|
3426
|
+
const destructure = includeParamNames.map((name) => name).join(", ");
|
|
3427
|
+
if (pattern.type === "single") {
|
|
3428
|
+
const key = pattern.keys[0];
|
|
3429
|
+
const paramName = includeParamNames[0];
|
|
3430
|
+
transformCode = `
|
|
3431
|
+
const { ${destructure} } = params ?? {};
|
|
3432
|
+
const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};`;
|
|
3433
|
+
} else if (pattern.type === "parallel") {
|
|
3434
|
+
const includeSpecCode = pattern.keys.map((key, idx) => {
|
|
3435
|
+
const paramName = includeParamNames[idx];
|
|
3436
|
+
return `${key}: ${paramName} ?? true`;
|
|
3437
|
+
}).join(", ");
|
|
3438
|
+
transformCode = `
|
|
3439
|
+
const { ${destructure} } = params ?? {};
|
|
3440
|
+
const includeSpec = { ${includeSpecCode} };`;
|
|
3441
|
+
} else if (pattern.type === "nested" && pattern.nestedKey) {
|
|
3442
|
+
const key = pattern.nestedKey;
|
|
3443
|
+
const paramName = includeParamNames[0];
|
|
3444
|
+
transformCode = `
|
|
3445
|
+
const { ${destructure} } = params ?? {};
|
|
3446
|
+
const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};`;
|
|
3447
|
+
}
|
|
3448
|
+
} else {
|
|
3449
|
+
transformCode = `
|
|
3450
|
+
const includeSpec = ${JSON.stringify(method.includeSpec)};`;
|
|
3451
|
+
}
|
|
3305
3452
|
includeMethodsCode += `
|
|
3306
3453
|
/**
|
|
3307
3454
|
* Get a ${table.name} record by primary key with included related ${relationshipDesc}
|
|
3308
3455
|
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3456
|
+
* @param params - Optional include options
|
|
3309
3457
|
* @returns The record with nested ${method.path.join(" and ")} if found, null otherwise
|
|
3310
3458
|
*/
|
|
3311
|
-
async ${method.name}(pk: ${pkType}): Promise<${method.returnType}> {
|
|
3459
|
+
async ${method.name}(pk: ${pkType}, params?: ${paramsType}): Promise<${method.returnType}> {${transformCode}
|
|
3312
3460
|
const results = await this.post<PaginatedResponse<${baseReturnType}>>(\`\${this.resource}/list\`, {
|
|
3313
3461
|
where: ${pkWhere},
|
|
3314
|
-
include:
|
|
3462
|
+
include: includeSpec,
|
|
3315
3463
|
limit: 1
|
|
3316
3464
|
});
|
|
3317
3465
|
return (results.data[0] as ${baseReturnType}) ?? null;
|
|
3318
3466
|
}
|
|
3319
3467
|
`;
|
|
3320
3468
|
} else {
|
|
3469
|
+
let transformCode = "";
|
|
3470
|
+
if (includeParamNames.length > 0) {
|
|
3471
|
+
const destructure = includeParamNames.map((name) => name).join(", ");
|
|
3472
|
+
if (pattern.type === "single") {
|
|
3473
|
+
const key = pattern.keys[0];
|
|
3474
|
+
const paramName = includeParamNames[0];
|
|
3475
|
+
transformCode = `
|
|
3476
|
+
const { ${destructure}, ...baseParams } = params ?? {};
|
|
3477
|
+
const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};
|
|
3478
|
+
return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...baseParams, include: includeSpec });`;
|
|
3479
|
+
} else if (pattern.type === "parallel") {
|
|
3480
|
+
const includeSpecCode = pattern.keys.map((key, idx) => {
|
|
3481
|
+
const paramName = includeParamNames[idx];
|
|
3482
|
+
return `${key}: ${paramName} ?? true`;
|
|
3483
|
+
}).join(", ");
|
|
3484
|
+
transformCode = `
|
|
3485
|
+
const { ${destructure}, ...baseParams } = params ?? {};
|
|
3486
|
+
const includeSpec = { ${includeSpecCode} };
|
|
3487
|
+
return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...baseParams, include: includeSpec });`;
|
|
3488
|
+
} else if (pattern.type === "nested" && pattern.nestedKey) {
|
|
3489
|
+
const key = pattern.nestedKey;
|
|
3490
|
+
const paramName = includeParamNames[0];
|
|
3491
|
+
transformCode = `
|
|
3492
|
+
const { ${destructure}, ...baseParams } = params ?? {};
|
|
3493
|
+
const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};
|
|
3494
|
+
return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...baseParams, include: includeSpec });`;
|
|
3495
|
+
}
|
|
3496
|
+
} else {
|
|
3497
|
+
transformCode = `
|
|
3498
|
+
return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...params, include: ${JSON.stringify(method.includeSpec)} });`;
|
|
3499
|
+
}
|
|
3321
3500
|
includeMethodsCode += `
|
|
3322
3501
|
/**
|
|
3323
3502
|
* List ${table.name} records with included related ${relationshipDesc}
|
|
3324
|
-
* @param params - Query parameters (where, orderBy, order, limit, offset)
|
|
3503
|
+
* @param params - Query parameters (where, orderBy, order, limit, offset) and include options
|
|
3325
3504
|
* @returns Paginated results with nested ${method.path.join(" and ")} included
|
|
3326
3505
|
*/
|
|
3327
|
-
async ${method.name}(${
|
|
3328
|
-
return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...params, include: ${JSON.stringify(method.includeSpec)} });
|
|
3506
|
+
async ${method.name}(params?: ${paramsType}): Promise<${method.returnType}> {${transformCode}
|
|
3329
3507
|
}
|
|
3330
3508
|
`;
|
|
3331
3509
|
}
|
|
@@ -3402,7 +3580,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
3402
3580
|
* @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
|
|
3403
3581
|
* @param params.orderBy - Column(s) to sort by
|
|
3404
3582
|
* @param params.order - Sort direction(s): "asc" or "desc"
|
|
3405
|
-
* @param params.limit - Maximum number of records to return (default: 50, max:
|
|
3583
|
+
* @param params.limit - Maximum number of records to return (default: 50, max: 1000)
|
|
3406
3584
|
* @param params.offset - Number of records to skip for pagination
|
|
3407
3585
|
* @param params.include - Related records to include (see listWith* methods for typed includes)
|
|
3408
3586
|
* @returns Paginated results with data, total count, and hasMore flag
|
|
@@ -3431,7 +3609,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
3431
3609
|
* @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
|
|
3432
3610
|
* @param params.orderBy - Column(s) to sort by
|
|
3433
3611
|
* @param params.order - Sort direction(s): "asc" or "desc"
|
|
3434
|
-
* @param params.limit - Maximum number of records to return (default: 50, max:
|
|
3612
|
+
* @param params.limit - Maximum number of records to return (default: 50, max: 1000)
|
|
3435
3613
|
* @param params.offset - Number of records to skip for pagination
|
|
3436
3614
|
* @param params.include - Related records to include (see listWith* methods for typed includes)
|
|
3437
3615
|
* @returns Paginated results with data, total count, and hasMore flag
|
|
@@ -3768,6 +3946,13 @@ type Graph = typeof RELATION_GRAPH;
|
|
|
3768
3946
|
type TableName = keyof Graph;
|
|
3769
3947
|
type IncludeSpec = any;
|
|
3770
3948
|
|
|
3949
|
+
type RelationOptions = {
|
|
3950
|
+
limit?: number;
|
|
3951
|
+
offset?: number;
|
|
3952
|
+
orderBy?: string;
|
|
3953
|
+
order?: "asc" | "desc";
|
|
3954
|
+
};
|
|
3955
|
+
|
|
3771
3956
|
// Debug helpers (enabled with SDK_DEBUG=1)
|
|
3772
3957
|
const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
|
|
3773
3958
|
const log = {
|
|
@@ -3869,14 +4054,43 @@ export async function loadIncludes(
|
|
|
3869
4054
|
// Safely run each loader; never let one bad edge 500 the route
|
|
3870
4055
|
if (rel.via) {
|
|
3871
4056
|
// M:N via junction
|
|
4057
|
+
const specValue = s[key];
|
|
4058
|
+
const options: RelationOptions = {};
|
|
4059
|
+
let childSpec: any = undefined;
|
|
4060
|
+
|
|
4061
|
+
if (specValue && typeof specValue === "object" && specValue !== true) {
|
|
4062
|
+
// Extract options
|
|
4063
|
+
if (specValue.limit !== undefined) options.limit = specValue.limit;
|
|
4064
|
+
if (specValue.offset !== undefined) options.offset = specValue.offset;
|
|
4065
|
+
if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
|
|
4066
|
+
if (specValue.order !== undefined) options.order = specValue.order;
|
|
4067
|
+
|
|
4068
|
+
// Extract nested spec - support both formats:
|
|
4069
|
+
// New: { limit: 3, include: { tags: true } }
|
|
4070
|
+
// Old: { tags: true } (backward compatibility)
|
|
4071
|
+
if (specValue.include !== undefined) {
|
|
4072
|
+
childSpec = specValue.include;
|
|
4073
|
+
} else {
|
|
4074
|
+
// Build childSpec from non-option keys
|
|
4075
|
+
const nonOptionKeys = Object.keys(specValue).filter(
|
|
4076
|
+
k => k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
|
|
4077
|
+
);
|
|
4078
|
+
if (nonOptionKeys.length > 0) {
|
|
4079
|
+
childSpec = {};
|
|
4080
|
+
for (const k of nonOptionKeys) {
|
|
4081
|
+
childSpec[k] = specValue[k];
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
|
|
3872
4087
|
try {
|
|
3873
|
-
await loadManyToMany(table, target, rel.via as string, rows, key);
|
|
4088
|
+
await loadManyToMany(table, target, rel.via as string, rows, key, options);
|
|
3874
4089
|
} catch (e: any) {
|
|
3875
4090
|
log.error("loadManyToMany failed", { table, key, via: rel.via, target }, e?.message ?? e);
|
|
3876
4091
|
for (const r of rows) r[key] = [];
|
|
3877
4092
|
}
|
|
3878
|
-
|
|
3879
|
-
const childSpec = s[key] && typeof s[key] === "object" ? s[key] : undefined;
|
|
4093
|
+
|
|
3880
4094
|
if (childSpec) {
|
|
3881
4095
|
const children = rows.flatMap(r => (r[key] ?? []));
|
|
3882
4096
|
try {
|
|
@@ -3890,13 +4104,43 @@ export async function loadIncludes(
|
|
|
3890
4104
|
|
|
3891
4105
|
if (rel.kind === "many") {
|
|
3892
4106
|
// 1:N target has FK to current
|
|
4107
|
+
const specValue = s[key];
|
|
4108
|
+
const options: RelationOptions = {};
|
|
4109
|
+
let childSpec: any = undefined;
|
|
4110
|
+
|
|
4111
|
+
if (specValue && typeof specValue === "object" && specValue !== true) {
|
|
4112
|
+
// Extract options
|
|
4113
|
+
if (specValue.limit !== undefined) options.limit = specValue.limit;
|
|
4114
|
+
if (specValue.offset !== undefined) options.offset = specValue.offset;
|
|
4115
|
+
if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
|
|
4116
|
+
if (specValue.order !== undefined) options.order = specValue.order;
|
|
4117
|
+
|
|
4118
|
+
// Extract nested spec - support both formats:
|
|
4119
|
+
// New: { limit: 3, include: { tags: true } }
|
|
4120
|
+
// Old: { tags: true } (backward compatibility)
|
|
4121
|
+
if (specValue.include !== undefined) {
|
|
4122
|
+
childSpec = specValue.include;
|
|
4123
|
+
} else {
|
|
4124
|
+
// Build childSpec from non-option keys
|
|
4125
|
+
const nonOptionKeys = Object.keys(specValue).filter(
|
|
4126
|
+
k => k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
|
|
4127
|
+
);
|
|
4128
|
+
if (nonOptionKeys.length > 0) {
|
|
4129
|
+
childSpec = {};
|
|
4130
|
+
for (const k of nonOptionKeys) {
|
|
4131
|
+
childSpec[k] = specValue[k];
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
|
|
3893
4137
|
try {
|
|
3894
|
-
await loadOneToMany(table, target, rows, key);
|
|
4138
|
+
await loadOneToMany(table, target, rows, key, options);
|
|
3895
4139
|
} catch (e: any) {
|
|
3896
4140
|
log.error("loadOneToMany failed", { table, key, target }, e?.message ?? e);
|
|
3897
4141
|
for (const r of rows) r[key] = [];
|
|
3898
4142
|
}
|
|
3899
|
-
|
|
4143
|
+
|
|
3900
4144
|
if (childSpec) {
|
|
3901
4145
|
const children = rows.flatMap(r => (r[key] ?? []));
|
|
3902
4146
|
try {
|
|
@@ -3984,7 +4228,7 @@ export async function loadIncludes(
|
|
|
3984
4228
|
}
|
|
3985
4229
|
}
|
|
3986
4230
|
|
|
3987
|
-
async function loadOneToMany(curr: TableName, target: TableName, rows: any[], key: string) {
|
|
4231
|
+
async function loadOneToMany(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
|
|
3988
4232
|
// target has FK cols referencing current PK
|
|
3989
4233
|
const fk = (FK_INDEX as any)[target].find((f: any) => f.toTable === curr);
|
|
3990
4234
|
if (!fk) { for (const r of rows) r[key] = []; return; }
|
|
@@ -3995,18 +4239,50 @@ export async function loadIncludes(
|
|
|
3995
4239
|
|
|
3996
4240
|
const where = buildOrAndPredicate(fk.from, tuples.length, 1);
|
|
3997
4241
|
const params = tuples.flat();
|
|
3998
|
-
|
|
3999
|
-
|
|
4242
|
+
|
|
4243
|
+
// Build SQL with optional ORDER BY, LIMIT, OFFSET
|
|
4244
|
+
let sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
|
|
4245
|
+
|
|
4246
|
+
// If limit/offset are needed, use window functions to limit per parent
|
|
4247
|
+
if (options.limit !== undefined || options.offset !== undefined) {
|
|
4248
|
+
const orderByClause = options.orderBy
|
|
4249
|
+
? \`ORDER BY "\${options.orderBy}" \${options.order === 'desc' ? 'DESC' : 'ASC'}\`
|
|
4250
|
+
: 'ORDER BY (SELECT NULL)';
|
|
4251
|
+
|
|
4252
|
+
const partitionCols = fk.from.map((c: string) => \`"\${c}"\`).join(', ');
|
|
4253
|
+
const offset = options.offset ?? 0;
|
|
4254
|
+
const limit = options.limit ?? 999999999;
|
|
4255
|
+
|
|
4256
|
+
sql = \`
|
|
4257
|
+
SELECT * FROM (
|
|
4258
|
+
SELECT *, ROW_NUMBER() OVER (PARTITION BY \${partitionCols} \${orderByClause}) as __rn
|
|
4259
|
+
FROM "\${target}"
|
|
4260
|
+
WHERE \${where}
|
|
4261
|
+
) __sub
|
|
4262
|
+
WHERE __rn > \${offset} AND __rn <= \${offset + limit}
|
|
4263
|
+
\`;
|
|
4264
|
+
} else if (options.orderBy) {
|
|
4265
|
+
// Just ORDER BY without limit/offset
|
|
4266
|
+
sql += \` ORDER BY "\${options.orderBy}" \${options.order === 'desc' ? 'DESC' : 'ASC'}\`;
|
|
4267
|
+
}
|
|
4268
|
+
|
|
4269
|
+
log.debug("oneToMany SQL", { curr, target, key, sql, paramsCount: params.length, options });
|
|
4000
4270
|
const { rows: children } = await pg.query(sql, params);
|
|
4001
4271
|
|
|
4002
|
-
|
|
4272
|
+
// Remove __rn column if it exists
|
|
4273
|
+
const cleanChildren = children.map((row: any) => {
|
|
4274
|
+
const { __rn, ...rest } = row;
|
|
4275
|
+
return rest;
|
|
4276
|
+
});
|
|
4277
|
+
|
|
4278
|
+
const groups = groupByTuple(cleanChildren, fk.from);
|
|
4003
4279
|
for (const r of rows) {
|
|
4004
4280
|
const keyStr = JSON.stringify(pkCols.map((c: string) => r[c]));
|
|
4005
4281
|
r[key] = groups.get(keyStr) ?? [];
|
|
4006
4282
|
}
|
|
4007
4283
|
}
|
|
4008
4284
|
|
|
4009
|
-
async function loadManyToMany(curr: TableName, target: TableName, via: string, rows: any[], key: string) {
|
|
4285
|
+
async function loadManyToMany(curr: TableName, target: TableName, via: string, rows: any[], key: string, options: RelationOptions = {}) {
|
|
4010
4286
|
// via has two FKs: one to curr, one to target
|
|
4011
4287
|
const toCurr = (FK_INDEX as any)[via].find((f: any) => f.toTable === curr);
|
|
4012
4288
|
const toTarget = (FK_INDEX as any)[via].find((f: any) => f.toTable === target);
|
|
@@ -4016,31 +4292,83 @@ export async function loadIncludes(
|
|
|
4016
4292
|
const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
|
|
4017
4293
|
if (!tuples.length) { for (const r of rows) r[key] = []; return; }
|
|
4018
4294
|
|
|
4019
|
-
// 1) Load junction rows for current parents
|
|
4020
4295
|
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; }
|
|
4296
|
+
const params = tuples.flat();
|
|
4027
4297
|
|
|
4028
|
-
//
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4298
|
+
// If we have limit/offset/orderBy, use a JOIN with window functions
|
|
4299
|
+
if (options.limit !== undefined || options.offset !== undefined || options.orderBy) {
|
|
4300
|
+
const orderByClause = options.orderBy
|
|
4301
|
+
? \`ORDER BY t."\${options.orderBy}" \${options.order === 'desc' ? 'DESC' : 'ASC'}\`
|
|
4302
|
+
: 'ORDER BY (SELECT NULL)';
|
|
4303
|
+
|
|
4304
|
+
const partitionCols = toCurr.from.map((c: string) => \`j."\${c}"\`).join(', ');
|
|
4305
|
+
const offset = options.offset ?? 0;
|
|
4306
|
+
const limit = options.limit ?? 999999999;
|
|
4307
|
+
|
|
4308
|
+
const targetPkCols = (PKS as any)[target] as string[];
|
|
4309
|
+
const joinConditions = toTarget.from.map((jCol: string, i: number) => {
|
|
4310
|
+
return \`j."\${jCol}" = t."\${targetPkCols[i]}"\`;
|
|
4311
|
+
}).join(' AND ');
|
|
4312
|
+
|
|
4313
|
+
const sql = \`
|
|
4314
|
+
SELECT __numbered.*
|
|
4315
|
+
FROM (
|
|
4316
|
+
SELECT t.*,
|
|
4317
|
+
ROW_NUMBER() OVER (PARTITION BY \${partitionCols} \${orderByClause}) as __rn,
|
|
4318
|
+
\${toCurr.from.map((c: string) => \`j."\${c}"\`).join(' || \\',\\' || ')} as __parent_fk
|
|
4319
|
+
FROM "\${via}" j
|
|
4320
|
+
INNER JOIN "\${target}" t ON \${joinConditions}
|
|
4321
|
+
WHERE \${whereVia}
|
|
4322
|
+
) __numbered
|
|
4323
|
+
WHERE __numbered.__rn > \${offset} AND __numbered.__rn <= \${offset + limit}
|
|
4324
|
+
\`;
|
|
4325
|
+
|
|
4326
|
+
log.debug("manyToMany SQL with options", { curr, target, via, key, sql, paramsCount: params.length, options });
|
|
4327
|
+
const { rows: results } = await pg.query(sql, params);
|
|
4328
|
+
|
|
4329
|
+
// Clean and group results
|
|
4330
|
+
const cleanResults = results.map((row: any) => {
|
|
4331
|
+
const { __rn, __parent_fk, ...rest } = row;
|
|
4332
|
+
return { ...rest, __parent_fk };
|
|
4333
|
+
});
|
|
4035
4334
|
|
|
4036
|
-
|
|
4335
|
+
const grouped = new Map<string, any[]>();
|
|
4336
|
+
for (const row of cleanResults) {
|
|
4337
|
+
const { __parent_fk, ...cleanRow } = row;
|
|
4338
|
+
const arr = grouped.get(__parent_fk) ?? [];
|
|
4339
|
+
arr.push(cleanRow);
|
|
4340
|
+
grouped.set(__parent_fk, arr);
|
|
4341
|
+
}
|
|
4037
4342
|
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4343
|
+
for (const r of rows) {
|
|
4344
|
+
const currKey = pkCols.map((c: string) => r[c]).join(',');
|
|
4345
|
+
r[key] = grouped.get(currKey) ?? [];
|
|
4346
|
+
}
|
|
4347
|
+
} else {
|
|
4348
|
+
// Original logic without options
|
|
4349
|
+
const sqlVia = \`SELECT * FROM "\${via}" WHERE \${whereVia}\`;
|
|
4350
|
+
log.debug("manyToMany junction SQL", { curr, target, via, key, sql: sqlVia, paramsCount: params.length });
|
|
4351
|
+
const { rows: jrows } = await pg.query(sqlVia, params);
|
|
4352
|
+
|
|
4353
|
+
if (!jrows.length) { for (const r of rows) r[key] = []; return; }
|
|
4354
|
+
|
|
4355
|
+
// 2) Load targets by distinct target fk tuples in junction
|
|
4356
|
+
const tTuples = distinctTuples(jrows, toTarget.from);
|
|
4357
|
+
const whereT = buildOrAndPredicate((PKS as any)[target], tTuples.length, 1);
|
|
4358
|
+
const sqlT = \`SELECT * FROM "\${target}" WHERE \${whereT}\`;
|
|
4359
|
+
const paramsT = tTuples.flat();
|
|
4360
|
+
log.debug("manyToMany target SQL", { curr, target, via, key, sql: sqlT, paramsCount: paramsT.length });
|
|
4361
|
+
const { rows: targets } = await pg.query(sqlT, paramsT);
|
|
4362
|
+
|
|
4363
|
+
const tIdx = indexByTuple(targets, (PKS as any)[target]);
|
|
4364
|
+
|
|
4365
|
+
// 3) Group junction rows by current pk tuple, map to target rows
|
|
4366
|
+
const byCurr = groupByTuple(jrows, toCurr.from);
|
|
4367
|
+
for (const r of rows) {
|
|
4368
|
+
const currKey = JSON.stringify(pkCols.map((c: string) => r[c]));
|
|
4369
|
+
const j = byCurr.get(currKey) ?? [];
|
|
4370
|
+
r[key] = j.map(jr => tIdx.get(JSON.stringify(toTarget.from.map((c: string) => jr[c])))).filter(Boolean);
|
|
4371
|
+
}
|
|
4044
4372
|
}
|
|
4045
4373
|
}
|
|
4046
4374
|
}
|
|
@@ -4060,7 +4388,11 @@ function tsTypeFor(pgType, opts, enums) {
|
|
|
4060
4388
|
if (t === "bool" || t === "boolean")
|
|
4061
4389
|
return "boolean";
|
|
4062
4390
|
if (t === "int2" || t === "int4" || t === "int8" || t === "float4" || t === "float8" || t === "numeric") {
|
|
4063
|
-
|
|
4391
|
+
if (opts.numericMode === "number")
|
|
4392
|
+
return "number";
|
|
4393
|
+
if (opts.numericMode === "string")
|
|
4394
|
+
return "string";
|
|
4395
|
+
return t === "int2" || t === "int4" || t === "float4" || t === "float8" ? "number" : "string";
|
|
4064
4396
|
}
|
|
4065
4397
|
if (t === "date" || t.startsWith("timestamp"))
|
|
4066
4398
|
return "string";
|
|
@@ -6265,10 +6597,11 @@ async function generate(configPath) {
|
|
|
6265
6597
|
console.log(`[Index] About to process ${Object.keys(model.tables || {}).length} tables for generation`);
|
|
6266
6598
|
}
|
|
6267
6599
|
for (const table of Object.values(model.tables)) {
|
|
6268
|
-
const
|
|
6600
|
+
const numericMode = cfg.numericMode ?? "auto";
|
|
6601
|
+
const typesSrc = emitTypes(table, { numericMode }, model.enums);
|
|
6269
6602
|
files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
6270
6603
|
files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
6271
|
-
const zodSrc = emitZod(table, { numericMode
|
|
6604
|
+
const zodSrc = emitZod(table, { numericMode }, model.enums);
|
|
6272
6605
|
files.push({ path: join(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
6273
6606
|
files.push({ path: join(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
6274
6607
|
const paramsZodSrc = emitParamsZod(table, graph);
|
package/dist/emit-types.d.ts
CHANGED
package/dist/emit-zod.d.ts
CHANGED
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;
|
|
@@ -1978,10 +1978,20 @@ function emitZod(table, opts, enums) {
|
|
|
1978
1978
|
return `z.string()`;
|
|
1979
1979
|
if (t === "bool" || t === "boolean")
|
|
1980
1980
|
return `z.boolean()`;
|
|
1981
|
-
if (t === "int2" || t === "int4" || t === "int8")
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1981
|
+
if (t === "int2" || t === "int4" || t === "int8") {
|
|
1982
|
+
if (opts.numericMode === "number")
|
|
1983
|
+
return `z.number()`;
|
|
1984
|
+
if (opts.numericMode === "string")
|
|
1985
|
+
return `z.string()`;
|
|
1986
|
+
return t === "int2" || t === "int4" ? `z.number()` : `z.string()`;
|
|
1987
|
+
}
|
|
1988
|
+
if (t === "numeric" || t === "float4" || t === "float8") {
|
|
1989
|
+
if (opts.numericMode === "number")
|
|
1990
|
+
return `z.number()`;
|
|
1991
|
+
if (opts.numericMode === "string")
|
|
1992
|
+
return `z.string()`;
|
|
1993
|
+
return t === "float4" || t === "float8" ? `z.number()` : `z.string()`;
|
|
1994
|
+
}
|
|
1985
1995
|
if (t === "jsonb" || t === "json")
|
|
1986
1996
|
return `z.unknown()`;
|
|
1987
1997
|
if (t === "date" || t.startsWith("timestamp"))
|
|
@@ -2354,6 +2364,24 @@ function isJsonbType(pgType) {
|
|
|
2354
2364
|
const t = pgType.toLowerCase();
|
|
2355
2365
|
return t === "json" || t === "jsonb";
|
|
2356
2366
|
}
|
|
2367
|
+
function toIncludeParamName(relationKey) {
|
|
2368
|
+
return `${relationKey}Include`;
|
|
2369
|
+
}
|
|
2370
|
+
function analyzeIncludeSpec(includeSpec) {
|
|
2371
|
+
const keys = Object.keys(includeSpec);
|
|
2372
|
+
if (keys.length > 1) {
|
|
2373
|
+
return { type: "parallel", keys };
|
|
2374
|
+
}
|
|
2375
|
+
const key = keys[0];
|
|
2376
|
+
if (!key) {
|
|
2377
|
+
return { type: "single", keys: [] };
|
|
2378
|
+
}
|
|
2379
|
+
const value = includeSpec[key];
|
|
2380
|
+
if (typeof value === "object" && value !== null) {
|
|
2381
|
+
return { type: "nested", keys: [key], nestedKey: key, nestedValue: value };
|
|
2382
|
+
}
|
|
2383
|
+
return { type: "single", keys: [key] };
|
|
2384
|
+
}
|
|
2357
2385
|
function emitClient(table, graph, opts, model) {
|
|
2358
2386
|
const Type = pascal(table.name);
|
|
2359
2387
|
const ext = opts.useJsExtensions ? ".js" : "";
|
|
@@ -2392,39 +2420,157 @@ function emitClient(table, graph, opts, model) {
|
|
|
2392
2420
|
let includeMethodsCode = "";
|
|
2393
2421
|
for (const method of includeMethods) {
|
|
2394
2422
|
const isGetByPk = method.name.startsWith("getByPk");
|
|
2395
|
-
const
|
|
2423
|
+
const pattern = analyzeIncludeSpec(method.includeSpec);
|
|
2396
2424
|
const relationshipDesc = method.path.map((p, i) => {
|
|
2397
2425
|
const isLast = i === method.path.length - 1;
|
|
2398
2426
|
const relation = method.isMany[i] ? "many" : "one";
|
|
2399
2427
|
return isLast ? p : `${p} -> `;
|
|
2400
2428
|
}).join("");
|
|
2429
|
+
let paramsType = "";
|
|
2430
|
+
const includeParamNames = [];
|
|
2431
|
+
if (pattern.type === "single") {
|
|
2432
|
+
const key = pattern.keys[0];
|
|
2433
|
+
if (key) {
|
|
2434
|
+
const paramName = toIncludeParamName(key);
|
|
2435
|
+
includeParamNames.push(paramName);
|
|
2436
|
+
paramsType = `{
|
|
2437
|
+
limit?: number;
|
|
2438
|
+
offset?: number;
|
|
2439
|
+
where?: Where<Select${Type}>;
|
|
2440
|
+
orderBy?: string | string[];
|
|
2441
|
+
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
2442
|
+
${paramName}?: {
|
|
2443
|
+
orderBy?: string | string[];
|
|
2444
|
+
order?: "asc" | "desc";
|
|
2445
|
+
limit?: number;
|
|
2446
|
+
offset?: number;
|
|
2447
|
+
};
|
|
2448
|
+
}`;
|
|
2449
|
+
}
|
|
2450
|
+
} else if (pattern.type === "parallel") {
|
|
2451
|
+
const includeParams = pattern.keys.map((key) => {
|
|
2452
|
+
const paramName = toIncludeParamName(key);
|
|
2453
|
+
includeParamNames.push(paramName);
|
|
2454
|
+
return `${paramName}?: {
|
|
2455
|
+
orderBy?: string | string[];
|
|
2456
|
+
order?: "asc" | "desc";
|
|
2457
|
+
limit?: number;
|
|
2458
|
+
offset?: number;
|
|
2459
|
+
}`;
|
|
2460
|
+
}).join(`;
|
|
2461
|
+
`);
|
|
2462
|
+
paramsType = `{
|
|
2463
|
+
limit?: number;
|
|
2464
|
+
offset?: number;
|
|
2465
|
+
where?: Where<Select${Type}>;
|
|
2466
|
+
orderBy?: string | string[];
|
|
2467
|
+
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
2468
|
+
${includeParams};
|
|
2469
|
+
}`;
|
|
2470
|
+
} else if (pattern.type === "nested" && pattern.nestedKey) {
|
|
2471
|
+
const paramName = toIncludeParamName(pattern.nestedKey);
|
|
2472
|
+
includeParamNames.push(paramName);
|
|
2473
|
+
paramsType = `{
|
|
2474
|
+
limit?: number;
|
|
2475
|
+
offset?: number;
|
|
2476
|
+
where?: Where<Select${Type}>;
|
|
2477
|
+
orderBy?: string | string[];
|
|
2478
|
+
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
2479
|
+
${paramName}?: {
|
|
2480
|
+
orderBy?: string | string[];
|
|
2481
|
+
order?: "asc" | "desc";
|
|
2482
|
+
limit?: number;
|
|
2483
|
+
offset?: number;
|
|
2484
|
+
include?: any;
|
|
2485
|
+
};
|
|
2486
|
+
}`;
|
|
2487
|
+
}
|
|
2401
2488
|
if (isGetByPk) {
|
|
2402
2489
|
const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
|
|
2403
2490
|
const baseReturnType = method.returnType.replace(" | null", "");
|
|
2491
|
+
let transformCode = "";
|
|
2492
|
+
if (includeParamNames.length > 0) {
|
|
2493
|
+
const destructure = includeParamNames.map((name) => name).join(", ");
|
|
2494
|
+
if (pattern.type === "single") {
|
|
2495
|
+
const key = pattern.keys[0];
|
|
2496
|
+
const paramName = includeParamNames[0];
|
|
2497
|
+
transformCode = `
|
|
2498
|
+
const { ${destructure} } = params ?? {};
|
|
2499
|
+
const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};`;
|
|
2500
|
+
} else if (pattern.type === "parallel") {
|
|
2501
|
+
const includeSpecCode = pattern.keys.map((key, idx) => {
|
|
2502
|
+
const paramName = includeParamNames[idx];
|
|
2503
|
+
return `${key}: ${paramName} ?? true`;
|
|
2504
|
+
}).join(", ");
|
|
2505
|
+
transformCode = `
|
|
2506
|
+
const { ${destructure} } = params ?? {};
|
|
2507
|
+
const includeSpec = { ${includeSpecCode} };`;
|
|
2508
|
+
} else if (pattern.type === "nested" && pattern.nestedKey) {
|
|
2509
|
+
const key = pattern.nestedKey;
|
|
2510
|
+
const paramName = includeParamNames[0];
|
|
2511
|
+
transformCode = `
|
|
2512
|
+
const { ${destructure} } = params ?? {};
|
|
2513
|
+
const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};`;
|
|
2514
|
+
}
|
|
2515
|
+
} else {
|
|
2516
|
+
transformCode = `
|
|
2517
|
+
const includeSpec = ${JSON.stringify(method.includeSpec)};`;
|
|
2518
|
+
}
|
|
2404
2519
|
includeMethodsCode += `
|
|
2405
2520
|
/**
|
|
2406
2521
|
* Get a ${table.name} record by primary key with included related ${relationshipDesc}
|
|
2407
2522
|
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
2523
|
+
* @param params - Optional include options
|
|
2408
2524
|
* @returns The record with nested ${method.path.join(" and ")} if found, null otherwise
|
|
2409
2525
|
*/
|
|
2410
|
-
async ${method.name}(pk: ${pkType}): Promise<${method.returnType}> {
|
|
2526
|
+
async ${method.name}(pk: ${pkType}, params?: ${paramsType}): Promise<${method.returnType}> {${transformCode}
|
|
2411
2527
|
const results = await this.post<PaginatedResponse<${baseReturnType}>>(\`\${this.resource}/list\`, {
|
|
2412
2528
|
where: ${pkWhere},
|
|
2413
|
-
include:
|
|
2529
|
+
include: includeSpec,
|
|
2414
2530
|
limit: 1
|
|
2415
2531
|
});
|
|
2416
2532
|
return (results.data[0] as ${baseReturnType}) ?? null;
|
|
2417
2533
|
}
|
|
2418
2534
|
`;
|
|
2419
2535
|
} else {
|
|
2536
|
+
let transformCode = "";
|
|
2537
|
+
if (includeParamNames.length > 0) {
|
|
2538
|
+
const destructure = includeParamNames.map((name) => name).join(", ");
|
|
2539
|
+
if (pattern.type === "single") {
|
|
2540
|
+
const key = pattern.keys[0];
|
|
2541
|
+
const paramName = includeParamNames[0];
|
|
2542
|
+
transformCode = `
|
|
2543
|
+
const { ${destructure}, ...baseParams } = params ?? {};
|
|
2544
|
+
const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};
|
|
2545
|
+
return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...baseParams, include: includeSpec });`;
|
|
2546
|
+
} else if (pattern.type === "parallel") {
|
|
2547
|
+
const includeSpecCode = pattern.keys.map((key, idx) => {
|
|
2548
|
+
const paramName = includeParamNames[idx];
|
|
2549
|
+
return `${key}: ${paramName} ?? true`;
|
|
2550
|
+
}).join(", ");
|
|
2551
|
+
transformCode = `
|
|
2552
|
+
const { ${destructure}, ...baseParams } = params ?? {};
|
|
2553
|
+
const includeSpec = { ${includeSpecCode} };
|
|
2554
|
+
return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...baseParams, include: includeSpec });`;
|
|
2555
|
+
} else if (pattern.type === "nested" && pattern.nestedKey) {
|
|
2556
|
+
const key = pattern.nestedKey;
|
|
2557
|
+
const paramName = includeParamNames[0];
|
|
2558
|
+
transformCode = `
|
|
2559
|
+
const { ${destructure}, ...baseParams } = params ?? {};
|
|
2560
|
+
const includeSpec = ${paramName} ? { ${key}: ${paramName} } : ${JSON.stringify(method.includeSpec)};
|
|
2561
|
+
return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...baseParams, include: includeSpec });`;
|
|
2562
|
+
}
|
|
2563
|
+
} else {
|
|
2564
|
+
transformCode = `
|
|
2565
|
+
return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...params, include: ${JSON.stringify(method.includeSpec)} });`;
|
|
2566
|
+
}
|
|
2420
2567
|
includeMethodsCode += `
|
|
2421
2568
|
/**
|
|
2422
2569
|
* List ${table.name} records with included related ${relationshipDesc}
|
|
2423
|
-
* @param params - Query parameters (where, orderBy, order, limit, offset)
|
|
2570
|
+
* @param params - Query parameters (where, orderBy, order, limit, offset) and include options
|
|
2424
2571
|
* @returns Paginated results with nested ${method.path.join(" and ")} included
|
|
2425
2572
|
*/
|
|
2426
|
-
async ${method.name}(${
|
|
2427
|
-
return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...params, include: ${JSON.stringify(method.includeSpec)} });
|
|
2573
|
+
async ${method.name}(params?: ${paramsType}): Promise<${method.returnType}> {${transformCode}
|
|
2428
2574
|
}
|
|
2429
2575
|
`;
|
|
2430
2576
|
}
|
|
@@ -2501,7 +2647,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
2501
2647
|
* @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
|
|
2502
2648
|
* @param params.orderBy - Column(s) to sort by
|
|
2503
2649
|
* @param params.order - Sort direction(s): "asc" or "desc"
|
|
2504
|
-
* @param params.limit - Maximum number of records to return (default: 50, max:
|
|
2650
|
+
* @param params.limit - Maximum number of records to return (default: 50, max: 1000)
|
|
2505
2651
|
* @param params.offset - Number of records to skip for pagination
|
|
2506
2652
|
* @param params.include - Related records to include (see listWith* methods for typed includes)
|
|
2507
2653
|
* @returns Paginated results with data, total count, and hasMore flag
|
|
@@ -2530,7 +2676,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
2530
2676
|
* @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
|
|
2531
2677
|
* @param params.orderBy - Column(s) to sort by
|
|
2532
2678
|
* @param params.order - Sort direction(s): "asc" or "desc"
|
|
2533
|
-
* @param params.limit - Maximum number of records to return (default: 50, max:
|
|
2679
|
+
* @param params.limit - Maximum number of records to return (default: 50, max: 1000)
|
|
2534
2680
|
* @param params.offset - Number of records to skip for pagination
|
|
2535
2681
|
* @param params.include - Related records to include (see listWith* methods for typed includes)
|
|
2536
2682
|
* @returns Paginated results with data, total count, and hasMore flag
|
|
@@ -2867,6 +3013,13 @@ type Graph = typeof RELATION_GRAPH;
|
|
|
2867
3013
|
type TableName = keyof Graph;
|
|
2868
3014
|
type IncludeSpec = any;
|
|
2869
3015
|
|
|
3016
|
+
type RelationOptions = {
|
|
3017
|
+
limit?: number;
|
|
3018
|
+
offset?: number;
|
|
3019
|
+
orderBy?: string;
|
|
3020
|
+
order?: "asc" | "desc";
|
|
3021
|
+
};
|
|
3022
|
+
|
|
2870
3023
|
// Debug helpers (enabled with SDK_DEBUG=1)
|
|
2871
3024
|
const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
|
|
2872
3025
|
const log = {
|
|
@@ -2968,14 +3121,43 @@ export async function loadIncludes(
|
|
|
2968
3121
|
// Safely run each loader; never let one bad edge 500 the route
|
|
2969
3122
|
if (rel.via) {
|
|
2970
3123
|
// M:N via junction
|
|
3124
|
+
const specValue = s[key];
|
|
3125
|
+
const options: RelationOptions = {};
|
|
3126
|
+
let childSpec: any = undefined;
|
|
3127
|
+
|
|
3128
|
+
if (specValue && typeof specValue === "object" && specValue !== true) {
|
|
3129
|
+
// Extract options
|
|
3130
|
+
if (specValue.limit !== undefined) options.limit = specValue.limit;
|
|
3131
|
+
if (specValue.offset !== undefined) options.offset = specValue.offset;
|
|
3132
|
+
if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
|
|
3133
|
+
if (specValue.order !== undefined) options.order = specValue.order;
|
|
3134
|
+
|
|
3135
|
+
// Extract nested spec - support both formats:
|
|
3136
|
+
// New: { limit: 3, include: { tags: true } }
|
|
3137
|
+
// Old: { tags: true } (backward compatibility)
|
|
3138
|
+
if (specValue.include !== undefined) {
|
|
3139
|
+
childSpec = specValue.include;
|
|
3140
|
+
} else {
|
|
3141
|
+
// Build childSpec from non-option keys
|
|
3142
|
+
const nonOptionKeys = Object.keys(specValue).filter(
|
|
3143
|
+
k => k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
|
|
3144
|
+
);
|
|
3145
|
+
if (nonOptionKeys.length > 0) {
|
|
3146
|
+
childSpec = {};
|
|
3147
|
+
for (const k of nonOptionKeys) {
|
|
3148
|
+
childSpec[k] = specValue[k];
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
|
|
2971
3154
|
try {
|
|
2972
|
-
await loadManyToMany(table, target, rel.via as string, rows, key);
|
|
3155
|
+
await loadManyToMany(table, target, rel.via as string, rows, key, options);
|
|
2973
3156
|
} catch (e: any) {
|
|
2974
3157
|
log.error("loadManyToMany failed", { table, key, via: rel.via, target }, e?.message ?? e);
|
|
2975
3158
|
for (const r of rows) r[key] = [];
|
|
2976
3159
|
}
|
|
2977
|
-
|
|
2978
|
-
const childSpec = s[key] && typeof s[key] === "object" ? s[key] : undefined;
|
|
3160
|
+
|
|
2979
3161
|
if (childSpec) {
|
|
2980
3162
|
const children = rows.flatMap(r => (r[key] ?? []));
|
|
2981
3163
|
try {
|
|
@@ -2989,13 +3171,43 @@ export async function loadIncludes(
|
|
|
2989
3171
|
|
|
2990
3172
|
if (rel.kind === "many") {
|
|
2991
3173
|
// 1:N target has FK to current
|
|
3174
|
+
const specValue = s[key];
|
|
3175
|
+
const options: RelationOptions = {};
|
|
3176
|
+
let childSpec: any = undefined;
|
|
3177
|
+
|
|
3178
|
+
if (specValue && typeof specValue === "object" && specValue !== true) {
|
|
3179
|
+
// Extract options
|
|
3180
|
+
if (specValue.limit !== undefined) options.limit = specValue.limit;
|
|
3181
|
+
if (specValue.offset !== undefined) options.offset = specValue.offset;
|
|
3182
|
+
if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
|
|
3183
|
+
if (specValue.order !== undefined) options.order = specValue.order;
|
|
3184
|
+
|
|
3185
|
+
// Extract nested spec - support both formats:
|
|
3186
|
+
// New: { limit: 3, include: { tags: true } }
|
|
3187
|
+
// Old: { tags: true } (backward compatibility)
|
|
3188
|
+
if (specValue.include !== undefined) {
|
|
3189
|
+
childSpec = specValue.include;
|
|
3190
|
+
} else {
|
|
3191
|
+
// Build childSpec from non-option keys
|
|
3192
|
+
const nonOptionKeys = Object.keys(specValue).filter(
|
|
3193
|
+
k => k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
|
|
3194
|
+
);
|
|
3195
|
+
if (nonOptionKeys.length > 0) {
|
|
3196
|
+
childSpec = {};
|
|
3197
|
+
for (const k of nonOptionKeys) {
|
|
3198
|
+
childSpec[k] = specValue[k];
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
|
|
2992
3204
|
try {
|
|
2993
|
-
await loadOneToMany(table, target, rows, key);
|
|
3205
|
+
await loadOneToMany(table, target, rows, key, options);
|
|
2994
3206
|
} catch (e: any) {
|
|
2995
3207
|
log.error("loadOneToMany failed", { table, key, target }, e?.message ?? e);
|
|
2996
3208
|
for (const r of rows) r[key] = [];
|
|
2997
3209
|
}
|
|
2998
|
-
|
|
3210
|
+
|
|
2999
3211
|
if (childSpec) {
|
|
3000
3212
|
const children = rows.flatMap(r => (r[key] ?? []));
|
|
3001
3213
|
try {
|
|
@@ -3083,7 +3295,7 @@ export async function loadIncludes(
|
|
|
3083
3295
|
}
|
|
3084
3296
|
}
|
|
3085
3297
|
|
|
3086
|
-
async function loadOneToMany(curr: TableName, target: TableName, rows: any[], key: string) {
|
|
3298
|
+
async function loadOneToMany(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
|
|
3087
3299
|
// target has FK cols referencing current PK
|
|
3088
3300
|
const fk = (FK_INDEX as any)[target].find((f: any) => f.toTable === curr);
|
|
3089
3301
|
if (!fk) { for (const r of rows) r[key] = []; return; }
|
|
@@ -3094,18 +3306,50 @@ export async function loadIncludes(
|
|
|
3094
3306
|
|
|
3095
3307
|
const where = buildOrAndPredicate(fk.from, tuples.length, 1);
|
|
3096
3308
|
const params = tuples.flat();
|
|
3097
|
-
|
|
3098
|
-
|
|
3309
|
+
|
|
3310
|
+
// Build SQL with optional ORDER BY, LIMIT, OFFSET
|
|
3311
|
+
let sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
|
|
3312
|
+
|
|
3313
|
+
// If limit/offset are needed, use window functions to limit per parent
|
|
3314
|
+
if (options.limit !== undefined || options.offset !== undefined) {
|
|
3315
|
+
const orderByClause = options.orderBy
|
|
3316
|
+
? \`ORDER BY "\${options.orderBy}" \${options.order === 'desc' ? 'DESC' : 'ASC'}\`
|
|
3317
|
+
: 'ORDER BY (SELECT NULL)';
|
|
3318
|
+
|
|
3319
|
+
const partitionCols = fk.from.map((c: string) => \`"\${c}"\`).join(', ');
|
|
3320
|
+
const offset = options.offset ?? 0;
|
|
3321
|
+
const limit = options.limit ?? 999999999;
|
|
3322
|
+
|
|
3323
|
+
sql = \`
|
|
3324
|
+
SELECT * FROM (
|
|
3325
|
+
SELECT *, ROW_NUMBER() OVER (PARTITION BY \${partitionCols} \${orderByClause}) as __rn
|
|
3326
|
+
FROM "\${target}"
|
|
3327
|
+
WHERE \${where}
|
|
3328
|
+
) __sub
|
|
3329
|
+
WHERE __rn > \${offset} AND __rn <= \${offset + limit}
|
|
3330
|
+
\`;
|
|
3331
|
+
} else if (options.orderBy) {
|
|
3332
|
+
// Just ORDER BY without limit/offset
|
|
3333
|
+
sql += \` ORDER BY "\${options.orderBy}" \${options.order === 'desc' ? 'DESC' : 'ASC'}\`;
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
log.debug("oneToMany SQL", { curr, target, key, sql, paramsCount: params.length, options });
|
|
3099
3337
|
const { rows: children } = await pg.query(sql, params);
|
|
3100
3338
|
|
|
3101
|
-
|
|
3339
|
+
// Remove __rn column if it exists
|
|
3340
|
+
const cleanChildren = children.map((row: any) => {
|
|
3341
|
+
const { __rn, ...rest } = row;
|
|
3342
|
+
return rest;
|
|
3343
|
+
});
|
|
3344
|
+
|
|
3345
|
+
const groups = groupByTuple(cleanChildren, fk.from);
|
|
3102
3346
|
for (const r of rows) {
|
|
3103
3347
|
const keyStr = JSON.stringify(pkCols.map((c: string) => r[c]));
|
|
3104
3348
|
r[key] = groups.get(keyStr) ?? [];
|
|
3105
3349
|
}
|
|
3106
3350
|
}
|
|
3107
3351
|
|
|
3108
|
-
async function loadManyToMany(curr: TableName, target: TableName, via: string, rows: any[], key: string) {
|
|
3352
|
+
async function loadManyToMany(curr: TableName, target: TableName, via: string, rows: any[], key: string, options: RelationOptions = {}) {
|
|
3109
3353
|
// via has two FKs: one to curr, one to target
|
|
3110
3354
|
const toCurr = (FK_INDEX as any)[via].find((f: any) => f.toTable === curr);
|
|
3111
3355
|
const toTarget = (FK_INDEX as any)[via].find((f: any) => f.toTable === target);
|
|
@@ -3115,31 +3359,83 @@ export async function loadIncludes(
|
|
|
3115
3359
|
const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
|
|
3116
3360
|
if (!tuples.length) { for (const r of rows) r[key] = []; return; }
|
|
3117
3361
|
|
|
3118
|
-
// 1) Load junction rows for current parents
|
|
3119
3362
|
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; }
|
|
3363
|
+
const params = tuples.flat();
|
|
3126
3364
|
|
|
3127
|
-
//
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3365
|
+
// If we have limit/offset/orderBy, use a JOIN with window functions
|
|
3366
|
+
if (options.limit !== undefined || options.offset !== undefined || options.orderBy) {
|
|
3367
|
+
const orderByClause = options.orderBy
|
|
3368
|
+
? \`ORDER BY t."\${options.orderBy}" \${options.order === 'desc' ? 'DESC' : 'ASC'}\`
|
|
3369
|
+
: 'ORDER BY (SELECT NULL)';
|
|
3370
|
+
|
|
3371
|
+
const partitionCols = toCurr.from.map((c: string) => \`j."\${c}"\`).join(', ');
|
|
3372
|
+
const offset = options.offset ?? 0;
|
|
3373
|
+
const limit = options.limit ?? 999999999;
|
|
3374
|
+
|
|
3375
|
+
const targetPkCols = (PKS as any)[target] as string[];
|
|
3376
|
+
const joinConditions = toTarget.from.map((jCol: string, i: number) => {
|
|
3377
|
+
return \`j."\${jCol}" = t."\${targetPkCols[i]}"\`;
|
|
3378
|
+
}).join(' AND ');
|
|
3379
|
+
|
|
3380
|
+
const sql = \`
|
|
3381
|
+
SELECT __numbered.*
|
|
3382
|
+
FROM (
|
|
3383
|
+
SELECT t.*,
|
|
3384
|
+
ROW_NUMBER() OVER (PARTITION BY \${partitionCols} \${orderByClause}) as __rn,
|
|
3385
|
+
\${toCurr.from.map((c: string) => \`j."\${c}"\`).join(' || \\',\\' || ')} as __parent_fk
|
|
3386
|
+
FROM "\${via}" j
|
|
3387
|
+
INNER JOIN "\${target}" t ON \${joinConditions}
|
|
3388
|
+
WHERE \${whereVia}
|
|
3389
|
+
) __numbered
|
|
3390
|
+
WHERE __numbered.__rn > \${offset} AND __numbered.__rn <= \${offset + limit}
|
|
3391
|
+
\`;
|
|
3392
|
+
|
|
3393
|
+
log.debug("manyToMany SQL with options", { curr, target, via, key, sql, paramsCount: params.length, options });
|
|
3394
|
+
const { rows: results } = await pg.query(sql, params);
|
|
3395
|
+
|
|
3396
|
+
// Clean and group results
|
|
3397
|
+
const cleanResults = results.map((row: any) => {
|
|
3398
|
+
const { __rn, __parent_fk, ...rest } = row;
|
|
3399
|
+
return { ...rest, __parent_fk };
|
|
3400
|
+
});
|
|
3134
3401
|
|
|
3135
|
-
|
|
3402
|
+
const grouped = new Map<string, any[]>();
|
|
3403
|
+
for (const row of cleanResults) {
|
|
3404
|
+
const { __parent_fk, ...cleanRow } = row;
|
|
3405
|
+
const arr = grouped.get(__parent_fk) ?? [];
|
|
3406
|
+
arr.push(cleanRow);
|
|
3407
|
+
grouped.set(__parent_fk, arr);
|
|
3408
|
+
}
|
|
3136
3409
|
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3410
|
+
for (const r of rows) {
|
|
3411
|
+
const currKey = pkCols.map((c: string) => r[c]).join(',');
|
|
3412
|
+
r[key] = grouped.get(currKey) ?? [];
|
|
3413
|
+
}
|
|
3414
|
+
} else {
|
|
3415
|
+
// Original logic without options
|
|
3416
|
+
const sqlVia = \`SELECT * FROM "\${via}" WHERE \${whereVia}\`;
|
|
3417
|
+
log.debug("manyToMany junction SQL", { curr, target, via, key, sql: sqlVia, paramsCount: params.length });
|
|
3418
|
+
const { rows: jrows } = await pg.query(sqlVia, params);
|
|
3419
|
+
|
|
3420
|
+
if (!jrows.length) { for (const r of rows) r[key] = []; return; }
|
|
3421
|
+
|
|
3422
|
+
// 2) Load targets by distinct target fk tuples in junction
|
|
3423
|
+
const tTuples = distinctTuples(jrows, toTarget.from);
|
|
3424
|
+
const whereT = buildOrAndPredicate((PKS as any)[target], tTuples.length, 1);
|
|
3425
|
+
const sqlT = \`SELECT * FROM "\${target}" WHERE \${whereT}\`;
|
|
3426
|
+
const paramsT = tTuples.flat();
|
|
3427
|
+
log.debug("manyToMany target SQL", { curr, target, via, key, sql: sqlT, paramsCount: paramsT.length });
|
|
3428
|
+
const { rows: targets } = await pg.query(sqlT, paramsT);
|
|
3429
|
+
|
|
3430
|
+
const tIdx = indexByTuple(targets, (PKS as any)[target]);
|
|
3431
|
+
|
|
3432
|
+
// 3) Group junction rows by current pk tuple, map to target rows
|
|
3433
|
+
const byCurr = groupByTuple(jrows, toCurr.from);
|
|
3434
|
+
for (const r of rows) {
|
|
3435
|
+
const currKey = JSON.stringify(pkCols.map((c: string) => r[c]));
|
|
3436
|
+
const j = byCurr.get(currKey) ?? [];
|
|
3437
|
+
r[key] = j.map(jr => tIdx.get(JSON.stringify(toTarget.from.map((c: string) => jr[c])))).filter(Boolean);
|
|
3438
|
+
}
|
|
3143
3439
|
}
|
|
3144
3440
|
}
|
|
3145
3441
|
}
|
|
@@ -3159,7 +3455,11 @@ function tsTypeFor(pgType, opts, enums) {
|
|
|
3159
3455
|
if (t === "bool" || t === "boolean")
|
|
3160
3456
|
return "boolean";
|
|
3161
3457
|
if (t === "int2" || t === "int4" || t === "int8" || t === "float4" || t === "float8" || t === "numeric") {
|
|
3162
|
-
|
|
3458
|
+
if (opts.numericMode === "number")
|
|
3459
|
+
return "number";
|
|
3460
|
+
if (opts.numericMode === "string")
|
|
3461
|
+
return "string";
|
|
3462
|
+
return t === "int2" || t === "int4" || t === "float4" || t === "float8" ? "number" : "string";
|
|
3163
3463
|
}
|
|
3164
3464
|
if (t === "date" || t.startsWith("timestamp"))
|
|
3165
3465
|
return "string";
|
|
@@ -5364,10 +5664,11 @@ async function generate(configPath) {
|
|
|
5364
5664
|
console.log(`[Index] About to process ${Object.keys(model.tables || {}).length} tables for generation`);
|
|
5365
5665
|
}
|
|
5366
5666
|
for (const table of Object.values(model.tables)) {
|
|
5367
|
-
const
|
|
5667
|
+
const numericMode = cfg.numericMode ?? "auto";
|
|
5668
|
+
const typesSrc = emitTypes(table, { numericMode }, model.enums);
|
|
5368
5669
|
files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
5369
5670
|
files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
5370
|
-
const zodSrc = emitZod(table, { numericMode
|
|
5671
|
+
const zodSrc = emitZod(table, { numericMode }, model.enums);
|
|
5371
5672
|
files.push({ path: join(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
5372
5673
|
files.push({ path: join(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
5373
5674
|
const paramsZodSrc = emitParamsZod(table, graph);
|
package/dist/types.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface Config {
|
|
|
24
24
|
};
|
|
25
25
|
softDeleteColumn?: string | null;
|
|
26
26
|
dateType?: "date" | "string";
|
|
27
|
+
numericMode?: "string" | "number" | "auto";
|
|
27
28
|
includeMethodsDepth?: number;
|
|
28
29
|
skipJunctionTables?: boolean;
|
|
29
30
|
serverFramework?: "hono" | "express" | "fastify";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postgresdk",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.13",
|
|
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 && bun test test/test-numeric-mode-integration.test.ts",
|
|
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",
|