postgresdk 0.1.2-alpha.3 → 0.2.0

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.
@@ -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;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Generates a bundle containing all client SDK files as a single module
3
+ * that can be served by the API
4
+ */
5
+ export declare function emitSdkBundle(clientFiles: {
6
+ path: string;
7
+ content: string;
8
+ }[]): string;
package/dist/index.js CHANGED
@@ -16,6 +16,16 @@ var __toESM = (mod, isNodeMode, target) => {
16
16
  return to;
17
17
  };
18
18
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
19
29
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
30
 
21
31
  // node_modules/dotenv/package.json
@@ -957,13 +967,116 @@ function emitClient(table) {
957
967
  const pkType = hasCompositePk ? `{ ${safePk.map((c) => `${c}: string`).join("; ")} }` : `string`;
958
968
  const pkPathExpr = hasCompositePk ? safePk.map((c) => `pk.${c}`).join(` + "/" + `) : `pk`;
959
969
  return `/* Generated. Do not edit. */
970
+ import { BaseClient } from "./base-client";
960
971
  import type { ${Type}IncludeSpec } from "./include-spec";
961
972
  import type { Insert${Type}, Update${Type}, Select${Type} } from "./types/${table.name}";
962
973
 
963
- type HeaderMap = Record<string, string>;
964
- type AuthHeadersProvider = () => Promise<HeaderMap> | HeaderMap;
974
+ /**
975
+ * Client for ${table.name} table operations
976
+ */
977
+ export class ${Type}Client extends BaseClient {
978
+ private readonly resource = "/v1/${table.name}";
979
+
980
+ async create(data: Insert${Type}): Promise<Select${Type}> {
981
+ return this.post<Select${Type}>(this.resource, data);
982
+ }
983
+
984
+ async getByPk(pk: ${pkType}): Promise<Select${Type} | null> {
985
+ const path = ${pkPathExpr};
986
+ return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
987
+ }
988
+
989
+ async list(params?: {
990
+ include?: ${Type}IncludeSpec;
991
+ limit?: number;
992
+ offset?: number;
993
+ where?: any;
994
+ orderBy?: string;
995
+ order?: "asc" | "desc";
996
+ }): Promise<Select${Type}[]> {
997
+ return this.post<Select${Type}[]>(\`\${this.resource}/list\`, params ?? {});
998
+ }
999
+
1000
+ async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null> {
1001
+ const path = ${pkPathExpr};
1002
+ return this.patch<Select${Type} | null>(\`\${this.resource}/\${path}\`, patch);
1003
+ }
965
1004
 
966
- type AuthConfig =
1005
+ async delete(pk: ${pkType}): Promise<Select${Type} | null> {
1006
+ const path = ${pkPathExpr};
1007
+ return this.del<Select${Type} | null>(\`\${this.resource}/\${path}\`);
1008
+ }
1009
+ }
1010
+ `;
1011
+ }
1012
+ function emitClientIndex(tables) {
1013
+ let out = `/* Generated. Do not edit. */
1014
+ `;
1015
+ out += `import { BaseClient, AuthConfig } from "./base-client";
1016
+ `;
1017
+ for (const t of tables) {
1018
+ out += `import { ${pascal(t.name)}Client } from "./${t.name}";
1019
+ `;
1020
+ }
1021
+ out += `
1022
+ // Re-export auth types for convenience
1023
+ `;
1024
+ out += `export type { AuthConfig as SDKAuth, AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client";
1025
+
1026
+ `;
1027
+ out += `/**
1028
+ `;
1029
+ out += ` * Main SDK class that provides access to all table clients
1030
+ `;
1031
+ out += ` */
1032
+ `;
1033
+ out += `export class SDK {
1034
+ `;
1035
+ for (const t of tables) {
1036
+ out += ` public ${t.name}: ${pascal(t.name)}Client;
1037
+ `;
1038
+ }
1039
+ out += `
1040
+ constructor(cfg: { baseUrl: string; fetch?: typeof fetch; auth?: AuthConfig }) {
1041
+ `;
1042
+ out += ` const f = cfg.fetch ?? fetch;
1043
+ `;
1044
+ for (const t of tables) {
1045
+ out += ` this.${t.name} = new ${pascal(t.name)}Client(cfg.baseUrl, f, cfg.auth);
1046
+ `;
1047
+ }
1048
+ out += ` }
1049
+ `;
1050
+ out += `}
1051
+
1052
+ `;
1053
+ out += `// Export individual table clients
1054
+ `;
1055
+ for (const t of tables) {
1056
+ out += `export { ${pascal(t.name)}Client } from "./${t.name}";
1057
+ `;
1058
+ }
1059
+ out += `
1060
+ // Export base client for custom extensions
1061
+ `;
1062
+ out += `export { BaseClient } from "./base-client";
1063
+ `;
1064
+ out += `
1065
+ // Export include specifications
1066
+ `;
1067
+ out += `export * from "./include-spec";
1068
+ `;
1069
+ return out;
1070
+ }
1071
+
1072
+ // src/emit-base-client.ts
1073
+ function emitBaseClient() {
1074
+ return `/* Generated. Do not edit. */
1075
+
1076
+ export type HeaderMap = Record<string, string>;
1077
+ export type AuthHeadersProvider = () => Promise<HeaderMap> | HeaderMap;
1078
+
1079
+ export type AuthConfig =
967
1080
  | AuthHeadersProvider
968
1081
  | {
969
1082
  apiKey?: string;
@@ -975,14 +1088,18 @@ type AuthConfig =
975
1088
  headers?: AuthHeadersProvider;
976
1089
  };
977
1090
 
978
- export class ${Type}Client {
1091
+ /**
1092
+ * Base client class with shared authentication and request handling logic.
1093
+ * All table-specific clients extend this class.
1094
+ */
1095
+ export abstract class BaseClient {
979
1096
  constructor(
980
- private baseUrl: string,
981
- private fetchFn: typeof fetch = fetch,
982
- private auth?: AuthConfig
1097
+ protected baseUrl: string,
1098
+ protected fetchFn: typeof fetch = fetch,
1099
+ protected auth?: AuthConfig
983
1100
  ) {}
984
1101
 
985
- private async authHeaders(): Promise<HeaderMap> {
1102
+ protected async authHeaders(): Promise<HeaderMap> {
986
1103
  if (!this.auth) return {};
987
1104
  if (typeof this.auth === "function") {
988
1105
  const h = await this.auth();
@@ -1008,129 +1125,91 @@ export class ${Type}Client {
1008
1125
  return out;
1009
1126
  }
1010
1127
 
1011
- private async headers(json = false) {
1128
+ protected async headers(json = false): Promise<HeaderMap> {
1012
1129
  const extra = await this.authHeaders();
1013
1130
  return json ? { "Content-Type": "application/json", ...extra } : extra;
1014
1131
  }
1015
1132
 
1016
- private async okOrThrow(res: Response, action: string) {
1133
+ protected async okOrThrow(res: Response, action: string, entity: string): Promise<void> {
1017
1134
  if (!res.ok) {
1018
1135
  let detail = "";
1019
1136
  try { detail = await res.text(); } catch {}
1020
- throw new Error(\`\${action} ${table.name} failed: \${res.status} \${detail}\`);
1137
+ throw new Error(\`\${action} \${entity} failed: \${res.status} \${detail}\`);
1021
1138
  }
1022
1139
  }
1023
1140
 
1024
- async create(data: Insert${Type}): Promise<Select${Type}> {
1025
- const res = await this.fetchFn(\`\${this.baseUrl}/v1/${table.name}\`, {
1141
+ /**
1142
+ * Make a POST request
1143
+ */
1144
+ protected async post<T>(path: string, body?: any): Promise<T> {
1145
+ const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
1026
1146
  method: "POST",
1027
1147
  headers: await this.headers(true),
1028
- body: JSON.stringify(data),
1148
+ body: JSON.stringify(body),
1029
1149
  });
1030
- await this.okOrThrow(res, "create");
1031
- return (await res.json()) as Select${Type};
1150
+
1151
+ // Handle 404 specially for operations that might return null
1152
+ if (res.status === 404) {
1153
+ return null as T;
1154
+ }
1155
+
1156
+ await this.okOrThrow(res, "POST", path);
1157
+ return (await res.json()) as T;
1032
1158
  }
1033
1159
 
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}\`, {
1160
+ /**
1161
+ * Make a GET request
1162
+ */
1163
+ protected async get<T>(path: string): Promise<T> {
1164
+ const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
1037
1165
  headers: await this.headers(),
1038
1166
  });
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}[];
1167
+
1168
+ if (res.status === 404) {
1169
+ return null as T;
1170
+ }
1171
+
1172
+ await this.okOrThrow(res, "GET", path);
1173
+ return (await res.json()) as T;
1052
1174
  }
1053
1175
 
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}\`, {
1176
+ /**
1177
+ * Make a PATCH request
1178
+ */
1179
+ protected async patch<T>(path: string, body?: any): Promise<T> {
1180
+ const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
1057
1181
  method: "PATCH",
1058
1182
  headers: await this.headers(true),
1059
- body: JSON.stringify(patch),
1183
+ body: JSON.stringify(body),
1060
1184
  });
1061
- if (res.status === 404) return null;
1062
- await this.okOrThrow(res, "update");
1063
- return (await res.json()) as Select${Type};
1185
+
1186
+ if (res.status === 404) {
1187
+ return null as T;
1188
+ }
1189
+
1190
+ await this.okOrThrow(res, "PATCH", path);
1191
+ return (await res.json()) as T;
1064
1192
  }
1065
1193
 
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}\`, {
1194
+ /**
1195
+ * Make a DELETE request
1196
+ */
1197
+ protected async del<T>(path: string): Promise<T> {
1198
+ const res = await this.fetchFn(\`\${this.baseUrl}\${path}\`, {
1069
1199
  method: "DELETE",
1070
1200
  headers: await this.headers(),
1071
1201
  });
1072
- if (res.status === 404) return null;
1073
- await this.okOrThrow(res, "delete");
1074
- return (await res.json()) as Select${Type};
1202
+
1203
+ if (res.status === 404) {
1204
+ return null as T;
1205
+ }
1206
+
1207
+ await this.okOrThrow(res, "DELETE", path);
1208
+ return (await res.json()) as T;
1075
1209
  }
1076
1210
  }
1077
1211
  `;
1078
1212
  }
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
1213
 
1135
1214
  // src/emit-include-loader.ts
1136
1215
  function emitIncludeLoader(graph, model, maxDepth) {
@@ -1678,6 +1757,7 @@ function emitRouter(tables, hasAuth) {
1678
1757
  `);
1679
1758
  return `/* Generated. Do not edit. */
1680
1759
  import { Hono } from "hono";
1760
+ import { SDK_MANIFEST } from "./sdk-bundle";
1681
1761
  ${imports}
1682
1762
  ${hasAuth ? `export { authMiddleware } from "./auth";` : ""}
1683
1763
 
@@ -1705,7 +1785,34 @@ export function createRouter(
1705
1785
  deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> } }
1706
1786
  ): Hono {
1707
1787
  const router = new Hono();
1788
+
1789
+ // Register table routes
1708
1790
  ${registrations}
1791
+
1792
+ // SDK distribution endpoints
1793
+ router.get("/sdk/manifest", (c) => {
1794
+ return c.json({
1795
+ version: SDK_MANIFEST.version,
1796
+ generated: SDK_MANIFEST.generated,
1797
+ files: Object.keys(SDK_MANIFEST.files)
1798
+ });
1799
+ });
1800
+
1801
+ router.get("/sdk/download", (c) => {
1802
+ return c.json(SDK_MANIFEST);
1803
+ });
1804
+
1805
+ router.get("/sdk/files/:path{.*}", (c) => {
1806
+ const path = c.req.param("path");
1807
+ const content = SDK_MANIFEST.files[path];
1808
+ if (!content) {
1809
+ return c.text("File not found", 404);
1810
+ }
1811
+ return c.text(content, 200, {
1812
+ "Content-Type": "text/plain; charset=utf-8"
1813
+ });
1814
+ });
1815
+
1709
1816
  return router;
1710
1817
  }
