postgresdk 0.1.2-alpha.2 → 0.1.2-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -348,7 +348,23 @@ export default {
348
348
  ### API Key Authentication
349
349
 
350
350
  ```typescript
351
- // postgresdk.config.ts
351
+ // postgresdk.config.ts - Simplified syntax
352
+ export default {
353
+ connectionString: "...",
354
+ auth: {
355
+ apiKey: "your-api-key" // Single key shorthand
356
+ }
357
+ };
358
+
359
+ // Or multiple keys
360
+ export default {
361
+ connectionString: "...",
362
+ auth: {
363
+ apiKeys: ["key1", "key2", "key3"]
364
+ }
365
+ };
366
+
367
+ // Or full syntax with custom header
352
368
  export default {
353
369
  connectionString: "...",
354
370
  auth: {
@@ -373,7 +389,15 @@ const sdk = new SDK({
373
389
  ### JWT Authentication (HS256)
374
390
 
375
391
  ```typescript
376
- // postgresdk.config.ts
392
+ // postgresdk.config.ts - Simplified syntax
393
+ export default {
394
+ connectionString: "...",
395
+ auth: {
396
+ jwt: "your-secret-key" // Shared secret shorthand
397
+ }
398
+ };
399
+
400
+ // Or full syntax with issuer/audience validation
377
401
  export default {
378
402
  connectionString: "...",
379
403
  auth: {
package/dist/cli.js CHANGED
@@ -958,13 +958,116 @@ function emitClient(table) {
958
958
  const pkType = hasCompositePk ? `{ ${safePk.map((c) => `${c}: string`).join("; ")} }` : `string`;
959
959
  const pkPathExpr = hasCompositePk ? safePk.map((c) => `pk.${c}`).join(` + "/" + `) : `pk`;
960
960
  return `/* Generated. Do not edit. */
961
+ import { BaseClient } from "./base-client";
961
962
  import type { ${Type}IncludeSpec } from "./include-spec";
962
963
  import type { Insert${Type}, Update${Type}, Select${Type} } from "./types/${table.name}";
963
964
 
964
- type HeaderMap = Record<string, string>;
965
- type AuthHeadersProvider = () => Promise<HeaderMap> | HeaderMap;
965
+ /**
966
+ * Client for ${table.name} table operations
967
+ */
968
+ export class ${Type}Client extends BaseClient {
969
+ private readonly resource = "/v1/${table.name}";
970
+
971
+ async create(data: Insert${Type}): Promise<Select${Type}> {
972
+ return this.post<Select${Type}>(this.resource, data);
973
+ }
974
+
975
+ async getByPk(pk: ${pkType}): Promise<Select${Type} | null> {
976
+ const path = ${pkPathExpr};
977
+ return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
978
+ }
979
+
980
+ async list(params?: {
981
+ include?: ${Type}IncludeSpec;
982
+ limit?: number;
983
+ offset?: number;
984
+ where?: any;
985
+ orderBy?: string;
986
+ order?: "asc" | "desc";
987
+ }): Promise<Select${Type}[]> {
988
+ return this.post<Select${Type}[]>(\`\${this.resource}/list\`, params ?? {});
989
+ }
990
+
991
+ async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null> {
992
+ const path = ${pkPathExpr};
993
+ return this.patch<Select${Type} | null>(\`\${this.resource}/\${path}\`, patch);
994
+ }
995
+
996
+ async delete(pk: ${pkType}): Promise<Select${Type} | null> {
997
+ const path = ${pkPathExpr};
998
+ return this.del<Select${Type} | null>(\`\${this.resource}/\${path}\`);
999
+ }
1000
+ }
1001
+ `;
1002
+ }
1003
+ function emitClientIndex(tables) {
1004
+ let out = `/* Generated. Do not edit. */
1005
+ `;
1006
+ out += `import { BaseClient, AuthConfig } from "./base-client";
1007
+ `;
1008
+ for (const t of tables) {
1009
+ out += `import { ${pascal(t.name)}Client } from "./${t.name}";
1010
+ `;
1011
+ }
1012
+ out += `
1013
+ // Re-export auth types for convenience
1014
+ `;
1015
+ out += `export type { AuthConfig as SDKAuth, AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client";
1016
+
1017
+ `;
1018
+ out += `/**
1019
+ `;
1020
+ out += ` * Main SDK class that provides access to all table clients
1021
+ `;
1022
+ out += ` */
1023
+ `;
1024
+ out += `export class SDK {
1025
+ `;
1026
+ for (const t of tables) {
1027
+ out += ` public ${t.name}: ${pascal(t.name)}Client;
1028
+ `;
1029
+ }
1030
+ out += `
1031
+ constructor(cfg: { baseUrl: string; fetch?: typeof fetch; auth?: AuthConfig }) {
1032
+ `;
1033
+ out += ` const f = cfg.fetch ?? fetch;
1034
+ `;
1035
+ for (const t of tables) {
1036
+ out += ` this.${t.name} = new ${pascal(t.name)}Client(cfg.baseUrl, f, cfg.auth);
1037
+ `;
1038
+ }
1039
+ out += ` }
1040
+ `;
1041
+ out += `}
1042
+
1043
+ `;
1044
+ out += `// Export individual table clients
1045
+ `;
1046
+ for (const t of tables) {
1047
+ out += `export { ${pascal(t.name)}Client } from "./${t.name}";
1048
+ `;
1049
+ }
1050
+ out += `
1051
+ // Export base client for custom extensions
1052
+ `;
1053
+ out += `export { BaseClient } from "./base-client";
1054
+ `;
1055
+ out += `
1056
+ // Export include specifications
1057
+ `;
1058
+ out += `export * from "./include-spec";
1059
+ `;
1060
+ return out;
1061
+ }
1062
+
1063
+ // src/emit-base-client.ts
1064
+ function emitBaseClient() {
1065
+ return `/* Generated. Do not edit. */
1066
+
1067
+ export type HeaderMap = Record<string, string>;
1068
+ export type AuthHeadersProvider = () => Promise<HeaderMap> | HeaderMap;
966
1069
 
967
- type AuthConfig =
1070
+ export type AuthConfig =
968
1071
  | AuthHeadersProvider
969
1072
  | {
970
1073
  apiKey?: string;
@@ -976,14 +1079,18 @@ type AuthConfig =
976
1079
  headers?: AuthHeadersProvider;
977
1080
  };
978
1081
 
979
- export class ${Type}Client {
1082
+ /**
1083
+ * Base client class with shared authentication and request handling logic.
1084
+ * All table-specific clients extend this class.
1085
+ */
1086
+ export abstract class BaseClient {
980
1087
  constructor(
981
- private baseUrl: string,
982
- private fetchFn: typeof fetch = fetch,
983
- private auth?: AuthConfig
1088
+ protected baseUrl: string,
1089
+ protected fetchFn: typeof fetch = fetch,
1090
+ protected auth?: AuthConfig
984
1091
  ) {}
985
1092
 
986
- private async authHeaders(): Promise<HeaderMap> {
1093
+ protected async authHeaders(): Promise<HeaderMap> {
987
1094
  if (!this.auth) return {};
988
1095
  if (typeof this.auth === "function") {
989
1096
  const h = await this.auth();
@@ -1009,129 +1116,91 @@ export class ${Type}Client {
1009
1116
  return out;
1010
1117
  }
1011
1118
 
1012
- private async headers(json = false) {
1119
+ protected async headers(json = false): Promise<HeaderMap> {
1013
1120
  const extra = await this.authHeaders();
1014
1121
  return json ? { "Content-Type": "application/json", ...extra } : extra;
1015
1122
  }
1016
1123
 
1017
- private async okOrThrow(res: Response, action: string) {
1124
+ protected async okOrThrow(res: Response, action: string, entity: string): Promise<void> {
1018
1125
  if (!res.ok) {
1019
1126
  let detail = "";
1020
1127
  try { detail = await res.text(); } catch {}
1021
- throw new Error(\`\${action} ${table.name} failed: \${res.status} \${detail}\`);
1128
+ throw new Error(\`\${action} \${entity} failed: \${res.status} \${detail}\`);
1022
1129
  }
1023
1130
  }
1024
1131
 
1025
- async create(data: Insert${Type}): Promise<Select${Type}> {
1026
- const res = await this.fetchFn(\`\${this.baseUrl}/v1/${table.name}\`, {
1132
+ /**
1133
+ * Make a POST request
1134
+ */
1135
+ protected async post<T>(path: string, body?: any): Promise<T> {
1136
+ const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
1027
1137
  method: "POST",
1028
1138
  headers: await this.headers(true),
1029
- body: JSON.stringify(data),
1139
+ body: JSON.stringify(body),
1030
1140
  });
1031
- await this.okOrThrow(res, "create");
1032
- return (await res.json()) as Select${Type};
1141
+
1142
+ // Handle 404 specially for operations that might return null
1143
+ if (res.status === 404) {
1144
+ return null as T;
1145
+ }
1146
+
1147
+ await this.okOrThrow(res, "POST", path);
1148
+ return (await res.json()) as T;
1033
1149
  }
1034
1150
 
1035
- async getByPk(pk: ${pkType}): Promise<Select${Type} | null> {
1036
- const path = ${pkPathExpr};
1037
- const res = await this.fetchFn(\`\${this.baseUrl}/v1/${table.name}/\${path}\`, {
1151
+ /**
1152
+ * Make a GET request
1153
+ */
1154
+ protected async get<T>(path: string): Promise<T> {
1155
+ const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
1038
1156
  headers: await this.headers(),
1039
1157
  });
1040
- if (res.status === 404) return null;
1041
- await this.okOrThrow(res, "get");
1042
- return (await res.json()) as Select${Type};
1043
- }
1044
-
1045
- async list(params?: { include?: ${Type}IncludeSpec; limit?: number; offset?: number }): Promise<Select${Type}[]> {
1046
- const res = await this.fetchFn(\`\${this.baseUrl}/v1/${table.name}/list\`, {
1047
- method: "POST",
1048
- headers: await this.headers(true),
1049
- body: JSON.stringify(params ?? {}),
1050
- });
1051
- await this.okOrThrow(res, "list");
1052
- return (await res.json()) as Select${Type}[];
1158
+
1159
+ if (res.status === 404) {
1160
+ return null as T;
1161
+ }
1162
+
1163
+ await this.okOrThrow(res, "GET", path);
1164
+ return (await res.json()) as T;
1053
1165
  }
1054
1166
 
1055
- async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null> {
1056
- const path = ${pkPathExpr};
1057
- const res = await this.fetchFn(\`\${this.baseUrl}/v1/${table.name}/\${path}\`, {
1167
+ /**
1168
+ * Make a PATCH request
1169
+ */
1170
+ protected async patch<T>(path: string, body?: any): Promise<T> {
1171
+ const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
1058
1172
  method: "PATCH",
1059
1173
  headers: await this.headers(true),
1060
- body: JSON.stringify(patch),
1174
+ body: JSON.stringify(body),
1061
1175
  });
1062
- if (res.status === 404) return null;
1063
- await this.okOrThrow(res, "update");
1064
- return (await res.json()) as Select${Type};
1176
+
1177
+ if (res.status === 404) {
1178
+ return null as T;
1179
+ }
1180
+
1181
+ await this.okOrThrow(res, "PATCH", path);
1182
+ return (await res.json()) as T;
1065
1183
  }
1066
1184
 
1067
- async delete(pk: ${pkType}): Promise<Select${Type} | null> {
1068
- const path = ${pkPathExpr};
1069
- const res = await this.fetchFn(\`\${this.baseUrl}/v1/${table.name}/\${path}\`, {
1185
+ /**
1186
+ * Make a DELETE request
1187
+ */
1188
+ protected async del<T>(path: string): Promise<T> {
1189
+ const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
1070
1190
  method: "DELETE",
1071
1191
  headers: await this.headers(),
1072
1192
  });
1073
- if (res.status === 404) return null;
1074
- await this.okOrThrow(res, "delete");
1075
- return (await res.json()) as Select${Type};
1193
+
1194
+ if (res.status === 404) {
1195
+ return null as T;
1196
+ }
1197
+
1198
+ await this.okOrThrow(res, "DELETE", path);
1199
+ return (await res.json()) as T;
1076
1200
  }
1077
1201
  }
1078
1202
  `;
1079
1203
  }
1080
- function emitClientIndex(tables) {
1081
- let out = `/* Generated. Do not edit. */
1082
- `;
1083
- for (const t of tables) {
1084
- out += `import { ${pascal(t.name)}Client } from "./${t.name}";
1085
- `;
1086
- }
1087
- out += `
1088
- export type SDKAuthHeadersProvider = () => Promise<Record<string,string>> | Record<string,string>;
1089
- `;
1090
- out += `export type SDKAuth =
1091
- `;
1092
- out += ` | SDKAuthHeadersProvider
1093
- `;
1094
- out += ` | {
1095
- `;
1096
- out += ` apiKey?: string;
1097
- `;
1098
- out += ` /** defaults to "x-api-key" */
1099
- `;
1100
- out += ` apiKeyHeader?: string;
1101
- `;
1102
- out += ` jwt?: string | (() => Promise<string>);
1103
- `;
1104
- out += ` headers?: SDKAuthHeadersProvider;
1105
- `;
1106
- out += ` };
1107
-
1108
- `;
1109
- out += `export class SDK {
1110
- `;
1111
- for (const t of tables) {
1112
- out += ` public ${t.name}: ${pascal(t.name)}Client;
1113
- `;
1114
- }
1115
- out += `
1116
- constructor(cfg: { baseUrl: string; fetch?: typeof fetch; auth?: SDKAuth }) {
1117
- `;
1118
- out += ` const f = cfg.fetch ?? fetch;
1119
- `;
1120
- for (const t of tables) {
1121
- out += ` this.${t.name} = new ${pascal(t.name)}Client(cfg.baseUrl, f, cfg.auth);
1122
- `;
1123
- }
1124
- out += ` }
1125
- `;
1126
- out += `}
1127
- `;
1128
- for (const t of tables)
1129
- out += `export { ${pascal(t.name)}Client } from "./${t.name}";
1130
- `;
1131
- out += `export * from "./include-spec";
1132
- `;
1133
- return out;
1134
- }
1135
1204
 
1136
1205
  // src/emit-include-loader.ts
1137
1206
  function emitIncludeLoader(graph, model, maxDepth) {
@@ -1740,11 +1809,50 @@ export * from "./include-spec";
1740
1809
  `;
1741
1810
  }
1742
1811
 
1812
+ // src/types.ts
1813
+ function normalizeAuthConfig(input) {
1814
+ if (!input)
1815
+ return;
1816
+ if ("strategy" in input && input.strategy) {
1817
+ return input;
1818
+ }
1819
+ if ("apiKey" in input && input.apiKey) {
1820
+ return {
1821
+ strategy: "api-key",
1822
+ apiKeyHeader: input.apiKeyHeader,
1823
+ apiKeys: [input.apiKey, ...input.apiKeys || []]
1824
+ };
1825
+ }
1826
+ if ("apiKeys" in input && input.apiKeys?.length) {
1827
+ return {
1828
+ strategy: "api-key",
1829
+ apiKeyHeader: input.apiKeyHeader,
1830
+ apiKeys: input.apiKeys
1831
+ };
1832
+ }
1833
+ if ("jwt" in input && input.jwt) {
1834
+ if (typeof input.jwt === "string") {
1835
+ return {
1836
+ strategy: "jwt-hs256",
1837
+ jwt: { sharedSecret: input.jwt }
1838
+ };
1839
+ } else {
1840
+ return {
1841
+ strategy: "jwt-hs256",
1842
+ jwt: input.jwt
1843
+ };
1844
+ }
1845
+ }
1846
+ return { strategy: "none" };
1847
+ }
1848
+
1743
1849
  // src/index.ts
1744
1850
  async function generate(configPath) {
1745
1851
  const configUrl = pathToFileURL(configPath).href;
1746
1852
  const module = await import(configUrl);
1747
- const cfg = module.default || module;
1853
+ const rawCfg = module.default || module;
1854
+ const normalizedAuth = normalizeAuthConfig(rawCfg.auth);
1855
+ const cfg = { ...rawCfg, auth: normalizedAuth };
1748
1856
  console.log("\uD83D\uDD0D Introspecting database...");
1749
1857
  const model = await introspect(cfg.connectionString, cfg.schema || "public");
1750
1858
  console.log("\uD83D\uDD17 Building relationship graph...");
@@ -1765,6 +1873,7 @@ async function generate(configPath) {
1765
1873
  const includeSpec = emitIncludeSpec(graph);
1766
1874
  files.push({ path: join(serverDir, "include-spec.ts"), content: includeSpec });
1767
1875
  files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
1876
+ files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
1768
1877
  files.push({
1769
1878
  path: join(serverDir, "include-builder.ts"),
1770
1879
  content: emitIncludeBuilder(graph, cfg.includeDepthLimit || 3)
@@ -1774,8 +1883,8 @@ async function generate(configPath) {
1774
1883
  content: emitIncludeLoader(graph, model, cfg.includeDepthLimit || 3)
1775
1884
  });
1776
1885
  files.push({ path: join(serverDir, "logger.ts"), content: emitLogger() });
1777
- if (cfg.auth?.strategy && cfg.auth.strategy !== "none") {
1778
- files.push({ path: join(serverDir, "auth.ts"), content: emitAuth(cfg.auth) });
1886
+ if (normalizedAuth?.strategy && normalizedAuth.strategy !== "none") {
1887
+ files.push({ path: join(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
1779
1888
  }
1780
1889
  for (const table of Object.values(model.tables)) {
1781
1890
  const typesSrc = emitTypes(table, { dateType: normDateType, numericMode: "string" });
@@ -1790,7 +1899,7 @@ async function generate(configPath) {
1790
1899
  content: emitRoutes(table, graph, {
1791
1900
  softDeleteColumn: cfg.softDeleteColumn || null,
1792
1901
  includeDepthLimit: cfg.includeDepthLimit || 3,
1793
- authStrategy: cfg.auth?.strategy
1902
+ authStrategy: normalizedAuth?.strategy
1794
1903
  })
1795
1904
  });
1796
1905
  files.push({
@@ -1804,7 +1913,7 @@ async function generate(configPath) {
1804
1913
  });
1805
1914
  files.push({
1806
1915
  path: join(serverDir, "router.ts"),
1807
- content: emitRouter(Object.values(model.tables), !!cfg.auth?.strategy && cfg.auth.strategy !== "none")
1916
+ content: emitRouter(Object.values(model.tables), !!normalizedAuth?.strategy && normalizedAuth.strategy !== "none")
1808
1917
  });
1809
1918
  console.log("✍️ Writing files...");
1810
1919
  await writeFiles(files);
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Emits the BaseClient class that all table-specific clients will extend.
3
+ * Contains all shared logic for auth, headers, and HTTP operations.
4
+ */
5
+ export declare function emitBaseClient(): string;
package/dist/index.js CHANGED
@@ -957,13 +957,116 @@ function emitClient(table) {
957
957
  const pkType = hasCompositePk ? `{ ${safePk.map((c) => `${c}: string`).join("; ")} }` : `string`;
958
958
  const pkPathExpr = hasCompositePk ? safePk.map((c) => `pk.${c}`).join(` + "/" + `) : `pk`;
959
959
  return `/* Generated. Do not edit. */
960
+ import { BaseClient } from "./base-client";
960
961
  import type { ${Type}IncludeSpec } from "./include-spec";
961
962
  import type { Insert${Type}, Update${Type}, Select${Type} } from "./types/${table.name}";
962
963
 
963
- type HeaderMap = Record<string, string>;
964
- type AuthHeadersProvider = () => Promise<HeaderMap> | HeaderMap;
964
+ /**
965
+ * Client for ${table.name} table operations
966
+ */
967
+ export class ${Type}Client extends BaseClient {
968
+ private readonly resource = "/v1/${table.name}";
969
+
970
+ async create(data: Insert${Type}): Promise<Select${Type}> {
971
+ return this.post<Select${Type}>(this.resource, data);
972
+ }
973
+
974
+ async getByPk(pk: ${pkType}): Promise<Select${Type} | null> {
975
+ const path = ${pkPathExpr};
976
+ return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
977
+ }
978
+
979
+ async list(params?: {
980
+ include?: ${Type}IncludeSpec;
981
+ limit?: number;
982
+ offset?: number;
983
+ where?: any;
984
+ orderBy?: string;
985
+ order?: "asc" | "desc";
986
+ }): Promise<Select${Type}[]> {
987
+ return this.post<Select${Type}[]>(\`\${this.resource}/list\`, params ?? {});
988
+ }
989
+
990
+ async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null> {
991
+ const path = ${pkPathExpr};
992
+ return this.patch<Select${Type} | null>(\`\${this.resource}/\${path}\`, patch);
993
+ }
994
+
995
+ async delete(pk: ${pkType}): Promise<Select${Type} | null> {
996
+ const path = ${pkPathExpr};
997
+ return this.del<Select${Type} | null>(\`\${this.resource}/\${path}\`);
998
+ }
999
+ }
1000
+ `;
1001
+ }
1002
+ function emitClientIndex(tables) {
1003
+ let out = `/* Generated. Do not edit. */
1004
+ `;
1005
+ out += `import { BaseClient, AuthConfig } from "./base-client";
1006
+ `;
1007
+ for (const t of tables) {
1008
+ out += `import { ${pascal(t.name)}Client } from "./${t.name}";
1009
+ `;
1010
+ }
1011
+ out += `
1012
+ // Re-export auth types for convenience
1013
+ `;
1014
+ out += `export type { AuthConfig as SDKAuth, AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client";
1015
+
1016
+ `;
1017
+ out += `/**
1018
+ `;
1019
+ out += ` * Main SDK class that provides access to all table clients
1020
+ `;
1021
+ out += ` */
1022
+ `;
1023
+ out += `export class SDK {
1024
+ `;
1025
+ for (const t of tables) {
1026
+ out += ` public ${t.name}: ${pascal(t.name)}Client;
1027
+ `;
1028
+ }
1029
+ out += `
1030
+ constructor(cfg: { baseUrl: string; fetch?: typeof fetch; auth?: AuthConfig }) {
1031
+ `;
1032
+ out += ` const f = cfg.fetch ?? fetch;
1033
+ `;
1034
+ for (const t of tables) {
1035
+ out += ` this.${t.name} = new ${pascal(t.name)}Client(cfg.baseUrl, f, cfg.auth);
1036
+ `;
1037
+ }
1038
+ out += ` }
1039
+ `;
1040
+ out += `}
1041
+
1042
+ `;
1043
+ out += `// Export individual table clients
1044
+ `;
1045
+ for (const t of tables) {
1046
+ out += `export { ${pascal(t.name)}Client } from "./${t.name}";
1047
+ `;
1048
+ }
1049
+ out += `
1050
+ // Export base client for custom extensions
1051
+ `;
1052
+ out += `export { BaseClient } from "./base-client";
1053
+ `;
1054
+ out += `
1055
+ // Export include specifications
1056
+ `;
1057
+ out += `export * from "./include-spec";
1058
+ `;
1059
+ return out;
1060
+ }
1061
+
1062
+ // src/emit-base-client.ts
1063
+ function emitBaseClient() {
1064
+ return `/* Generated. Do not edit. */
1065
+
1066
+ export type HeaderMap = Record<string, string>;
1067
+ export type AuthHeadersProvider = () => Promise<HeaderMap> | HeaderMap;
965
1068
 
966
- type AuthConfig =
1069
+ export type AuthConfig =
967
1070
  | AuthHeadersProvider
968
1071
  | {
969
1072
  apiKey?: string;
@@ -975,14 +1078,18 @@ type AuthConfig =
975
1078
  headers?: AuthHeadersProvider;
976
1079
  };
977
1080
 
978
- export class ${Type}Client {
1081
+ /**
1082
+ * Base client class with shared authentication and request handling logic.
1083
+ * All table-specific clients extend this class.
1084
+ */
1085
+ export abstract class BaseClient {
979
1086
  constructor(
980
- private baseUrl: string,
981
- private fetchFn: typeof fetch = fetch,
982
- private auth?: AuthConfig
1087
+ protected baseUrl: string,
1088
+ protected fetchFn: typeof fetch = fetch,
1089
+ protected auth?: AuthConfig
983
1090
  ) {}
984
1091
 
985
- private async authHeaders(): Promise<HeaderMap> {
1092
+ protected async authHeaders(): Promise<HeaderMap> {
986
1093
  if (!this.auth) return {};
987
1094
  if (typeof this.auth === "function") {
988
1095
  const h = await this.auth();
@@ -1008,129 +1115,91 @@ export class ${Type}Client {
1008
1115
  return out;
1009
1116
  }
1010
1117
 
1011
- private async headers(json = false) {
1118
+ protected async headers(json = false): Promise<HeaderMap> {
1012
1119
  const extra = await this.authHeaders();
1013
1120
  return json ? { "Content-Type": "application/json", ...extra } : extra;
1014
1121
  }
1015
1122
 
1016
- private async okOrThrow(res: Response, action: string) {
1123
+ protected async okOrThrow(res: Response, action: string, entity: string): Promise<void> {
1017
1124
  if (!res.ok) {
1018
1125
  let detail = "";
1019
1126
  try { detail = await res.text(); } catch {}
1020
- throw new Error(\`\${action} ${table.name} failed: \${res.status} \${detail}\`);
1127
+ throw new Error(\`\${action} \${entity} failed: \${res.status} \${detail}\`);
1021
1128
  }
1022
1129
  }
1023
1130
 
1024
- async create(data: Insert${Type}): Promise<Select${Type}> {
1025
- const res = await this.fetchFn(\`\${this.baseUrl}/v1/${table.name}\`, {
1131
+ /**
1132
+ * Make a POST request
1133
+ */
1134
+ protected async post<T>(path: string, body?: any): Promise<T> {
1135
+ const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
1026
1136
  method: "POST",
1027
1137
  headers: await this.headers(true),
1028
- body: JSON.stringify(data),
1138
+ body: JSON.stringify(body),
1029
1139
  });
1030
- await this.okOrThrow(res, "create");
1031
- return (await res.json()) as Select${Type};
1140
+
1141
+ // Handle 404 specially for operations that might return null
1142
+ if (res.status === 404) {
1143
+ return null as T;
1144
+ }
1145
+
1146
+ await this.okOrThrow(res, "POST", path);
1147
+ return (await res.json()) as T;
1032
1148
  }
1033
1149
 
1034
- async getByPk(pk: ${pkType}): Promise<Select${Type} | null> {
1035
- const path = ${pkPathExpr};
1036
- const res = await this.fetchFn(\`\${this.baseUrl}/v1/${table.name}/\${path}\`, {
1150
+ /**
1151
+ * Make a GET request
1152
+ */
1153
+ protected async get<T>(path: string): Promise<T> {
1154
+ const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
1037
1155
  headers: await this.headers(),
1038
1156
  });
1039
- if (res.status === 404) return null;
1040
- await this.okOrThrow(res, "get");
1041
- return (await res.json()) as Select${Type};
1042
- }
1043
-
1044
- async list(params?: { include?: ${Type}IncludeSpec; limit?: number; offset?: number }): Promise<Select${Type}[]> {
1045
- const res = await this.fetchFn(\`\${this.baseUrl}/v1/${table.name}/list\`, {
1046
- method: "POST",
1047
- headers: await this.headers(true),
1048
- body: JSON.stringify(params ?? {}),
1049
- });
1050
- await this.okOrThrow(res, "list");
1051
- return (await res.json()) as Select${Type}[];
1157
+
1158
+ if (res.status === 404) {
1159
+ return null as T;
1160
+ }
1161
+
1162
+ await this.okOrThrow(res, "GET", path);
1163
+ return (await res.json()) as T;
1052
1164
  }
1053
1165
 
1054
- async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null> {
1055
- const path = ${pkPathExpr};
1056
- const res = await this.fetchFn(\`\${this.baseUrl}/v1/${table.name}/\${path}\`, {
1166
+ /**
1167
+ * Make a PATCH request
1168
+ */
1169
+ protected async patch<T>(path: string, body?: any): Promise<T> {
1170
+ const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
1057
1171
  method: "PATCH",
1058
1172
  headers: await this.headers(true),
1059
- body: JSON.stringify(patch),
1173
+ body: JSON.stringify(body),
1060
1174
  });
1061
- if (res.status === 404) return null;
1062
- await this.okOrThrow(res, "update");
1063
- return (await res.json()) as Select${Type};
1175
+
1176
+ if (res.status === 404) {
1177
+ return null as T;
1178
+ }
1179
+
1180
+ await this.okOrThrow(res, "PATCH", path);
1181
+ return (await res.json()) as T;
1064
1182
  }
1065
1183
 
1066
- async delete(pk: ${pkType}): Promise<Select${Type} | null> {
1067
- const path = ${pkPathExpr};
1068
- const res = await this.fetchFn(\`\${this.baseUrl}/v1/${table.name}/\${path}\`, {
1184
+ /**
1185
+ * Make a DELETE request
1186
+ */
1187
+ protected async del<T>(path: string): Promise<T> {
1188
+ const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
1069
1189
  method: "DELETE",
1070
1190
  headers: await this.headers(),
1071
1191
  });
1072
- if (res.status === 404) return null;
1073
- await this.okOrThrow(res, "delete");
1074
- return (await res.json()) as Select${Type};
1192
+
1193
+ if (res.status === 404) {
1194
+ return null as T;
1195
+ }
1196
+
1197
+ await this.okOrThrow(res, "DELETE", path);
1198
+ return (await res.json()) as T;
1075
1199
  }
1076
1200
  }
1077
1201
  `;
1078
1202
  }
1079
- function emitClientIndex(tables) {
1080
- let out = `/* Generated. Do not edit. */
1081
- `;
1082
- for (const t of tables) {
1083
- out += `import { ${pascal(t.name)}Client } from "./${t.name}";
1084
- `;
1085
- }
1086
- out += `
1087
- export type SDKAuthHeadersProvider = () => Promise<Record<string,string>> | Record<string,string>;
1088
- `;
1089
- out += `export type SDKAuth =
1090
- `;
1091
- out += ` | SDKAuthHeadersProvider
1092
- `;
1093
- out += ` | {
1094
- `;
1095
- out += ` apiKey?: string;
1096
- `;
1097
- out += ` /** defaults to "x-api-key" */
1098
- `;
1099
- out += ` apiKeyHeader?: string;
1100
- `;
1101
- out += ` jwt?: string | (() => Promise<string>);
1102
- `;
1103
- out += ` headers?: SDKAuthHeadersProvider;
1104
- `;
1105
- out += ` };
1106
-
1107
- `;
1108
- out += `export class SDK {
1109
- `;
1110
- for (const t of tables) {
1111
- out += ` public ${t.name}: ${pascal(t.name)}Client;
1112
- `;
1113
- }
1114
- out += `
1115
- constructor(cfg: { baseUrl: string; fetch?: typeof fetch; auth?: SDKAuth }) {
1116
- `;
1117
- out += ` const f = cfg.fetch ?? fetch;
1118
- `;
1119
- for (const t of tables) {
1120
- out += ` this.${t.name} = new ${pascal(t.name)}Client(cfg.baseUrl, f, cfg.auth);
1121
- `;
1122
- }
1123
- out += ` }
1124
- `;
1125
- out += `}
1126
- `;
1127
- for (const t of tables)
1128
- out += `export { ${pascal(t.name)}Client } from "./${t.name}";
1129
- `;
1130
- out += `export * from "./include-spec";
1131
- `;
1132
- return out;
1133
- }
1134
1203
 
1135
1204
  // src/emit-include-loader.ts
1136
1205
  function emitIncludeLoader(graph, model, maxDepth) {
@@ -1739,11 +1808,50 @@ export * from "./include-spec";
1739
1808
  `;
1740
1809
  }
1741
1810
 
1811
+ // src/types.ts
1812
+ function normalizeAuthConfig(input) {
1813
+ if (!input)
1814
+ return;
1815
+ if ("strategy" in input && input.strategy) {
1816
+ return input;
1817
+ }
1818
+ if ("apiKey" in input && input.apiKey) {
1819
+ return {
1820
+ strategy: "api-key",
1821
+ apiKeyHeader: input.apiKeyHeader,
1822
+ apiKeys: [input.apiKey, ...input.apiKeys || []]
1823
+ };
1824
+ }
1825
+ if ("apiKeys" in input && input.apiKeys?.length) {
1826
+ return {
1827
+ strategy: "api-key",
1828
+ apiKeyHeader: input.apiKeyHeader,
1829
+ apiKeys: input.apiKeys
1830
+ };
1831
+ }
1832
+ if ("jwt" in input && input.jwt) {
1833
+ if (typeof input.jwt === "string") {
1834
+ return {
1835
+ strategy: "jwt-hs256",
1836
+ jwt: { sharedSecret: input.jwt }
1837
+ };
1838
+ } else {
1839
+ return {
1840
+ strategy: "jwt-hs256",
1841
+ jwt: input.jwt
1842
+ };
1843
+ }
1844
+ }
1845
+ return { strategy: "none" };
1846
+ }
1847
+
1742
1848
  // src/index.ts
1743
1849
  async function generate(configPath) {
1744
1850
  const configUrl = pathToFileURL(configPath).href;
1745
1851
  const module = await import(configUrl);
1746
- const cfg = module.default || module;
1852
+ const rawCfg = module.default || module;
1853
+ const normalizedAuth = normalizeAuthConfig(rawCfg.auth);
1854
+ const cfg = { ...rawCfg, auth: normalizedAuth };
1747
1855
  console.log("\uD83D\uDD0D Introspecting database...");
1748
1856
  const model = await introspect(cfg.connectionString, cfg.schema || "public");
1749
1857
  console.log("\uD83D\uDD17 Building relationship graph...");
@@ -1764,6 +1872,7 @@ async function generate(configPath) {
1764
1872
  const includeSpec = emitIncludeSpec(graph);
1765
1873
  files.push({ path: join(serverDir, "include-spec.ts"), content: includeSpec });
1766
1874
  files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
1875
+ files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
1767
1876
  files.push({
1768
1877
  path: join(serverDir, "include-builder.ts"),
1769
1878
  content: emitIncludeBuilder(graph, cfg.includeDepthLimit || 3)
@@ -1773,8 +1882,8 @@ async function generate(configPath) {
1773
1882
  content: emitIncludeLoader(graph, model, cfg.includeDepthLimit || 3)
1774
1883
  });
1775
1884
  files.push({ path: join(serverDir, "logger.ts"), content: emitLogger() });
1776
- if (cfg.auth?.strategy && cfg.auth.strategy !== "none") {
1777
- files.push({ path: join(serverDir, "auth.ts"), content: emitAuth(cfg.auth) });
1885
+ if (normalizedAuth?.strategy && normalizedAuth.strategy !== "none") {
1886
+ files.push({ path: join(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
1778
1887
  }
1779
1888
  for (const table of Object.values(model.tables)) {
1780
1889
  const typesSrc = emitTypes(table, { dateType: normDateType, numericMode: "string" });
@@ -1789,7 +1898,7 @@ async function generate(configPath) {
1789
1898
  content: emitRoutes(table, graph, {
1790
1899
  softDeleteColumn: cfg.softDeleteColumn || null,
1791
1900
  includeDepthLimit: cfg.includeDepthLimit || 3,
1792
- authStrategy: cfg.auth?.strategy
1901
+ authStrategy: normalizedAuth?.strategy
1793
1902
  })
1794
1903
  });
1795
1904
  files.push({
@@ -1803,7 +1912,7 @@ async function generate(configPath) {
1803
1912
  });
1804
1913
  files.push({
1805
1914
  path: join(serverDir, "router.ts"),
1806
- content: emitRouter(Object.values(model.tables), !!cfg.auth?.strategy && cfg.auth.strategy !== "none")
1915
+ content: emitRouter(Object.values(model.tables), !!normalizedAuth?.strategy && normalizedAuth.strategy !== "none")
1807
1916
  });
1808
1917
  console.log("✍️ Writing files...");
1809
1918
  await writeFiles(files);
package/dist/types.d.ts CHANGED
@@ -8,6 +8,16 @@ export interface AuthConfig {
8
8
  audience?: string;
9
9
  };
10
10
  }
11
+ export type AuthConfigInput = AuthConfig | {
12
+ apiKey?: string;
13
+ apiKeys?: string[];
14
+ apiKeyHeader?: string;
15
+ jwt?: string | {
16
+ sharedSecret?: string;
17
+ issuer?: string;
18
+ audience?: string;
19
+ };
20
+ };
11
21
  export interface Config {
12
22
  connectionString: string;
13
23
  schema?: string;
@@ -16,5 +26,6 @@ export interface Config {
16
26
  softDeleteColumn?: string | null;
17
27
  includeDepthLimit?: number;
18
28
  dateType?: "date" | "string";
19
- auth?: AuthConfig;
29
+ auth?: AuthConfigInput;
20
30
  }
31
+ export declare function normalizeAuthConfig(input: AuthConfigInput | undefined): AuthConfig | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.1.2-alpha.2",
3
+ "version": "0.1.2-alpha.4",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {