postgresdk 0.9.0 → 0.9.2

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 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, params?: GetParams): Promise<${Type} | null>`,
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,157 +2077,7 @@ ${hasAuth ? `
1914
2077
 
1915
2078
  // src/emit-client.ts
1916
2079
  init_utils();
1917
-
1918
- // src/emit-include-methods.ts
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, allTables) {
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
- if (opts.skipJunctionTables && allTables) {
1997
- const targetTable = allTables.find((t) => t.name === edge.target);
1998
- if (targetTable && isJunctionTable(targetTable)) {
1999
- continue;
2000
- }
2001
- }
2002
- const newPath = [...path, key];
2003
- const newIsMany = [...isMany, edge.kind === "many"];
2004
- const newTargets = [...targets, edge.target];
2005
- const methodSuffix = pathToMethodSuffix(newPath);
2006
- methods.push({
2007
- name: `list${methodSuffix}`,
2008
- path: newPath,
2009
- isMany: newIsMany,
2010
- targets: newTargets,
2011
- returnType: `(${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)})[]`,
2012
- includeSpec: buildIncludeSpec(newPath)
2013
- });
2014
- methods.push({
2015
- name: `getByPk${methodSuffix}`,
2016
- path: newPath,
2017
- isMany: newIsMany,
2018
- targets: newTargets,
2019
- returnType: `${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)} | null`,
2020
- includeSpec: buildIncludeSpec(newPath)
2021
- });
2022
- explore(edge.target, newPath, newIsMany, newTargets, new Set([...visited, edge.target]), depth + 1);
2023
- }
2024
- if (depth === 1 && Object.keys(currentEdges).length > 1 && Object.keys(currentEdges).length <= 3) {
2025
- const edgeEntries = Object.entries(currentEdges);
2026
- if (edgeEntries.length >= 2) {
2027
- for (let i = 0;i < edgeEntries.length - 1; i++) {
2028
- for (let j = i + 1;j < edgeEntries.length; j++) {
2029
- const entry1 = edgeEntries[i];
2030
- const entry2 = edgeEntries[j];
2031
- if (!entry1 || !entry2)
2032
- continue;
2033
- const [key1, edge1] = entry1;
2034
- const [key2, edge2] = entry2;
2035
- if (opts.skipJunctionTables && (edge1.target.includes("_") || edge2.target.includes("_"))) {
2036
- continue;
2037
- }
2038
- const combinedPath = [key1, key2];
2039
- const combinedSuffix = `With${pascal(key1)}And${pascal(key2)}`;
2040
- const type1 = `${key1}: ${edge1.kind === "many" ? `Select${pascal(edge1.target)}[]` : `Select${pascal(edge1.target)}`}`;
2041
- const type2 = `${key2}: ${edge2.kind === "many" ? `Select${pascal(edge2.target)}[]` : `Select${pascal(edge2.target)}`}`;
2042
- methods.push({
2043
- name: `list${combinedSuffix}`,
2044
- path: combinedPath,
2045
- isMany: [edge1.kind === "many", edge2.kind === "many"],
2046
- targets: [edge1.target, edge2.target],
2047
- returnType: `(Select${pascal(baseTableName)} & { ${type1}; ${type2} })[]`,
2048
- includeSpec: { [key1]: true, [key2]: true }
2049
- });
2050
- methods.push({
2051
- name: `getByPk${combinedSuffix}`,
2052
- path: combinedPath,
2053
- isMany: [edge1.kind === "many", edge2.kind === "many"],
2054
- targets: [edge1.target, edge2.target],
2055
- returnType: `(Select${pascal(baseTableName)} & { ${type1}; ${type2} }) | null`,
2056
- includeSpec: { [key1]: true, [key2]: true }
2057
- });
2058
- }
2059
- }
2060
- }
2061
- }
2062
- }
2063
- explore(baseTableName, [], [], [], new Set([baseTableName]), 1);
2064
- return methods;
2065
- }
2066
-
2067
- // src/emit-client.ts
2080
+ init_emit_include_methods();
2068
2081
  function emitClient(table, graph, opts, model) {
2069
2082
  const Type = pascal(table.name);
2070
2083
  const ext = opts.useJsExtensions ? ".js" : "";
@@ -4146,26 +4159,30 @@ async function generate(configPath) {
4146
4159
  content: emitHonoRouter(Object.values(model.tables), !!normalizedAuth?.strategy && normalizedAuth.strategy !== "none", cfg.useJsExtensions)
4147
4160
  });
4148
4161
  }
