postgresdk 0.8.0 → 0.9.1
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/dist/cli.js +194 -178
- package/dist/emit-client.d.ts +2 -2
- package/dist/emit-include-methods.d.ts +1 -1
- package/dist/emit-sdk-contract.d.ts +3 -2
- package/dist/index.js +194 -178
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -486,6 +486,157 @@ async function ensureDirs(dirs) {
|
|
|
486
486
|
var pascal = (s) => s.split(/[_\s-]+/).map((w) => w?.[0] ? w[0].toUpperCase() + w.slice(1) : "").join("");
|
|
487
487
|
var init_utils = () => {};
|
|
488
488
|
|
|
489
|
+
// src/emit-include-methods.ts
|
|
490
|
+
function isJunctionTable(table) {
|
|
491
|
+
if (!table.name.includes("_"))
|
|
492
|
+
return false;
|
|
493
|
+
const fkColumns = new Set(table.fks.flatMap((fk) => fk.from));
|
|
494
|
+
const nonPkColumns = table.columns.filter((c) => !table.pk.includes(c.name));
|
|
495
|
+
return nonPkColumns.every((c) => fkColumns.has(c.name));
|
|
496
|
+
}
|
|
497
|
+
function pathToMethodSuffix(path) {
|
|
498
|
+
return "With" + path.map((p) => pascal(p)).join("And");
|
|
499
|
+
}
|
|
500
|
+
function buildReturnType(baseTable, path, isMany, targets, graph) {
|
|
501
|
+
const BaseType = `Select${pascal(baseTable)}`;
|
|
502
|
+
if (path.length === 0)
|
|
503
|
+
return BaseType;
|
|
504
|
+
let type = BaseType;
|
|
505
|
+
let currentTable = baseTable;
|
|
506
|
+
const parts = [];
|
|
507
|
+
for (let i = 0;i < path.length; i++) {
|
|
508
|
+
const key = path[i];
|
|
509
|
+
const target = targets[i];
|
|
510
|
+
if (!key || !target)
|
|
511
|
+
continue;
|
|
512
|
+
const targetType = `Select${pascal(target)}`;
|
|
513
|
+
if (i === 0) {
|
|
514
|
+
parts.push(`${key}: ${isMany[i] ? `${targetType}[]` : targetType}`);
|
|
515
|
+
} else {
|
|
516
|
+
let nestedType = targetType;
|
|
517
|
+
for (let j = i;j < path.length; j++) {
|
|
518
|
+
if (j > i) {
|
|
519
|
+
const nestedKey = path[j];
|
|
520
|
+
const nestedTarget = targets[j];
|
|
521
|
+
if (!nestedKey || !nestedTarget)
|
|
522
|
+
continue;
|
|
523
|
+
const nestedTargetType = `Select${pascal(nestedTarget)}`;
|
|
524
|
+
nestedType = `${nestedType} & { ${nestedKey}: ${isMany[j] ? `${nestedTargetType}[]` : nestedTargetType} }`;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const prevKey = path[i - 1];
|
|
528
|
+
const prevTarget = targets[i - 1];
|
|
529
|
+
if (prevKey && prevTarget) {
|
|
530
|
+
parts[parts.length - 1] = `${prevKey}: ${isMany[i - 1] ? `(Select${pascal(prevTarget)} & { ${key}: ${isMany[i] ? `${targetType}[]` : targetType} })[]` : `Select${pascal(prevTarget)} & { ${key}: ${isMany[i] ? `${targetType}[]` : targetType} }`}`;
|
|
531
|
+
}
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return `${type} & { ${parts.join("; ")} }`;
|
|
536
|
+
}
|
|
537
|
+
function buildIncludeSpec(path) {
|
|
538
|
+
if (path.length === 0)
|
|
539
|
+
return {};
|
|
540
|
+
if (path.length === 1)
|
|
541
|
+
return { [path[0]]: true };
|
|
542
|
+
let spec = true;
|
|
543
|
+
for (let i = path.length - 1;i > 0; i--) {
|
|
544
|
+
const key = path[i];
|
|
545
|
+
if (!key)
|
|
546
|
+
continue;
|
|
547
|
+
spec = { [key]: spec };
|
|
548
|
+
}
|
|
549
|
+
const rootKey = path[0];
|
|
550
|
+
return rootKey ? { [rootKey]: spec } : {};
|
|
551
|
+
}
|
|
552
|
+
function generateIncludeMethods(table, graph, opts, allTables) {
|
|
553
|
+
const methods = [];
|
|
554
|
+
const baseTableName = table.name;
|
|
555
|
+
if (opts.skipJunctionTables && isJunctionTable(table)) {
|
|
556
|
+
return methods;
|
|
557
|
+
}
|
|
558
|
+
const edges = graph[baseTableName] || {};
|
|
559
|
+
function explore(currentTable, path, isMany, targets, visited, depth) {
|
|
560
|
+
if (depth > opts.maxDepth)
|
|
561
|
+
return;
|
|
562
|
+
const currentEdges = graph[currentTable] || {};
|
|
563
|
+
for (const [key, edge] of Object.entries(currentEdges)) {
|
|
564
|
+
if (visited.has(edge.target))
|
|
565
|
+
continue;
|
|
566
|
+
if (opts.skipJunctionTables && allTables) {
|
|
567
|
+
const targetTable = allTables.find((t) => t.name === edge.target);
|
|
568
|
+
if (targetTable && isJunctionTable(targetTable)) {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const newPath = [...path, key];
|
|
573
|
+
const newIsMany = [...isMany, edge.kind === "many"];
|
|
574
|
+
const newTargets = [...targets, edge.target];
|
|
575
|
+
const methodSuffix = pathToMethodSuffix(newPath);
|
|
576
|
+
methods.push({
|
|
577
|
+
name: `list${methodSuffix}`,
|
|
578
|
+
path: newPath,
|
|
579
|
+
isMany: newIsMany,
|
|
580
|
+
targets: newTargets,
|
|
581
|
+
returnType: `(${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)})[]`,
|
|
582
|
+
includeSpec: buildIncludeSpec(newPath)
|
|
583
|
+
});
|
|
584
|
+
methods.push({
|
|
585
|
+
name: `getByPk${methodSuffix}`,
|
|
586
|
+
path: newPath,
|
|
587
|
+
isMany: newIsMany,
|
|
588
|
+
targets: newTargets,
|
|
589
|
+
returnType: `${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)} | null`,
|
|
590
|
+
includeSpec: buildIncludeSpec(newPath)
|
|
591
|
+
});
|
|
592
|
+
explore(edge.target, newPath, newIsMany, newTargets, new Set([...visited, edge.target]), depth + 1);
|
|
593
|
+
}
|
|
594
|
+
if (depth === 1 && Object.keys(currentEdges).length > 1 && Object.keys(currentEdges).length <= 3) {
|
|
595
|
+
const edgeEntries = Object.entries(currentEdges);
|
|
596
|
+
if (edgeEntries.length >= 2) {
|
|
597
|
+
for (let i = 0;i < edgeEntries.length - 1; i++) {
|
|
598
|
+
for (let j = i + 1;j < edgeEntries.length; j++) {
|
|
599
|
+
const entry1 = edgeEntries[i];
|
|
600
|
+
const entry2 = edgeEntries[j];
|
|
601
|
+
if (!entry1 || !entry2)
|
|
602
|
+
continue;
|
|
603
|
+
const [key1, edge1] = entry1;
|
|
604
|
+
const [key2, edge2] = entry2;
|
|
605
|
+
if (opts.skipJunctionTables && (edge1.target.includes("_") || edge2.target.includes("_"))) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
const combinedPath = [key1, key2];
|
|
609
|
+
const combinedSuffix = `With${pascal(key1)}And${pascal(key2)}`;
|
|
610
|
+
const type1 = `${key1}: ${edge1.kind === "many" ? `Select${pascal(edge1.target)}[]` : `Select${pascal(edge1.target)}`}`;
|
|
611
|
+
const type2 = `${key2}: ${edge2.kind === "many" ? `Select${pascal(edge2.target)}[]` : `Select${pascal(edge2.target)}`}`;
|
|
612
|
+
methods.push({
|
|
613
|
+
name: `list${combinedSuffix}`,
|
|
614
|
+
path: combinedPath,
|
|
615
|
+
isMany: [edge1.kind === "many", edge2.kind === "many"],
|
|
616
|
+
targets: [edge1.target, edge2.target],
|
|
617
|
+
returnType: `(Select${pascal(baseTableName)} & { ${type1}; ${type2} })[]`,
|
|
618
|
+
includeSpec: { [key1]: true, [key2]: true }
|
|
619
|
+
});
|
|
620
|
+
methods.push({
|
|
621
|
+
name: `getByPk${combinedSuffix}`,
|
|
622
|
+
path: combinedPath,
|
|
623
|
+
isMany: [edge1.kind === "many", edge2.kind === "many"],
|
|
624
|
+
targets: [edge1.target, edge2.target],
|
|
625
|
+
returnType: `(Select${pascal(baseTableName)} & { ${type1}; ${type2} }) | null`,
|
|
626
|
+
includeSpec: { [key1]: true, [key2]: true }
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
explore(baseTableName, [], [], [], new Set([baseTableName]), 1);
|
|
634
|
+
return methods;
|
|
635
|
+
}
|
|
636
|
+
var init_emit_include_methods = __esm(() => {
|
|
637
|
+
init_utils();
|
|
638
|
+
});
|
|
639
|
+
|
|
489
640
|
// src/emit-sdk-contract.ts
|
|
490
641
|
var exports_emit_sdk_contract = {};
|
|
491
642
|
__export(exports_emit_sdk_contract, {
|
|
@@ -493,7 +644,7 @@ __export(exports_emit_sdk_contract, {
|
|
|
493
644
|
generateUnifiedContract: () => generateUnifiedContract,
|
|
494
645
|
emitUnifiedContract: () => emitUnifiedContract
|
|
495
646
|
});
|
|
496
|
-
function generateUnifiedContract(model, config) {
|
|
647
|
+
function generateUnifiedContract(model, config, graph) {
|
|
497
648
|
const resources = [];
|
|
498
649
|
const relationships = [];
|
|
499
650
|
const tables = model && model.tables ? Object.values(model.tables) : [];
|
|
@@ -501,7 +652,7 @@ function generateUnifiedContract(model, config) {
|
|
|
501
652
|
console.log(`[SDK Contract] Processing ${tables.length} tables`);
|
|
502
653
|
}
|
|
503
654
|
for (const table of tables) {
|
|
504
|
-
resources.push(generateResourceWithSDK(table, model));
|
|
655
|
+
resources.push(generateResourceWithSDK(table, model, graph, config));
|
|
505
656
|
for (const fk of table.fks) {
|
|
506
657
|
relationships.push({
|
|
507
658
|
from: table.name,
|
|
@@ -619,7 +770,7 @@ function generateSDKAuthExamples(auth) {
|
|
|
619
770
|
});
|
|
620
771
|
return examples;
|
|
621
772
|
}
|
|
622
|
-
function generateResourceWithSDK(table, model) {
|
|
773
|
+
function generateResourceWithSDK(table, model, graph, config) {
|
|
623
774
|
const Type = pascal(table.name);
|
|
624
775
|
const tableName = table.name;
|
|
625
776
|
const basePath = `/v1/${tableName}`;
|
|
@@ -641,11 +792,6 @@ const filtered = await sdk.${tableName}.list({
|
|
|
641
792
|
${table.columns[0]?.name || "field"}_like: 'search',
|
|
642
793
|
order_by: '${table.columns[0]?.name || "created_at"}',
|
|
643
794
|
order_dir: 'desc'
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
// With related data
|
|
647
|
-
const withRelations = await sdk.${tableName}.list({
|
|
648
|
-
include: '${table.fks[0]?.toTable || "related_table"}'
|
|
649
795
|
});`,
|
|
650
796
|
correspondsTo: `GET ${basePath}`
|
|
651
797
|
});
|
|
@@ -659,16 +805,11 @@ const withRelations = await sdk.${tableName}.list({
|
|
|
659
805
|
if (hasSinglePK) {
|
|
660
806
|
sdkMethods.push({
|
|
661
807
|
name: "getByPk",
|
|
662
|
-
signature: `getByPk(${pkField}: string
|
|
808
|
+
signature: `getByPk(${pkField}: string): Promise<${Type} | null>`,
|
|
663
809
|
description: `Get a single ${tableName} by primary key`,
|
|
664
810
|
example: `// Get by ID
|
|
665
811
|
const item = await sdk.${tableName}.getByPk('123e4567-e89b-12d3-a456-426614174000');
|
|
666
812
|
|
|
667
|
-
// With related data
|
|
668
|
-
const withRelations = await sdk.${tableName}.getByPk('123', {
|
|
669
|
-
include: '${table.fks[0]?.toTable || "related_table"}'
|
|
670
|
-
});
|
|
671
|
-
|
|
672
813
|
// Check if exists
|
|
673
814
|
if (item === null) {
|
|
674
815
|
console.log('Not found');
|
|
@@ -679,9 +820,6 @@ if (item === null) {
|
|
|
679
820
|
method: "GET",
|
|
680
821
|
path: `${basePath}/:${pkField}`,
|
|
681
822
|
description: `Get ${tableName} by ID`,
|
|
682
|
-
queryParameters: {
|
|
683
|
-
include: "string - Comma-separated list of related resources"
|
|
684
|
-
},
|
|
685
823
|
responseBody: `${Type}`
|
|
686
824
|
});
|
|
687
825
|
}
|
|
@@ -744,6 +882,31 @@ console.log('Deleted:', deleted);`,
|
|
|
744
882
|
responseBody: `${Type}`
|
|
745
883
|
});
|
|
746
884
|
}
|
|
885
|
+
if (graph && config) {
|
|
886
|
+
const allTables = model && model.tables ? Object.values(model.tables) : undefined;
|
|
887
|
+
const includeMethods = generateIncludeMethods(table, graph, {
|
|
888
|
+
maxDepth: config.includeMethodsDepth ?? 2,
|
|
889
|
+
skipJunctionTables: config.skipJunctionTables ?? true
|
|
890
|
+
}, allTables);
|
|
891
|
+
for (const method of includeMethods) {
|
|
892
|
+
const isGetByPk = method.name.startsWith("getByPk");
|
|
893
|
+
const exampleCall = isGetByPk ? `const result = await sdk.${tableName}.${method.name}('123e4567-e89b-12d3-a456-426614174000');` : `const results = await sdk.${tableName}.${method.name}();
|
|
894
|
+
|
|
895
|
+
// With filters and pagination
|
|
896
|
+
const filtered = await sdk.${tableName}.${method.name}({
|
|
897
|
+
limit: 20,
|
|
898
|
+
offset: 0,
|
|
899
|
+
where: { /* filter conditions */ }
|
|
900
|
+
});`;
|
|
901
|
+
sdkMethods.push({
|
|
902
|
+
name: method.name,
|
|
903
|
+
signature: `${method.name}(${isGetByPk ? `${pkField}: string` : "params?: ListParams"}): ${method.returnType}`,
|
|
904
|
+
description: `Get ${tableName} with included ${method.path.join(", ")} data`,
|
|
905
|
+
example: exampleCall,
|
|
906
|
+
correspondsTo: `POST ${basePath}/list`
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
}
|
|
747
910
|
const fields = table.columns.map((col) => generateFieldContract(col, table));
|
|
748
911
|
return {
|
|
749
912
|
name: Type,
|
|
@@ -892,8 +1055,7 @@ function generateQueryParams(table) {
|
|
|
892
1055
|
limit: "number - Max records to return (default: 50)",
|
|
893
1056
|
offset: "number - Records to skip",
|
|
894
1057
|
order_by: "string - Field to sort by",
|
|
895
|
-
order_dir: "'asc' | 'desc' - Sort direction"
|
|
896
|
-
include: "string - Related resources to include"
|
|
1058
|
+
order_dir: "'asc' | 'desc' - Sort direction"
|
|
897
1059
|
};
|
|
898
1060
|
let filterCount = 0;
|
|
899
1061
|
for (const col of table.columns) {
|
|
@@ -1080,8 +1242,8 @@ function generateUnifiedContractMarkdown(contract) {
|
|
|
1080
1242
|
return lines.join(`
|
|
1081
1243
|
`);
|
|
1082
1244
|
}
|
|
1083
|
-
function emitUnifiedContract(model, config) {
|
|
1084
|
-
const contract = generateUnifiedContract(model, config);
|
|
1245
|
+
function emitUnifiedContract(model, config, graph) {
|
|
1246
|
+
const contract = generateUnifiedContract(model, config, graph);
|
|
1085
1247
|
const contractJson = JSON.stringify(contract, null, 2);
|
|
1086
1248
|
return `/**
|
|
1087
1249
|
* Unified API & SDK Contract
|
|
@@ -1137,6 +1299,7 @@ import type * as Types from './client/types';
|
|
|
1137
1299
|
}
|
|
1138
1300
|
var init_emit_sdk_contract = __esm(() => {
|
|
1139
1301
|
init_utils();
|
|
1302
|
+
init_emit_include_methods();
|
|
1140
1303
|
});
|
|
1141
1304
|
|
|
1142
1305
|
// src/cli-init.ts
|
|
@@ -1914,156 +2077,8 @@ ${hasAuth ? `
|
|
|
1914
2077
|
|
|
1915
2078
|
// src/emit-client.ts
|
|
1916
2079
|
init_utils();
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
init_utils();
|
|
1920
|
-
function isJunctionTable(table) {
|
|
1921
|
-
if (!table.name.includes("_"))
|
|
1922
|
-
return false;
|
|
1923
|
-
const fkColumns = new Set(table.fks.flatMap((fk) => fk.from));
|
|
1924
|
-
const nonPkColumns = table.columns.filter((c) => !table.pk.includes(c.name));
|
|
1925
|
-
return nonPkColumns.every((c) => fkColumns.has(c.name));
|
|
1926
|
-
}
|
|
1927
|
-
function pathToMethodSuffix(path) {
|
|
1928
|
-
return "With" + path.map((p) => pascal(p)).join("And");
|
|
1929
|
-
}
|
|
1930
|
-
function buildReturnType(baseTable, path, isMany, targets, graph) {
|
|
1931
|
-
const BaseType = `Select${pascal(baseTable)}`;
|
|
1932
|
-
if (path.length === 0)
|
|
1933
|
-
return BaseType;
|
|
1934
|
-
let type = BaseType;
|
|
1935
|
-
let currentTable = baseTable;
|
|
1936
|
-
const parts = [];
|
|
1937
|
-
for (let i = 0;i < path.length; i++) {
|
|
1938
|
-
const key = path[i];
|
|
1939
|
-
const target = targets[i];
|
|
1940
|
-
if (!key || !target)
|
|
1941
|
-
continue;
|
|
1942
|
-
const targetType = `Select${pascal(target)}`;
|
|
1943
|
-
if (i === 0) {
|
|
1944
|
-
parts.push(`${key}: ${isMany[i] ? `${targetType}[]` : targetType}`);
|
|
1945
|
-
} else {
|
|
1946
|
-
let nestedType = targetType;
|
|
1947
|
-
for (let j = i;j < path.length; j++) {
|
|
1948
|
-
if (j > i) {
|
|
1949
|
-
const nestedKey = path[j];
|
|
1950
|
-
const nestedTarget = targets[j];
|
|
1951
|
-
if (!nestedKey || !nestedTarget)
|
|
1952
|
-
continue;
|
|
1953
|
-
const nestedTargetType = `Select${pascal(nestedTarget)}`;
|
|
1954
|
-
nestedType = `${nestedType} & { ${nestedKey}: ${isMany[j] ? `${nestedTargetType}[]` : nestedTargetType} }`;
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
const prevKey = path[i - 1];
|
|
1958
|
-
const prevTarget = targets[i - 1];
|
|
1959
|
-
if (prevKey && prevTarget) {
|
|
1960
|
-
parts[parts.length - 1] = `${prevKey}: ${isMany[i - 1] ? `(Select${pascal(prevTarget)} & { ${key}: ${isMany[i] ? `${targetType}[]` : targetType} })[]` : `Select${pascal(prevTarget)} & { ${key}: ${isMany[i] ? `${targetType}[]` : targetType} }`}`;
|
|
1961
|
-
}
|
|
1962
|
-
break;
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
return `${type} & { ${parts.join("; ")} }`;
|
|
1966
|
-
}
|
|
1967
|
-
function buildIncludeSpec(path) {
|
|
1968
|
-
if (path.length === 0)
|
|
1969
|
-
return {};
|
|
1970
|
-
if (path.length === 1)
|
|
1971
|
-
return { [path[0]]: true };
|
|
1972
|
-
let spec = true;
|
|
1973
|
-
for (let i = path.length - 1;i > 0; i--) {
|
|
1974
|
-
const key = path[i];
|
|
1975
|
-
if (!key)
|
|
1976
|
-
continue;
|
|
1977
|
-
spec = { [key]: spec };
|
|
1978
|
-
}
|
|
1979
|
-
const rootKey = path[0];
|
|
1980
|
-
return rootKey ? { [rootKey]: spec } : {};
|
|
1981
|
-
}
|
|
1982
|
-
function generateIncludeMethods(table, graph, opts) {
|
|
1983
|
-
const methods = [];
|
|
1984
|
-
const baseTableName = table.name;
|
|
1985
|
-
if (opts.skipJunctionTables && isJunctionTable(table)) {
|
|
1986
|
-
return methods;
|
|
1987
|
-
}
|
|
1988
|
-
const edges = graph[baseTableName] || {};
|
|
1989
|
-
function explore(currentTable, path, isMany, targets, visited, depth) {
|
|
1990
|
-
if (depth > opts.maxDepth)
|
|
1991
|
-
return;
|
|
1992
|
-
const currentEdges = graph[currentTable] || {};
|
|
1993
|
-
for (const [key, edge] of Object.entries(currentEdges)) {
|
|
1994
|
-
if (visited.has(edge.target))
|
|
1995
|
-
continue;
|
|
1996
|
-
const targetTable = Object.values(graph).find((t) => Object.values(t).some((e) => e.target === edge.target));
|
|
1997
|
-
if (opts.skipJunctionTables && edge.target.includes("_")) {
|
|
1998
|
-
continue;
|
|
1999
|
-
}
|
|
2000
|
-
const newPath = [...path, key];
|
|
2001
|
-
const newIsMany = [...isMany, edge.kind === "many"];
|
|
2002
|
-
const newTargets = [...targets, edge.target];
|
|
2003
|
-
const methodSuffix = pathToMethodSuffix(newPath);
|
|
2004
|
-
methods.push({
|
|
2005
|
-
name: `list${methodSuffix}`,
|
|
2006
|
-
path: newPath,
|
|
2007
|
-
isMany: newIsMany,
|
|
2008
|
-
targets: newTargets,
|
|
2009
|
-
returnType: `(${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)})[]`,
|
|
2010
|
-
includeSpec: buildIncludeSpec(newPath)
|
|
2011
|
-
});
|
|
2012
|
-
methods.push({
|
|
2013
|
-
name: `getByPk${methodSuffix}`,
|
|
2014
|
-
path: newPath,
|
|
2015
|
-
isMany: newIsMany,
|
|
2016
|
-
targets: newTargets,
|
|
2017
|
-
returnType: `${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)} | null`,
|
|
2018
|
-
includeSpec: buildIncludeSpec(newPath)
|
|
2019
|
-
});
|
|
2020
|
-
explore(edge.target, newPath, newIsMany, newTargets, new Set([...visited, edge.target]), depth + 1);
|
|
2021
|
-
}
|
|
2022
|
-
if (depth === 1 && Object.keys(currentEdges).length > 1 && Object.keys(currentEdges).length <= 3) {
|
|
2023
|
-
const edgeEntries = Object.entries(currentEdges);
|
|
2024
|
-
if (edgeEntries.length >= 2) {
|
|
2025
|
-
for (let i = 0;i < edgeEntries.length - 1; i++) {
|
|
2026
|
-
for (let j = i + 1;j < edgeEntries.length; j++) {
|
|
2027
|
-
const entry1 = edgeEntries[i];
|
|
2028
|
-
const entry2 = edgeEntries[j];
|
|
2029
|
-
if (!entry1 || !entry2)
|
|
2030
|
-
continue;
|
|
2031
|
-
const [key1, edge1] = entry1;
|
|
2032
|
-
const [key2, edge2] = entry2;
|
|
2033
|
-
if (opts.skipJunctionTables && (edge1.target.includes("_") || edge2.target.includes("_"))) {
|
|
2034
|
-
continue;
|
|
2035
|
-
}
|
|
2036
|
-
const combinedPath = [key1, key2];
|
|
2037
|
-
const combinedSuffix = `With${pascal(key1)}And${pascal(key2)}`;
|
|
2038
|
-
const type1 = `${key1}: ${edge1.kind === "many" ? `Select${pascal(edge1.target)}[]` : `Select${pascal(edge1.target)}`}`;
|
|
2039
|
-
const type2 = `${key2}: ${edge2.kind === "many" ? `Select${pascal(edge2.target)}[]` : `Select${pascal(edge2.target)}`}`;
|
|
2040
|
-
methods.push({
|
|
2041
|
-
name: `list${combinedSuffix}`,
|
|
2042
|
-
path: combinedPath,
|
|
2043
|
-
isMany: [edge1.kind === "many", edge2.kind === "many"],
|
|
2044
|
-
targets: [edge1.target, edge2.target],
|
|
2045
|
-
returnType: `(Select${pascal(baseTableName)} & { ${type1}; ${type2} })[]`,
|
|
2046
|
-
includeSpec: { [key1]: true, [key2]: true }
|
|
2047
|
-
});
|
|
2048
|
-
methods.push({
|
|
2049
|
-
name: `getByPk${combinedSuffix}`,
|
|
2050
|
-
path: combinedPath,
|
|
2051
|
-
isMany: [edge1.kind === "many", edge2.kind === "many"],
|
|
2052
|
-
targets: [edge1.target, edge2.target],
|
|
2053
|
-
returnType: `(Select${pascal(baseTableName)} & { ${type1}; ${type2} }) | null`,
|
|
2054
|
-
includeSpec: { [key1]: true, [key2]: true }
|
|
2055
|
-
});
|
|
2056
|
-
}
|
|
2057
|
-
}
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
}
|
|
2061
|
-
explore(baseTableName, [], [], [], new Set([baseTableName]), 1);
|
|
2062
|
-
return methods;
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
// src/emit-client.ts
|
|
2066
|
-
function emitClient(table, graph, opts) {
|
|
2080
|
+
init_emit_include_methods();
|
|
2081
|
+
function emitClient(table, graph, opts, model) {
|
|
2067
2082
|
const Type = pascal(table.name);
|
|
2068
2083
|
const ext = opts.useJsExtensions ? ".js" : "";
|
|
2069
2084
|
const pkCols = Array.isArray(table.pk) ? table.pk : table.pk ? [table.pk] : [];
|
|
@@ -2071,10 +2086,11 @@ function emitClient(table, graph, opts) {
|
|
|
2071
2086
|
const hasCompositePk = safePk.length > 1;
|
|
2072
2087
|
const pkType = hasCompositePk ? `{ ${safePk.map((c) => `${c}: string`).join("; ")} }` : `string`;
|
|
2073
2088
|
const pkPathExpr = hasCompositePk ? safePk.map((c) => `pk.${c}`).join(` + "/" + `) : `pk`;
|
|
2089
|
+
const allTables = model ? Object.values(model.tables) : undefined;
|
|
2074
2090
|
const includeMethods = generateIncludeMethods(table, graph, {
|
|
2075
2091
|
maxDepth: opts.includeMethodsDepth ?? 2,
|
|
2076
2092
|
skipJunctionTables: opts.skipJunctionTables ?? true
|
|
2077
|
-
});
|
|
2093
|
+
}, allTables);
|
|
2078
2094
|
const importedTypes = new Set;
|
|
2079
2095
|
importedTypes.add(table.name);
|
|
2080
2096
|
for (const method of includeMethods) {
|
|
@@ -2084,7 +2100,7 @@ function emitClient(table, graph, opts) {
|
|
|
2084
2100
|
}
|
|
2085
2101
|
const typeImports = `import type { Insert${Type}, Update${Type}, Select${Type} } from "./types/${table.name}${ext}";`;
|
|
2086
2102
|
const otherTableImports = [];
|
|
2087
|
-
for (const target of importedTypes) {
|
|
2103
|
+
for (const target of Array.from(importedTypes)) {
|
|
2088
2104
|
if (target !== table.name) {
|
|
2089
2105
|
otherTableImports.push(`import type { Select${pascal(target)} } from "./types/${target}${ext}";`);
|
|
2090
2106
|
}
|
|
@@ -4083,11 +4099,11 @@ async function generate(configPath) {
|
|
|
4083
4099
|
files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
|
|
4084
4100
|
files.push({
|
|
4085
4101
|
path: join(serverDir, "include-builder.ts"),
|
|
4086
|
-
content: emitIncludeBuilder(graph, cfg.
|
|
4102
|
+
content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
|
|
4087
4103
|
});
|
|
4088
4104
|
files.push({
|
|
4089
4105
|
path: join(serverDir, "include-loader.ts"),
|
|
4090
|
-
content: emitIncludeLoader(graph, model, cfg.
|
|
4106
|
+
content: emitIncludeLoader(graph, model, cfg.includeMethodsDepth || 2, cfg.useJsExtensions)
|
|
4091
4107
|
});
|
|
4092
4108
|
files.push({ path: join(serverDir, "logger.ts"), content: emitLogger() });
|
|
4093
4109
|
if (normalizedAuth?.strategy && normalizedAuth.strategy !== "none") {
|
|
@@ -4113,7 +4129,7 @@ async function generate(configPath) {
|
|
|
4113
4129
|
if (serverFramework === "hono") {
|
|
4114
4130
|
routeContent = emitHonoRoutes(table, graph, {
|
|
4115
4131
|
softDeleteColumn: cfg.softDeleteColumn || null,
|
|
4116
|
-
includeDepthLimit: cfg.
|
|
4132
|
+
includeDepthLimit: cfg.includeMethodsDepth || 2,
|
|
4117
4133
|
authStrategy: normalizedAuth?.strategy,
|
|
4118
4134
|
useJsExtensions: cfg.useJsExtensions
|
|
4119
4135
|
});
|
|
@@ -4130,7 +4146,7 @@ async function generate(configPath) {
|
|
|
4130
4146
|
useJsExtensions: cfg.useJsExtensionsClient,
|
|
4131
4147
|
includeMethodsDepth: cfg.includeMethodsDepth ?? 2,
|
|
4132
4148
|
skipJunctionTables: cfg.skipJunctionTables ?? true
|
|
4133
|
-
})
|
|
4149
|
+
}, model)
|
|
4134
4150
|
});
|
|
4135
4151
|
}
|
|
4136
4152
|
files.push({
|
|
@@ -4159,7 +4175,7 @@ async function generate(configPath) {
|
|
|
4159
4175
|
if (process.env.SDK_DEBUG) {
|
|
4160
4176
|
console.log(`[Index] Model has ${Object.keys(model.tables || {}).length} tables before contract generation`);
|
|
4161
4177
|
}
|
|
4162
|
-
const contract = generateUnifiedContract2(model, cfg);
|
|
4178
|
+
const contract = generateUnifiedContract2(model, cfg, graph);
|
|
4163
4179
|
files.push({
|
|
4164
4180
|
path: join(serverDir, "CONTRACT.md"),
|
|
4165
4181
|
content: generateUnifiedContractMarkdown2(contract)
|
package/dist/emit-client.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Table } from "./introspect";
|
|
1
|
+
import type { Table, Model } from "./introspect";
|
|
2
2
|
import type { Graph } from "./rel-classify";
|
|
3
3
|
export declare function emitClient(table: Table, graph: Graph, opts: {
|
|
4
4
|
useJsExtensions?: boolean;
|
|
5
5
|
includeMethodsDepth?: number;
|
|
6
6
|
skipJunctionTables?: boolean;
|
|
7
|
-
}): string;
|
|
7
|
+
}, model?: Model): string;
|
|
8
8
|
export declare function emitClientIndex(tables: Table[], useJsExtensions?: boolean): string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Model } from "./introspect";
|
|
2
2
|
import type { Config, AuthConfig } from "./types";
|
|
3
|
+
import type { Graph } from "./rel-classify";
|
|
3
4
|
export interface UnifiedContract {
|
|
4
5
|
version: string;
|
|
5
6
|
generatedAt: string;
|
|
@@ -70,7 +71,7 @@ export interface RelationshipContract {
|
|
|
70
71
|
*/
|
|
71
72
|
export declare function generateUnifiedContract(model: Model, config: Config & {
|
|
72
73
|
auth?: AuthConfig;
|
|
73
|
-
}): UnifiedContract;
|
|
74
|
+
}, graph?: Graph): UnifiedContract;
|
|
74
75
|
/**
|
|
75
76
|
* Generate markdown documentation for the unified contract
|
|
76
77
|
*/
|
|
@@ -80,4 +81,4 @@ export declare function generateUnifiedContractMarkdown(contract: UnifiedContrac
|
|
|
80
81
|
*/
|
|
81
82
|
export declare function emitUnifiedContract(model: Model, config: Config & {
|
|
82
83
|
auth?: AuthConfig;
|
|
83
|
-
}): string;
|
|
84
|
+
}, graph?: Graph): string;
|
package/dist/index.js
CHANGED
|
@@ -485,6 +485,157 @@ async function ensureDirs(dirs) {
|
|
|
485
485
|
var pascal = (s) => s.split(/[_\s-]+/).map((w) => w?.[0] ? w[0].toUpperCase() + w.slice(1) : "").join("");
|
|
486
486
|
var init_utils = () => {};
|
|
487
487
|
|
|
488
|
+
// src/emit-include-methods.ts
|
|
489
|
+
function isJunctionTable(table) {
|
|
490
|
+
if (!table.name.includes("_"))
|
|
491
|
+
return false;
|
|
492
|
+
const fkColumns = new Set(table.fks.flatMap((fk) => fk.from));
|
|
493
|
+
const nonPkColumns = table.columns.filter((c) => !table.pk.includes(c.name));
|
|
494
|
+
return nonPkColumns.every((c) => fkColumns.has(c.name));
|
|
495
|
+
}
|
|
496
|
+
function pathToMethodSuffix(path) {
|
|
497
|
+
return "With" + path.map((p) => pascal(p)).join("And");
|
|
498
|
+
}
|
|
499
|
+
function buildReturnType(baseTable, path, isMany, targets, graph) {
|
|
500
|
+
const BaseType = `Select${pascal(baseTable)}`;
|
|
501
|
+
if (path.length === 0)
|
|
502
|
+
return BaseType;
|
|
503
|
+
let type = BaseType;
|
|
504
|
+
let currentTable = baseTable;
|
|
505
|
+
const parts = [];
|
|
506
|
+
for (let i = 0;i < path.length; i++) {
|
|
507
|
+
const key = path[i];
|
|
508
|
+
const target = targets[i];
|
|
509
|
+
if (!key || !target)
|
|
510
|
+
continue;
|
|
511
|
+
const targetType = `Select${pascal(target)}`;
|
|
512
|
+
if (i === 0) {
|
|
513
|
+
parts.push(`${key}: ${isMany[i] ? `${targetType}[]` : targetType}`);
|
|
514
|
+
} else {
|
|
515
|
+
let nestedType = targetType;
|
|
516
|
+
for (let j = i;j < path.length; j++) {
|
|
517
|
+
if (j > i) {
|
|
518
|
+
const nestedKey = path[j];
|
|
519
|
+
const nestedTarget = targets[j];
|
|
520
|
+
if (!nestedKey || !nestedTarget)
|
|
521
|
+
continue;
|
|
522
|
+
const nestedTargetType = `Select${pascal(nestedTarget)}`;
|
|
523
|
+
nestedType = `${nestedType} & { ${nestedKey}: ${isMany[j] ? `${nestedTargetType}[]` : nestedTargetType} }`;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const prevKey = path[i - 1];
|
|
527
|
+
const prevTarget = targets[i - 1];
|
|
528
|
+
if (prevKey && prevTarget) {
|
|
529
|
+
parts[parts.length - 1] = `${prevKey}: ${isMany[i - 1] ? `(Select${pascal(prevTarget)} & { ${key}: ${isMany[i] ? `${targetType}[]` : targetType} })[]` : `Select${pascal(prevTarget)} & { ${key}: ${isMany[i] ? `${targetType}[]` : targetType} }`}`;
|
|
530
|
+
}
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return `${type} & { ${parts.join("; ")} }`;
|
|
535
|
+
}
|
|
536
|
+
function buildIncludeSpec(path) {
|
|
537
|
+
if (path.length === 0)
|
|
538
|
+
return {};
|
|
539
|
+
if (path.length === 1)
|
|
540
|
+
return { [path[0]]: true };
|
|
541
|
+
let spec = true;
|
|
542
|
+
for (let i = path.length - 1;i > 0; i--) {
|
|
543
|
+
const key = path[i];
|
|
544
|
+
if (!key)
|
|
545
|
+
continue;
|
|
546
|
+
spec = { [key]: spec };
|
|
547
|
+
}
|
|
548
|
+
const rootKey = path[0];
|
|
549
|
+
return rootKey ? { [rootKey]: spec } : {};
|
|
550
|
+
}
|
|
551
|
+
function generateIncludeMethods(table, graph, opts, allTables) {
|
|
552
|
+
const methods = [];
|
|
553
|
+
const baseTableName = table.name;
|
|
554
|
+
if (opts.skipJunctionTables && isJunctionTable(table)) {
|
|
555
|
+
return methods;
|
|
556
|
+
}
|
|
557
|
+
const edges = graph[baseTableName] || {};
|
|
558
|
+
function explore(currentTable, path, isMany, targets, visited, depth) {
|
|
559
|
+
if (depth > opts.maxDepth)
|
|
560
|
+
return;
|
|
561
|
+
const currentEdges = graph[currentTable] || {};
|
|
562
|
+
for (const [key, edge] of Object.entries(currentEdges)) {
|
|
563
|
+
if (visited.has(edge.target))
|
|
564
|
+
continue;
|
|
565
|
+
if (opts.skipJunctionTables && allTables) {
|
|
566
|
+
const targetTable = allTables.find((t) => t.name === edge.target);
|
|
567
|
+
if (targetTable && isJunctionTable(targetTable)) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const newPath = [...path, key];
|
|
572
|
+
const newIsMany = [...isMany, edge.kind === "many"];
|
|
573
|
+
const newTargets = [...targets, edge.target];
|
|
574
|
+
const methodSuffix = pathToMethodSuffix(newPath);
|
|
575
|
+
methods.push({
|
|
576
|
+
name: `list${methodSuffix}`,
|
|
577
|
+
path: newPath,
|
|
578
|
+
isMany: newIsMany,
|
|
579
|
+
targets: newTargets,
|
|
580
|
+
returnType: `(${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)})[]`,
|
|
581
|
+
includeSpec: buildIncludeSpec(newPath)
|
|
582
|
+
});
|
|
583
|
+
methods.push({
|
|
584
|
+
name: `getByPk${methodSuffix}`,
|
|
585
|
+
path: newPath,
|
|
586
|
+
isMany: newIsMany,
|
|
587
|
+
targets: newTargets,
|
|
588
|
+
returnType: `${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)} | null`,
|
|
589
|
+
includeSpec: buildIncludeSpec(newPath)
|
|
590
|
+
});
|
|
591
|
+
explore(edge.target, newPath, newIsMany, newTargets, new Set([...visited, edge.target]), depth + 1);
|
|
592
|
+
}
|
|
593
|
+
if (depth === 1 && Object.keys(currentEdges).length > 1 && Object.keys(currentEdges).length <= 3) {
|
|
594
|
+
const edgeEntries = Object.entries(currentEdges);
|
|
595
|
+
if (edgeEntries.length >= 2) {
|
|
596
|
+
for (let i = 0;i < edgeEntries.length - 1; i++) {
|
|
597
|
+
for (let j = i + 1;j < edgeEntries.length; j++) {
|
|
598
|
+
const entry1 = edgeEntries[i];
|
|
599
|
+
const entry2 = edgeEntries[j];
|
|
600
|
+
if (!entry1 || !entry2)
|
|
601
|
+
continue;
|
|
602
|
+
const [key1, edge1] = entry1;
|
|
603
|
+
const [key2, edge2] = entry2;
|
|
604
|
+
if (opts.skipJunctionTables && (edge1.target.includes("_") || edge2.target.includes("_"))) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
const combinedPath = [key1, key2];
|
|
608
|
+
const combinedSuffix = `With${pascal(key1)}And${pascal(key2)}`;
|
|
609
|
+
const type1 = `${key1}: ${edge1.kind === "many" ? `Select${pascal(edge1.target)}[]` : `Select${pascal(edge1.target)}`}`;
|
|
610
|
+
const type2 = `${key2}: ${edge2.kind === "many" ? `Select${pascal(edge2.target)}[]` : `Select${pascal(edge2.target)}`}`;
|
|
611
|
+
methods.push({
|
|
612
|
+
name: `list${combinedSuffix}`,
|
|
613
|
+
path: combinedPath,
|
|
614
|
+
isMany: [edge1.kind === "many", edge2.kind === "many"],
|
|
615
|
+
targets: [edge1.target, edge2.target],
|
|
616
|
+
returnType: `(Select${pascal(baseTableName)} & { ${type1}; ${type2} })[]`,
|
|
617
|
+
includeSpec: { [key1]: true, [key2]: true }
|
|
618
|
+
});
|
|
619
|
+
methods.push({
|
|
620
|
+
name: `getByPk${combinedSuffix}`,
|
|
621
|
+
path: combinedPath,
|
|
622
|
+
isMany: [edge1.kind === "many", edge2.kind === "many"],
|
|
623
|
+
targets: [edge1.target, edge2.target],
|
|
624
|
+
returnType: `(Select${pascal(baseTableName)} & { ${type1}; ${type2} }) | null`,
|
|
625
|
+
includeSpec: { [key1]: true, [key2]: true }
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
explore(baseTableName, [], [], [], new Set([baseTableName]), 1);
|
|
633
|
+
return methods;
|
|
634
|
+
}
|
|
635
|
+
var init_emit_include_methods = __esm(() => {
|
|
636
|
+
init_utils();
|
|
637
|
+
});
|
|
638
|
+
|
|
488
639
|
// src/emit-sdk-contract.ts
|
|
489
640
|
var exports_emit_sdk_contract = {};
|
|
490
641
|
__export(exports_emit_sdk_contract, {
|
|
@@ -492,7 +643,7 @@ __export(exports_emit_sdk_contract, {
|
|
|
492
643
|
generateUnifiedContract: () => generateUnifiedContract,
|
|
493
644
|
emitUnifiedContract: () => emitUnifiedContract
|
|
494
645
|
});
|
|
495
|
-
function generateUnifiedContract(model, config) {
|
|
646
|
+
function generateUnifiedContract(model, config, graph) {
|
|
496
647
|
const resources = [];
|
|
497
648
|
const relationships = [];
|
|
498
649
|
const tables = model && model.tables ? Object.values(model.tables) : [];
|
|
@@ -500,7 +651,7 @@ function generateUnifiedContract(model, config) {
|
|
|
500
651
|
console.log(`[SDK Contract] Processing ${tables.length} tables`);
|
|
501
652
|
}
|
|
502
653
|
for (const table of tables) {
|
|
503
|
-
resources.push(generateResourceWithSDK(table, model));
|
|
654
|
+
resources.push(generateResourceWithSDK(table, model, graph, config));
|
|
504
655
|
for (const fk of table.fks) {
|
|
505
656
|
relationships.push({
|
|
506
657
|
from: table.name,
|
|
@@ -618,7 +769,7 @@ function generateSDKAuthExamples(auth) {
|
|
|
618
769
|
});
|
|
619
770
|
return examples;
|
|
620
771
|
}
|
|
621
|
-
function generateResourceWithSDK(table, model) {
|
|
772
|
+
function generateResourceWithSDK(table, model, graph, config) {
|
|
622
773
|
const Type = pascal(table.name);
|
|
623
774
|
const tableName = table.name;
|
|
624
775
|
const basePath = `/v1/${tableName}`;
|
|
@@ -640,11 +791,6 @@ const filtered = await sdk.${tableName}.list({
|
|
|
640
791
|
${table.columns[0]?.name || "field"}_like: 'search',
|
|
641
792
|
order_by: '${table.columns[0]?.name || "created_at"}',
|
|
642
793
|
order_dir: 'desc'
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
// With related data
|
|
646
|
-
const withRelations = await sdk.${tableName}.list({
|
|
647
|
-
include: '${table.fks[0]?.toTable || "related_table"}'
|
|
648
794
|
});`,
|
|
649
795
|
correspondsTo: `GET ${basePath}`
|
|
650
796
|
});
|
|
@@ -658,16 +804,11 @@ const withRelations = await sdk.${tableName}.list({
|
|
|
658
804
|
if (hasSinglePK) {
|
|
659
805
|
sdkMethods.push({
|
|
660
806
|
name: "getByPk",
|
|
661
|
-
signature: `getByPk(${pkField}: string
|
|
807
|
+
signature: `getByPk(${pkField}: string): Promise<${Type} | null>`,
|
|
662
808
|
description: `Get a single ${tableName} by primary key`,
|
|
663
809
|
example: `// Get by ID
|
|
664
810
|
const item = await sdk.${tableName}.getByPk('123e4567-e89b-12d3-a456-426614174000');
|
|
665
811
|
|
|
666
|
-
// With related data
|
|
667
|
-
const withRelations = await sdk.${tableName}.getByPk('123', {
|
|
668
|
-
include: '${table.fks[0]?.toTable || "related_table"}'
|
|
669
|
-
});
|
|
670
|
-
|
|
671
812
|
// Check if exists
|
|
672
813
|
if (item === null) {
|
|
673
814
|
console.log('Not found');
|
|
@@ -678,9 +819,6 @@ if (item === null) {
|
|
|
678
819
|
method: "GET",
|
|
679
820
|
path: `${basePath}/:${pkField}`,
|
|
680
821
|
description: `Get ${tableName} by ID`,
|
|
681
|
-
queryParameters: {
|
|
682
|
-
include: "string - Comma-separated list of related resources"
|
|
683
|
-
},
|
|
684
822
|
responseBody: `${Type}`
|
|
685
823
|
});
|
|
686
824
|
}
|
|
@@ -743,6 +881,31 @@ console.log('Deleted:', deleted);`,
|
|
|
743
881
|
responseBody: `${Type}`
|
|
744
882
|
});
|
|
745
883
|
}
|
|
884
|
+
if (graph && config) {
|
|
885
|
+
const allTables = model && model.tables ? Object.values(model.tables) : undefined;
|
|
886
|
+
const includeMethods = generateIncludeMethods(table, graph, {
|
|
887
|
+
maxDepth: config.includeMethodsDepth ?? 2,
|
|
888
|
+
skipJunctionTables: config.skipJunctionTables ?? true
|
|
889
|
+
}, allTables);
|
|
890
|
+
for (const method of includeMethods) {
|
|
891
|
+
const isGetByPk = method.name.startsWith("getByPk");
|
|
892
|
+
const exampleCall = isGetByPk ? `const result = await sdk.${tableName}.${method.name}('123e4567-e89b-12d3-a456-426614174000');` : `const results = await sdk.${tableName}.${method.name}();
|
|
893
|
+
|
|
894
|
+
// With filters and pagination
|
|
895
|
+
const filtered = await sdk.${tableName}.${method.name}({
|
|
896
|
+
limit: 20,
|
|
897
|
+
offset: 0,
|
|
898
|
+
where: { /* filter conditions */ }
|
|
899
|
+
});`;
|
|
900
|
+
sdkMethods.push({
|
|
901
|
+
name: method.name,
|
|
902
|
+
signature: `${method.name}(${isGetByPk ? `${pkField}: string` : "params?: ListParams"}): ${method.returnType}`,
|
|
903
|
+
description: `Get ${tableName} with included ${method.path.join(", ")} data`,
|
|
904
|
+
example: exampleCall,
|
|
905
|
+
correspondsTo: `POST ${basePath}/list`
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
746
909
|
const fields = table.columns.map((col) => generateFieldContract(col, table));
|
|
747
910
|
return {
|
|
748
911
|
name: Type,
|
|
@@ -891,8 +1054,7 @@ function generateQueryParams(table) {
|
|
|
891
1054
|
limit: "number - Max records to return (default: 50)",
|
|
892
1055
|
offset: "number - Records to skip",
|
|
893
1056
|
order_by: "string - Field to sort by",
|
|
894
|
-
order_dir: "'asc' | 'desc' - Sort direction"
|
|
895
|
-
include: "string - Related resources to include"
|
|
1057
|
+
order_dir: "'asc' | 'desc' - Sort direction"
|
|
896
1058
|
};
|
|
897
1059
|
let filterCount = 0;
|
|
898
1060
|
for (const col of table.columns) {
|
|
@@ -1079,8 +1241,8 @@ function generateUnifiedContractMarkdown(contract) {
|
|
|
1079
1241
|
return lines.join(`
|
|
1080
1242
|
`);
|
|
1081
1243
|
}
|
|
1082
|
-
function emitUnifiedContract(model, config) {
|
|
1083
|
-
const contract = generateUnifiedContract(model, config);
|
|
1244
|
+
function emitUnifiedContract(model, config, graph) {
|
|
1245
|
+
const contract = generateUnifiedContract(model, config, graph);
|
|
1084
1246
|
const contractJson = JSON.stringify(contract, null, 2);
|
|
1085
1247
|
return `/**
|
|
1086
1248
|
* Unified API & SDK Contract
|
|
@@ -1136,6 +1298,7 @@ import type * as Types from './client/types';
|
|
|
1136
1298
|
}
|
|
1137
1299
|
var init_emit_sdk_contract = __esm(() => {
|
|
1138
1300
|
init_utils();
|
|
1301
|
+
init_emit_include_methods();
|
|
1139
1302
|
});
|
|
1140
1303
|
|
|
1141
1304
|
// src/index.ts
|
|
@@ -1651,156 +1814,8 @@ ${hasAuth ? `
|
|
|
1651
1814
|
|
|
1652
1815
|
// src/emit-client.ts
|
|
1653
1816
|
init_utils();
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
init_utils();
|
|
1657
|
-
function isJunctionTable(table) {
|
|
1658
|
-
if (!table.name.includes("_"))
|
|
1659
|
-
return false;
|
|
1660
|
-
const fkColumns = new Set(table.fks.flatMap((fk) => fk.from));
|
|
1661
|
-
const nonPkColumns = table.columns.filter((c) => !table.pk.includes(c.name));
|
|
1662
|
-
return nonPkColumns.every((c) => fkColumns.has(c.name));
|
|
1663
|
-
}
|
|
1664
|
-
function pathToMethodSuffix(path) {
|
|
1665
|
-
return "With" + path.map((p) => pascal(p)).join("And");
|
|
1666
|
-
}
|
|
1667
|
-
function buildReturnType(baseTable, path, isMany, targets, graph) {
|
|
1668
|
-
const BaseType = `Select${pascal(baseTable)}`;
|
|
1669
|
-
if (path.length === 0)
|
|
1670
|
-
return BaseType;
|
|
1671
|
-
let type = BaseType;
|
|
1672
|
-
let currentTable = baseTable;
|
|
1673
|
-
const parts = [];
|
|
1674
|
-
for (let i = 0;i < path.length; i++) {
|
|
1675
|
-
const key = path[i];
|
|
1676
|
-
const target = targets[i];
|
|
1677
|
-
if (!key || !target)
|
|
1678
|
-
continue;
|
|
1679
|
-
const targetType = `Select${pascal(target)}`;
|
|
1680
|
-
if (i === 0) {
|
|
1681
|
-
parts.push(`${key}: ${isMany[i] ? `${targetType}[]` : targetType}`);
|
|
1682
|
-
} else {
|
|
1683
|
-
let nestedType = targetType;
|
|
1684
|
-
for (let j = i;j < path.length; j++) {
|
|
1685
|
-
if (j > i) {
|
|
1686
|
-
const nestedKey = path[j];
|
|
1687
|
-
const nestedTarget = targets[j];
|
|
1688
|
-
if (!nestedKey || !nestedTarget)
|
|
1689
|
-
continue;
|
|
1690
|
-
const nestedTargetType = `Select${pascal(nestedTarget)}`;
|
|
1691
|
-
nestedType = `${nestedType} & { ${nestedKey}: ${isMany[j] ? `${nestedTargetType}[]` : nestedTargetType} }`;
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
const prevKey = path[i - 1];
|
|
1695
|
-
const prevTarget = targets[i - 1];
|
|
1696
|
-
if (prevKey && prevTarget) {
|
|
1697
|
-
parts[parts.length - 1] = `${prevKey}: ${isMany[i - 1] ? `(Select${pascal(prevTarget)} & { ${key}: ${isMany[i] ? `${targetType}[]` : targetType} })[]` : `Select${pascal(prevTarget)} & { ${key}: ${isMany[i] ? `${targetType}[]` : targetType} }`}`;
|
|
1698
|
-
}
|
|
1699
|
-
break;
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
return `${type} & { ${parts.join("; ")} }`;
|
|
1703
|
-
}
|
|
1704
|
-
function buildIncludeSpec(path) {
|
|
1705
|
-
if (path.length === 0)
|
|
1706
|
-
return {};
|
|
1707
|
-
if (path.length === 1)
|
|
1708
|
-
return { [path[0]]: true };
|
|
1709
|
-
let spec = true;
|
|
1710
|
-
for (let i = path.length - 1;i > 0; i--) {
|
|
1711
|
-
const key = path[i];
|
|
1712
|
-
if (!key)
|
|
1713
|
-
continue;
|
|
1714
|
-
spec = { [key]: spec };
|
|
1715
|
-
}
|
|
1716
|
-
const rootKey = path[0];
|
|
1717
|
-
return rootKey ? { [rootKey]: spec } : {};
|
|
1718
|
-
}
|
|
1719
|
-
function generateIncludeMethods(table, graph, opts) {
|
|
1720
|
-
const methods = [];
|
|
1721
|
-
const baseTableName = table.name;
|
|
1722
|
-
if (opts.skipJunctionTables && isJunctionTable(table)) {
|
|
1723
|
-
return methods;
|
|
1724
|
-
}
|
|
1725
|
-
const edges = graph[baseTableName] || {};
|
|
1726
|
-
function explore(currentTable, path, isMany, targets, visited, depth) {
|
|
1727
|
-
if (depth > opts.maxDepth)
|
|
1728
|
-
return;
|
|
1729
|
-
const currentEdges = graph[currentTable] || {};
|
|
1730
|
-
for (const [key, edge] of Object.entries(currentEdges)) {
|
|
1731
|
-
if (visited.has(edge.target))
|
|
1732
|
-
continue;
|
|
1733
|
-
const targetTable = Object.values(graph).find((t) => Object.values(t).some((e) => e.target === edge.target));
|
|
1734
|
-
if (opts.skipJunctionTables && edge.target.includes("_")) {
|
|
1735
|
-
continue;
|
|
1736
|
-
}
|
|
1737
|
-
const newPath = [...path, key];
|
|
1738
|
-
const newIsMany = [...isMany, edge.kind === "many"];
|
|
1739
|
-
const newTargets = [...targets, edge.target];
|
|
1740
|
-
const methodSuffix = pathToMethodSuffix(newPath);
|
|
1741
|
-
methods.push({
|
|
1742
|
-
name: `list${methodSuffix}`,
|
|
1743
|
-
path: newPath,
|
|
1744
|
-
isMany: newIsMany,
|
|
1745
|
-
targets: newTargets,
|
|
1746
|
-
returnType: `(${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)})[]`,
|
|
1747
|
-
includeSpec: buildIncludeSpec(newPath)
|
|
1748
|
-
});
|
|
1749
|
-
methods.push({
|
|
1750
|
-
name: `getByPk${methodSuffix}`,
|
|
1751
|
-
path: newPath,
|
|
1752
|
-
isMany: newIsMany,
|
|
1753
|
-
targets: newTargets,
|
|
1754
|
-
returnType: `${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)} | null`,
|
|
1755
|
-
includeSpec: buildIncludeSpec(newPath)
|
|
1756
|
-
});
|
|
1757
|
-
explore(edge.target, newPath, newIsMany, newTargets, new Set([...visited, edge.target]), depth + 1);
|
|
1758
|
-
}
|
|
1759
|
-
if (depth === 1 && Object.keys(currentEdges).length > 1 && Object.keys(currentEdges).length <= 3) {
|
|
1760
|
-
const edgeEntries = Object.entries(currentEdges);
|
|
1761
|
-
if (edgeEntries.length >= 2) {
|
|
1762
|
-
for (let i = 0;i < edgeEntries.length - 1; i++) {
|
|
1763
|
-
for (let j = i + 1;j < edgeEntries.length; j++) {
|
|
1764
|
-
const entry1 = edgeEntries[i];
|
|
1765
|
-
const entry2 = edgeEntries[j];
|
|
1766
|
-
if (!entry1 || !entry2)
|
|
1767
|
-
continue;
|
|
1768
|
-
const [key1, edge1] = entry1;
|
|
1769
|
-
const [key2, edge2] = entry2;
|
|
1770
|
-
if (opts.skipJunctionTables && (edge1.target.includes("_") || edge2.target.includes("_"))) {
|
|
1771
|
-
continue;
|
|
1772
|
-
}
|
|
1773
|
-
const combinedPath = [key1, key2];
|
|
1774
|
-
const combinedSuffix = `With${pascal(key1)}And${pascal(key2)}`;
|
|
1775
|
-
const type1 = `${key1}: ${edge1.kind === "many" ? `Select${pascal(edge1.target)}[]` : `Select${pascal(edge1.target)}`}`;
|
|
1776
|
-
const type2 = `${key2}: ${edge2.kind === "many" ? `Select${pascal(edge2.target)}[]` : `Select${pascal(edge2.target)}`}`;
|
|
1777
|
-
methods.push({
|
|
1778
|
-
name: `list${combinedSuffix}`,
|
|
1779
|
-
path: combinedPath,
|
|
1780
|
-
isMany: [edge1.kind === "many", edge2.kind === "many"],
|
|
1781
|
-
targets: [edge1.target, edge2.target],
|
|
1782
|
-
returnType: `(Select${pascal(baseTableName)} & { ${type1}; ${type2} })[]`,
|
|
1783
|
-
includeSpec: { [key1]: true, [key2]: true }
|
|
1784
|
-
});
|
|
1785
|
-
methods.push({
|
|
1786
|
-
name: `getByPk${combinedSuffix}`,
|
|
1787
|
-
path: combinedPath,
|
|
1788
|
-
isMany: [edge1.kind === "many", edge2.kind === "many"],
|
|
1789
|
-
targets: [edge1.target, edge2.target],
|
|
1790
|
-
returnType: `(Select${pascal(baseTableName)} & { ${type1}; ${type2} }) | null`,
|
|
1791
|
-
includeSpec: { [key1]: true, [key2]: true }
|
|
1792
|
-
});
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
explore(baseTableName, [], [], [], new Set([baseTableName]), 1);
|
|
1799
|
-
return methods;
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
// src/emit-client.ts
|
|
1803
|
-
function emitClient(table, graph, opts) {
|
|
1817
|
+
init_emit_include_methods();
|
|
1818
|
+
function emitClient(table, graph, opts, model) {
|
|
1804
1819
|
const Type = pascal(table.name);
|
|
1805
1820
|
const ext = opts.useJsExtensions ? ".js" : "";
|
|
1806
1821
|
const pkCols = Array.isArray(table.pk) ? table.pk : table.pk ? [table.pk] : [];
|
|
@@ -1808,10 +1823,11 @@ function emitClient(table, graph, opts) {
|
|
|
1808
1823
|
const hasCompositePk = safePk.length > 1;
|
|
1809
1824
|
const pkType = hasCompositePk ? `{ ${safePk.map((c) => `${c}: string`).join("; ")} }` : `string`;
|
|
1810
1825
|
const pkPathExpr = hasCompositePk ? safePk.map((c) => `pk.${c}`).join(` + "/" + `) : `pk`;
|
|
1826
|
+
const allTables = model ? Object.values(model.tables) : undefined;
|
|
1811
1827
|
const includeMethods = generateIncludeMethods(table, graph, {
|
|
1812
1828
|
maxDepth: opts.includeMethodsDepth ?? 2,
|
|
1813
1829
|
skipJunctionTables: opts.skipJunctionTables ?? true
|
|
1814
|
-
});
|
|
1830
|
+
}, allTables);
|
|
1815
1831
|
const importedTypes = new Set;
|
|
1816
1832
|
importedTypes.add(table.name);
|
|
1817
1833
|
for (const method of includeMethods) {
|
|
@@ -1821,7 +1837,7 @@ function emitClient(table, graph, opts) {
|
|
|
1821
1837
|
}
|
|
1822
1838
|
const typeImports = `import type { Insert${Type}, Update${Type}, Select${Type} } from "./types/${table.name}${ext}";`;
|
|
1823
1839
|
const otherTableImports = [];
|
|
1824
|
-
for (const target of importedTypes) {
|
|
1840
|
+
for (const target of Array.from(importedTypes)) {
|
|
1825
1841
|
if (target !== table.name) {
|
|
1826
1842
|
otherTableImports.push(`import type { Select${pascal(target)} } from "./types/${target}${ext}";`);
|
|
1827
1843
|
}
|
|
@@ -3820,11 +3836,11 @@ async function generate(configPath) {
|
|
|
3820
3836
|
files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
|
|
3821
3837
|
files.push({
|
|
3822
3838
|
path: join(serverDir, "include-builder.ts"),
|
|
3823
|
-
content: emitIncludeBuilder(graph, cfg.
|
|
3839
|
+
content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
|
|
3824
3840
|
});
|
|
3825
3841
|
files.push({
|
|
3826
3842
|
path: join(serverDir, "include-loader.ts"),
|
|
3827
|
-
content: emitIncludeLoader(graph, model, cfg.
|
|
3843
|
+
content: emitIncludeLoader(graph, model, cfg.includeMethodsDepth || 2, cfg.useJsExtensions)
|
|
3828
3844
|
});
|
|
3829
3845
|
files.push({ path: join(serverDir, "logger.ts"), content: emitLogger() });
|
|
3830
3846
|
if (normalizedAuth?.strategy && normalizedAuth.strategy !== "none") {
|
|
@@ -3850,7 +3866,7 @@ async function generate(configPath) {
|
|
|
3850
3866
|
if (serverFramework === "hono") {
|
|
3851
3867
|
routeContent = emitHonoRoutes(table, graph, {
|
|
3852
3868
|
softDeleteColumn: cfg.softDeleteColumn || null,
|
|
3853
|
-
includeDepthLimit: cfg.
|
|
3869
|
+
includeDepthLimit: cfg.includeMethodsDepth || 2,
|
|
3854
3870
|
authStrategy: normalizedAuth?.strategy,
|
|
3855
3871
|
useJsExtensions: cfg.useJsExtensions
|
|
3856
3872
|
});
|
|
@@ -3867,7 +3883,7 @@ async function generate(configPath) {
|
|
|
3867
3883
|
useJsExtensions: cfg.useJsExtensionsClient,
|
|
3868
3884
|
includeMethodsDepth: cfg.includeMethodsDepth ?? 2,
|
|
3869
3885
|
skipJunctionTables: cfg.skipJunctionTables ?? true
|
|
3870
|
-
})
|
|
3886
|
+
}, model)
|
|
3871
3887
|
});
|
|
3872
3888
|
}
|
|
3873
3889
|
files.push({
|
|
@@ -3896,7 +3912,7 @@ async function generate(configPath) {
|
|
|
3896
3912
|
if (process.env.SDK_DEBUG) {
|
|
3897
3913
|
console.log(`[Index] Model has ${Object.keys(model.tables || {}).length} tables before contract generation`);
|
|
3898
3914
|
}
|
|
3899
|
-
const contract = generateUnifiedContract2(model, cfg);
|
|
3915
|
+
const contract = generateUnifiedContract2(model, cfg, graph);
|
|
3900
3916
|
files.push({
|
|
3901
3917
|
path: join(serverDir, "CONTRACT.md"),
|
|
3902
3918
|
content: generateUnifiedContractMarkdown2(contract)
|
package/dist/types.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface Config {
|
|
|
24
24
|
outServer?: string;
|
|
25
25
|
outClient?: string;
|
|
26
26
|
softDeleteColumn?: string | null;
|
|
27
|
-
|
|
27
|
+
dateType?: "date" | "string";
|
|
28
28
|
includeMethodsDepth?: number;
|
|
29
29
|
skipJunctionTables?: boolean;
|
|
30
30
|
serverFramework?: "hono" | "express" | "fastify";
|