1711
1818
 
@@ -1739,6 +1846,29 @@ export * from "./include-spec";
1739
1846
  `;
1740
1847
  }
1741
1848
 
1849
+ // src/emit-sdk-bundle.ts
1850
+ function emitSdkBundle(clientFiles) {
1851
+ const files = {};
1852
+ for (const file of clientFiles) {
1853
+ const parts = file.path.split("/");
1854
+ const clientIndex = parts.lastIndexOf("client");
1855
+ if (clientIndex >= 0 && clientIndex < parts.length - 1) {
1856
+ const relativePath = parts.slice(clientIndex + 1).join("/");
1857
+ files[relativePath] = file.content;
1858
+ }
1859
+ }
1860
+ const version = `1.0.0`;
1861
+ const generated = new Date().toISOString();
1862
+ return `/* Generated. Do not edit. */
1863
+
1864
+ export const SDK_MANIFEST = {
1865
+ version: "${version}",
1866
+ generated: "${generated}",
1867
+ files: ${JSON.stringify(files, null, 2)}
1868
+ };
1869
+ `;
1870
+ }
1871
+
1742
1872
  // src/types.ts
1743
1873
  function normalizeAuthConfig(input) {
1744
1874
  if (!input)
@@ -1803,6 +1933,7 @@ async function generate(configPath) {
1803
1933
  const includeSpec = emitIncludeSpec(graph);
1804
1934
  files.push({ path: join(serverDir, "include-spec.ts"), content: includeSpec });
1805
1935
  files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
1936
+ files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
1806
1937
  files.push({
1807
1938
  path: join(serverDir, "include-builder.ts"),
1808
1939
  content: emitIncludeBuilder(graph, cfg.includeDepthLimit || 3)
@@ -1844,6 +1975,11 @@ async function generate(configPath) {
1844
1975
  path: join(serverDir, "router.ts"),
1845
1976
  content: emitRouter(Object.values(model.tables), !!normalizedAuth?.strategy && normalizedAuth.strategy !== "none")
1846
1977
  });
1978
+ const clientFiles = files.filter((f) => f.path.includes(clientDir));
1979
+ files.push({
1980
+ path: join(serverDir, "sdk-bundle.ts"),
1981
+ content: emitSdkBundle(clientFiles)
1982
+ });
1847
1983
  console.log("✍️ Writing files...");
1848
1984
  await writeFiles(files);
1849
1985
  console.log(`✅ Generated ${files.length} files`);
package/dist/types.d.ts CHANGED
@@ -27,5 +27,11 @@ export interface Config {
27
27
  includeDepthLimit?: number;
28
28
  dateType?: "date" | "string";
29
29
  auth?: AuthConfigInput;
30
+ pull?: PullConfig;
31
+ }
32
+ export interface PullConfig {
33
+ from: string;
34
+ output?: string;
35
+ token?: string;
30
36
  }
31
37
  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.3",
3
+ "version": "0.2.0",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,10 @@
22
22
  },
23
23
  "scripts": {
24
24
  "build": "bun build src/cli.ts src/index.ts --outdir dist --target node --format esm --external=pg --external=zod --external=hono --external=node:* && tsc -p tsconfig.build.json --emitDeclarationOnly",
25
- "test": "bun test/test-gen.ts",
25
+ "test": "bun test:init && bun test:gen && bun test:pull",
26
+ "test:init": "bun test/test-init.ts",
27
+ "test:gen": "bun test/test-gen.ts",
28
+ "test:pull": "bun test/test-pull.ts",
26
29
  "prepublishOnly": "npm run build",
27
30
  "publish:patch": "./publish.sh",
28
31
  "publish:minor": "./publish.sh",