4149
- const clientFiles = files.filter((f) => {
4150
- return f.path.includes(clientDir);
4162
+ const { generateUnifiedContract: generateUnifiedContract2, generateUnifiedContractMarkdown: generateUnifiedContractMarkdown2 } = await Promise.resolve().then(() => (init_emit_sdk_contract(), exports_emit_sdk_contract));
4163
+ if (process.env.SDK_DEBUG) {
4164
+ console.log(`[Index] Model has ${Object.keys(model.tables || {}).length} tables before contract generation`);
4165
+ }
4166
+ const contract = generateUnifiedContract2(model, cfg, graph);
4167
+ files.push({
4168
+ path: join(serverDir, "CONTRACT.md"),
4169
+ content: generateUnifiedContractMarkdown2(contract)
4151
4170
  });
4152
4171
  files.push({
4153
- path: join(serverDir, "sdk-bundle.ts"),
4154
- content: emitSdkBundle(clientFiles, clientDir)
4172
+ path: join(clientDir, "CONTRACT.md"),
4173
+ content: generateUnifiedContractMarkdown2(contract)
4155
4174
  });
4156
- const contractCode = emitUnifiedContract(model, cfg);
4175
+ const contractCode = emitUnifiedContract(model, cfg, graph);
4157
4176
  files.push({
4158
4177
  path: join(serverDir, "contract.ts"),
4159
4178
  content: contractCode
4160
4179
  });
4161
- const { generateUnifiedContract: generateUnifiedContract2, generateUnifiedContractMarkdown: generateUnifiedContractMarkdown2 } = await Promise.resolve().then(() => (init_emit_sdk_contract(), exports_emit_sdk_contract));
4162
- if (process.env.SDK_DEBUG) {
4163
- console.log(`[Index] Model has ${Object.keys(model.tables || {}).length} tables before contract generation`);
4164
- }
4165
- const contract = generateUnifiedContract2(model, cfg);
4180
+ const clientFiles = files.filter((f) => {
4181
+ return f.path.includes(clientDir);
4182
+ });
4166
4183
  files.push({
4167
- path: join(serverDir, "CONTRACT.md"),
4168
- content: generateUnifiedContractMarkdown2(contract)
4184
+ path: join(serverDir, "sdk-bundle.ts"),
4185
+ content: emitSdkBundle(clientFiles, clientDir)
4169
4186
  });
4170
4187
  if (generateTests) {
4171
4188
  console.log("\uD83E\uDDEA Generating tests...");
@@ -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, params?: GetParams): Promise<${Type} | null>`,
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,157 +1814,7 @@ ${hasAuth ? `
1651
1814
 
1652
1815
  // src/emit-client.ts
1653
1816
  init_utils();
1654
-
1655
- // src/emit-include-methods.ts
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, allTables) {
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
- if (opts.skipJunctionTables && allTables) {
1734
- const targetTable = allTables.find((t) => t.name === edge.target);
1735
- if (targetTable && isJunctionTable(targetTable)) {
1736
- continue;
1737
- }
1738
- }
1739
- const newPath = [...path, key];
1740
- const newIsMany = [...isMany, edge.kind === "many"];
1741
- const newTargets = [...targets, edge.target];
1742
- const methodSuffix = pathToMethodSuffix(newPath);
1743
- methods.push({
1744
- name: `list${methodSuffix}`,
1745
- path: newPath,
1746
- isMany: newIsMany,
1747
- targets: newTargets,
1748
- returnType: `(${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)})[]`,
1749
- includeSpec: buildIncludeSpec(newPath)
1750
- });
1751
- methods.push({
1752
- name: `getByPk${methodSuffix}`,
1753
- path: newPath,
1754
- isMany: newIsMany,
1755
- targets: newTargets,
1756
- returnType: `${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)} | null`,
1757
- includeSpec: buildIncludeSpec(newPath)
1758
- });
1759
- explore(edge.target, newPath, newIsMany, newTargets, new Set([...visited, edge.target]), depth + 1);
1760
- }
1761
- if (depth === 1 && Object.keys(currentEdges).length > 1 && Object.keys(currentEdges).length <= 3) {
1762
- const edgeEntries = Object.entries(currentEdges);
1763
- if (edgeEntries.length >= 2) {
1764
- for (let i = 0;i < edgeEntries.length - 1; i++) {
1765
- for (let j = i + 1;j < edgeEntries.length; j++) {
1766
- const entry1 = edgeEntries[i];
1767
- const entry2 = edgeEntries[j];
1768
- if (!entry1 || !entry2)
1769
- continue;
1770
- const [key1, edge1] = entry1;
1771
- const [key2, edge2] = entry2;
1772
- if (opts.skipJunctionTables && (edge1.target.includes("_") || edge2.target.includes("_"))) {
1773
- continue;
1774
- }
1775
- const combinedPath = [key1, key2];
1776
- const combinedSuffix = `With${pascal(key1)}And${pascal(key2)}`;
1777
- const type1 = `${key1}: ${edge1.kind === "many" ? `Select${pascal(edge1.target)}[]` : `Select${pascal(edge1.target)}`}`;
1778
- const type2 = `${key2}: ${edge2.kind === "many" ? `Select${pascal(edge2.target)}[]` : `Select${pascal(edge2.target)}`}`;
1779
- methods.push({
1780
- name: `list${combinedSuffix}`,
1781
- path: combinedPath,
1782
- isMany: [edge1.kind === "many", edge2.kind === "many"],
1783
- targets: [edge1.target, edge2.target],
1784
- returnType: `(Select${pascal(baseTableName)} & { ${type1}; ${type2} })[]`,
1785
- includeSpec: { [key1]: true, [key2]: true }
1786
- });
1787
- methods.push({
1788
- name: `getByPk${combinedSuffix}`,
1789
- path: combinedPath,
1790
- isMany: [edge1.kind === "many", edge2.kind === "many"],
1791
- targets: [edge1.target, edge2.target],
1792
- returnType: `(Select${pascal(baseTableName)} & { ${type1}; ${type2} }) | null`,
1793
- includeSpec: { [key1]: true, [key2]: true }
1794
- });
1795
- }
1796
- }
1797
- }
1798
- }
1799
- }
1800
- explore(baseTableName, [], [], [], new Set([baseTableName]), 1);
1801
- return methods;
1802
- }
1803
-
1804
- // src/emit-client.ts
1817
+ init_emit_include_methods();
1805
1818
  function emitClient(table, graph, opts, model) {
1806
1819
  const Type = pascal(table.name);
1807
1820
  const ext = opts.useJsExtensions ? ".js" : "";
@@ -3883,26 +3896,30 @@ async function generate(configPath) {
3883
3896
  content: emitHonoRouter(Object.values(model.tables), !!normalizedAuth?.strategy && normalizedAuth.strategy !== "none", cfg.useJsExtensions)
3884
3897
  });
