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 +26 -2
- package/dist/cli.js +214 -105
- package/dist/emit-base-client.d.ts +5 -0
- package/dist/index.js +214 -105
- package/dist/types.d.ts +12 -1
- package/package.json +1 -1
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
|
-
|
965
|
-
|
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
|
-
|
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
|
-
|
982
|
-
|
983
|
-
|
1088
|
+
protected baseUrl: string,
|
1089
|
+
protected fetchFn: typeof fetch = fetch,
|
1090
|
+
protected auth?: AuthConfig
|
984
1091
|
) {}
|
985
1092
|
|
986
|
-
|
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
|
-
|
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
|
-
|
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}
|
1128
|
+
throw new Error(\`\${action} \${entity} failed: \${res.status} \${detail}\`);
|
1022
1129
|
}
|
1023
1130
|
}
|
1024
1131
|
|
1025
|
-
|
1026
|
-
|
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(
|
1139
|
+
body: JSON.stringify(body),
|
1030
1140
|
});
|
1031
|
-
|
1032
|
-
|
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
|
-
|
1036
|
-
|
1037
|
-
|
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
|
-
|
1041
|
-
|
1042
|
-
|
1043
|
-
|
1044
|
-
|
1045
|
-
|
1046
|
-
|
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
|
-
|
1056
|
-
|
1057
|
-
|
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(
|
1174
|
+
body: JSON.stringify(body),
|
1061
1175
|
});
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
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
|
-
|
1068
|
-
|
1069
|
-
|
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
|
-
|
1074
|
-
|
1075
|
-
|
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
|
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 (
|
1778
|
-
files.push({ path: join(serverDir, "auth.ts"), content: emitAuth(
|
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:
|
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), !!
|
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);
|
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
|
-
|
964
|
-
|
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
|
-
|
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
|
-
|
981
|
-
|
982
|
-
|
1087
|
+
protected baseUrl: string,
|
1088
|
+
protected fetchFn: typeof fetch = fetch,
|
1089
|
+
protected auth?: AuthConfig
|
983
1090
|
) {}
|
984
1091
|
|
985
|
-
|
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
|
-
|
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
|
-
|
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}
|
1127
|
+
throw new Error(\`\${action} \${entity} failed: \${res.status} \${detail}\`);
|
1021
1128
|
}
|
1022
1129
|
}
|
1023
1130
|
|
1024
|
-
|
1025
|
-
|
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(
|
1138
|
+
body: JSON.stringify(body),
|
1029
1139
|
});
|
1030
|
-
|
1031
|
-
|
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
|
-
|
1035
|
-
|
1036
|
-
|
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
|
-
|
1040
|
-
|
1041
|
-
|
1042
|
-
|
1043
|
-
|
1044
|
-
|
1045
|
-
|
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
|
-
|
1055
|
-
|
1056
|
-
|
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(
|
1173
|
+
body: JSON.stringify(body),
|
1060
1174
|
});
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
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
|
-
|
1067
|
-
|
1068
|
-
|
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
|
-
|
1073
|
-
|
1074
|
-
|
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
|
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 (
|
1777
|
-
files.push({ path: join(serverDir, "auth.ts"), content: emitAuth(
|
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:
|
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), !!
|
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?:
|
29
|
+
auth?: AuthConfigInput;
|
20
30
|
}
|
31
|
+
export declare function normalizeAuthConfig(input: AuthConfigInput | undefined): AuthConfig | undefined;
|