3885
3898
  }
3886
- const clientFiles = files.filter((f) => {
3887
- return f.path.includes(clientDir);
3899
+ const { generateUnifiedContract: generateUnifiedContract2, generateUnifiedContractMarkdown: generateUnifiedContractMarkdown2 } = await Promise.resolve().then(() => (init_emit_sdk_contract(), exports_emit_sdk_contract));
3900
+ if (process.env.SDK_DEBUG) {
3901
+ console.log(`[Index] Model has ${Object.keys(model.tables || {}).length} tables before contract generation`);
3902
+ }
3903
+ const contract = generateUnifiedContract2(model, cfg, graph);
3904
+ files.push({
3905
+ path: join(serverDir, "CONTRACT.md"),
3906
+ content: generateUnifiedContractMarkdown2(contract)
3888
3907
  });
3889
3908
  files.push({
3890
- path: join(serverDir, "sdk-bundle.ts"),
3891
- content: emitSdkBundle(clientFiles, clientDir)
3909
+ path: join(clientDir, "CONTRACT.md"),
3910
+ content: generateUnifiedContractMarkdown2(contract)
3892
3911
  });
3893
- const contractCode = emitUnifiedContract(model, cfg);
3912
+ const contractCode = emitUnifiedContract(model, cfg, graph);
3894
3913
  files.push({
3895
3914
  path: join(serverDir, "contract.ts"),
3896
3915
  content: contractCode
3897
3916
  });
3898
- const { generateUnifiedContract: generateUnifiedContract2, generateUnifiedContractMarkdown: generateUnifiedContractMarkdown2 } = await Promise.resolve().then(() => (init_emit_sdk_contract(), exports_emit_sdk_contract));
3899
- if (process.env.SDK_DEBUG) {
3900
- console.log(`[Index] Model has ${Object.keys(model.tables || {}).length} tables before contract generation`);
3901
- }
3902
- const contract = generateUnifiedContract2(model, cfg);
3917
+ const clientFiles = files.filter((f) => {
3918
+ return f.path.includes(clientDir);
3919
+ });
3903
3920
  files.push({
3904
- path: join(serverDir, "CONTRACT.md"),
3905
- content: generateUnifiedContractMarkdown2(contract)
3921
+ path: join(serverDir, "sdk-bundle.ts"),
3922
+ content: emitSdkBundle(clientFiles, clientDir)
3906
3923
  });
3907
3924
  if (generateTests) {
3908
3925
  console.log("\uD83E\uDDEA Generating tests...");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {