hanzio 1.0.2 → 1.0.6
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 +21 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +149 -0
- package/dist/secrets/index.d.ts +27 -0
- package/dist/secrets/vite.d.ts +9 -0
- package/dist/src/api-wrapper/index.js +250 -0
- package/dist/src/gzip/index.js +16 -0
- package/dist/src/index.js +1967 -0
- package/dist/src/jwt/index.js +19 -0
- package/dist/src/p-queue/index.js +439 -0
- package/dist/src/secrets/index.js +145 -0
- package/dist/src/secrets/vite.js +19 -0
- package/dist/src/sitemap/index.js +552 -0
- package/dist/src/zod/index.js +210 -0
- package/package.json +3 -2
- package/src/index.ts +4 -0
- package/src/secrets/index.ts +206 -0
- package/src/secrets/vite.ts +30 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A small TypeScript-first utility library for common application code: arrays,
|
|
4
4
|
strings, promises, caching, state machines, dates, URLs, Zod helpers, API clients,
|
|
5
|
-
JWTs, gzip, sitemap crawling, queues, and colorful terminal logging.
|
|
5
|
+
JWTs, gzip, sitemap crawling, queues, secrets, and colorful terminal logging.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -211,6 +211,26 @@ import { promiseTimeout } from 'hanzio'
|
|
|
211
211
|
await promiseTimeout(250)
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
+
### Secrets
|
|
215
|
+
|
|
216
|
+
`defineSecretSet` loads a typed set of required secrets from `process.env` or
|
|
217
|
+
Infisical before the rest of the program starts.
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { defineSecretSet } from 'hanzio'
|
|
221
|
+
|
|
222
|
+
export const backendSecrets = await defineSecretSet(
|
|
223
|
+
['DATABASE_URL', 'APP_SECRET'] as const,
|
|
224
|
+
{ projectId: 'infisical-project-id' }
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
const databaseUrl = backendSecrets.secret('DATABASE_URL')
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
For Vite config, `viteSecretSetPlugin(secretSet)` exposes the loaded values as
|
|
231
|
+
`import.meta.env.*` replacements. Use `getViteDefine(secretSet)` if you need the
|
|
232
|
+
raw `define` object.
|
|
233
|
+
|
|
214
234
|
### Cache
|
|
215
235
|
|
|
216
236
|
`cacheFunction` wraps sync or async functions with TTL caching and request
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -879,6 +879,150 @@ class PQueue {
|
|
|
879
879
|
}
|
|
880
880
|
}
|
|
881
881
|
|
|
882
|
+
// src/jwt/index.ts
|
|
883
|
+
import * as jose from "jose";
|
|
884
|
+
async function jwtSign(payload, secret) {
|
|
885
|
+
const secretKey = new TextEncoder().encode(secret);
|
|
886
|
+
return await new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().sign(secretKey);
|
|
887
|
+
}
|
|
888
|
+
async function jwtVerify2(token, secret) {
|
|
889
|
+
try {
|
|
890
|
+
const secretKey = new TextEncoder().encode(secret);
|
|
891
|
+
const { payload } = await jose.jwtVerify(token, secretKey);
|
|
892
|
+
return payload;
|
|
893
|
+
} catch (_error) {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/gzip/index.ts
|
|
899
|
+
import pako from "pako";
|
|
900
|
+
function compressString(input) {
|
|
901
|
+
const uint8Array = new TextEncoder().encode(input);
|
|
902
|
+
const compressed = pako.deflate(uint8Array);
|
|
903
|
+
return btoa(String.fromCharCode.apply(null, compressed));
|
|
904
|
+
}
|
|
905
|
+
function decompressString(compressedString) {
|
|
906
|
+
const compressedData = new Uint8Array(atob(compressedString).split("").map((char) => char.charCodeAt(0)));
|
|
907
|
+
const decompressed = pako.inflate(compressedData);
|
|
908
|
+
return new TextDecoder().decode(decompressed);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/sitemap/index.ts
|
|
912
|
+
var createTimeoutController = (timeout) => {
|
|
913
|
+
const controller = new AbortController;
|
|
914
|
+
setTimeout(() => controller.abort(), timeout);
|
|
915
|
+
return controller;
|
|
916
|
+
};
|
|
917
|
+
var discoverSitemaps = async (domain) => {
|
|
918
|
+
const normalizedDomain = domain.replace(/\/$/, "");
|
|
919
|
+
const commonLocations = [
|
|
920
|
+
"/sitemap.xml",
|
|
921
|
+
"/sitemap_index.xml",
|
|
922
|
+
"/sitemap/",
|
|
923
|
+
"/sitemaps.xml",
|
|
924
|
+
"/sitemap/sitemap.xml"
|
|
925
|
+
];
|
|
926
|
+
const sitemaps = new Set;
|
|
927
|
+
const requestOptions = {
|
|
928
|
+
headers: {
|
|
929
|
+
"User-Agent": "SitemapCrawler/1.0"
|
|
930
|
+
},
|
|
931
|
+
signal: createTimeoutController(5000).signal
|
|
932
|
+
};
|
|
933
|
+
try {
|
|
934
|
+
const robotsTxt = await fetch(`${normalizedDomain}/robots.txt`, {
|
|
935
|
+
...requestOptions
|
|
936
|
+
});
|
|
937
|
+
if (robotsTxt.ok) {
|
|
938
|
+
const text = await robotsTxt.text();
|
|
939
|
+
const sitemapMatches = text.match(/Sitemap: (.*)/gi);
|
|
940
|
+
if (sitemapMatches) {
|
|
941
|
+
sitemapMatches.forEach((match) => {
|
|
942
|
+
const url = match.replace("Sitemap:", "").trim();
|
|
943
|
+
sitemaps.add(url);
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
if (sitemaps.size > 0) {
|
|
947
|
+
return [...sitemaps];
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
} catch (error) {
|
|
951
|
+
console.warn("Could not fetch robots.txt:", error instanceof Error ? error.message : "Unknown error");
|
|
952
|
+
}
|
|
953
|
+
for (const location of commonLocations) {
|
|
954
|
+
try {
|
|
955
|
+
const response = await fetch(`${normalizedDomain}${location}`, {
|
|
956
|
+
...requestOptions,
|
|
957
|
+
method: "HEAD"
|
|
958
|
+
});
|
|
959
|
+
if (response.ok) {
|
|
960
|
+
return [`${normalizedDomain}${location}`];
|
|
961
|
+
}
|
|
962
|
+
} catch (_error) {}
|
|
963
|
+
}
|
|
964
|
+
return [];
|
|
965
|
+
};
|
|
966
|
+
var getDomainSitemap = async (domain, options = {}) => {
|
|
967
|
+
const {
|
|
968
|
+
filterIndexes,
|
|
969
|
+
concurrency = 10,
|
|
970
|
+
maxDepth = 5,
|
|
971
|
+
timeout = 1e4
|
|
972
|
+
} = options;
|
|
973
|
+
let normalizedDomain = domain.replace(/\/$/, "");
|
|
974
|
+
if (!normalizedDomain.startsWith("http")) {
|
|
975
|
+
normalizedDomain = `https://${normalizedDomain}`;
|
|
976
|
+
}
|
|
977
|
+
const sitemapUrls = await discoverSitemaps(normalizedDomain);
|
|
978
|
+
if (sitemapUrls.length === 0) {
|
|
979
|
+
throw new Error("No sitemaps found for the domain");
|
|
980
|
+
}
|
|
981
|
+
const queue = new PQueue({ concurrency });
|
|
982
|
+
const result = {
|
|
983
|
+
links: new Set,
|
|
984
|
+
visited: new Set
|
|
985
|
+
};
|
|
986
|
+
const locRegex = /<loc>([^<]+)<\/loc>/g;
|
|
987
|
+
const isXmlSitemap = (url) => url.endsWith(".xml") || url.toLowerCase().includes("sitemap");
|
|
988
|
+
const processSitemap = async (sitemapUrl, depth = 0) => {
|
|
989
|
+
if (depth > maxDepth || result.visited.has(sitemapUrl)) {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
result.visited.add(sitemapUrl);
|
|
993
|
+
try {
|
|
994
|
+
const controller = createTimeoutController(timeout);
|
|
995
|
+
const response = await fetch(sitemapUrl, {
|
|
996
|
+
headers: {
|
|
997
|
+
"User-Agent": "SitemapCrawler/1.0"
|
|
998
|
+
},
|
|
999
|
+
signal: controller.signal
|
|
1000
|
+
});
|
|
1001
|
+
if (!response.ok)
|
|
1002
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
1003
|
+
const data = await response.text();
|
|
1004
|
+
const matches = [...data.matchAll(locRegex)].map((match) => match[1].trim()).filter((link) => {
|
|
1005
|
+
if (filterIndexes && isXmlSitemap(link)) {
|
|
1006
|
+
return link.includes(filterIndexes);
|
|
1007
|
+
}
|
|
1008
|
+
return true;
|
|
1009
|
+
});
|
|
1010
|
+
for (const link of matches) {
|
|
1011
|
+
if (isXmlSitemap(link)) {
|
|
1012
|
+
queue.add(() => processSitemap(link, depth + 1));
|
|
1013
|
+
} else {
|
|
1014
|
+
result.links.add(link);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
console.warn(`Failed to process sitemap ${sitemapUrl}:`, error instanceof Error ? error.message : "Unknown error");
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
await Promise.all(sitemapUrls.map((sitemapUrl) => queue.add(() => processSitemap(sitemapUrl))));
|
|
1022
|
+
await queue.onIdle();
|
|
1023
|
+
return [...result.links];
|
|
1024
|
+
};
|
|
1025
|
+
|
|
882
1026
|
// src/typedswitch/index.ts
|
|
883
1027
|
function runSwitch(value, keyOrCases, casesOrDefault, defaultCase) {
|
|
884
1028
|
let key;
|
|
@@ -1640,6 +1784,8 @@ export {
|
|
|
1640
1784
|
logBanner,
|
|
1641
1785
|
linearRegressionTrend,
|
|
1642
1786
|
keyBy,
|
|
1787
|
+
jwtVerify2 as jwtVerify,
|
|
1788
|
+
jwtSign,
|
|
1643
1789
|
jsonToZodSchemaMapper,
|
|
1644
1790
|
isSuccess,
|
|
1645
1791
|
isFailure,
|
|
@@ -1649,16 +1795,19 @@ export {
|
|
|
1649
1795
|
getUniqueValues,
|
|
1650
1796
|
getPreviousReportPeriod,
|
|
1651
1797
|
getHeatmapCellStyle,
|
|
1798
|
+
getDomainSitemap,
|
|
1652
1799
|
getDomainFaviconUrl,
|
|
1653
1800
|
generateId,
|
|
1654
1801
|
formatPeriodLabel,
|
|
1655
1802
|
findArrayDifferenceByKey,
|
|
1656
1803
|
extractNumber,
|
|
1804
|
+
decompressString,
|
|
1657
1805
|
createZId,
|
|
1658
1806
|
createResolveColorsByKeys,
|
|
1659
1807
|
createCoolLogger,
|
|
1660
1808
|
createApiWrapper,
|
|
1661
1809
|
createApiClient,
|
|
1810
|
+
compressString,
|
|
1662
1811
|
compact,
|
|
1663
1812
|
colorize,
|
|
1664
1813
|
chunkArray,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type SecretEnvironment = 'dev' | 'staging' | 'prod';
|
|
2
|
+
export type DefineSecretSetOptions<SecretKey extends string> = {
|
|
3
|
+
readonly projectId: string;
|
|
4
|
+
readonly siteUrl?: string;
|
|
5
|
+
readonly environment?: SecretEnvironment | (() => SecretEnvironment);
|
|
6
|
+
readonly clientIdEnvKey?: string;
|
|
7
|
+
readonly clientSecretEnvKey?: string;
|
|
8
|
+
readonly loader?: SecretSetLoader<SecretKey>;
|
|
9
|
+
readonly getLocalIp?: () => string;
|
|
10
|
+
};
|
|
11
|
+
export type SecretSetLoaderContext<SecretKey extends string> = {
|
|
12
|
+
readonly keys: readonly SecretKey[];
|
|
13
|
+
readonly projectId: string;
|
|
14
|
+
readonly environment: SecretEnvironment;
|
|
15
|
+
readonly siteUrl: string;
|
|
16
|
+
readonly clientIdEnvKey: string;
|
|
17
|
+
readonly clientSecretEnvKey: string;
|
|
18
|
+
};
|
|
19
|
+
export type SecretSetLoader<SecretKey extends string> = (context: SecretSetLoaderContext<SecretKey>) => Promise<Partial<Record<SecretKey, string>>>;
|
|
20
|
+
export type SecretSet<SecretKey extends string> = {
|
|
21
|
+
secret: (key: SecretKey) => string;
|
|
22
|
+
secrets: () => Record<SecretKey, string>;
|
|
23
|
+
reload: () => Promise<SecretSet<SecretKey>>;
|
|
24
|
+
};
|
|
25
|
+
export * from './vite';
|
|
26
|
+
export declare function getSecretEnvironment(): SecretEnvironment;
|
|
27
|
+
export declare function defineSecretSet<const SecretKeys extends readonly string[]>(keys: SecretKeys, options: DefineSecretSetOptions<SecretKeys[number]>): Promise<SecretSet<SecretKeys[number]>>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SecretSet } from '.';
|
|
2
|
+
export type ViteSecretSetPlugin = {
|
|
3
|
+
name: string;
|
|
4
|
+
config: () => {
|
|
5
|
+
define: Record<string, string>;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
export declare function getViteDefine<SecretKey extends string>(secretSet: SecretSet<SecretKey>): Record<string, string>;
|
|
9
|
+
export declare function viteSecretSetPlugin<SecretKey extends string>(secretSet: SecretSet<SecretKey>): ViteSecretSetPlugin;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// src/api-wrapper/index.ts
|
|
2
|
+
import { ZodError } from "zod";
|
|
3
|
+
|
|
4
|
+
class ResponseValidationError extends Error {
|
|
5
|
+
zodError;
|
|
6
|
+
rawResponse;
|
|
7
|
+
validationIssues;
|
|
8
|
+
constructor(zodError, rawResponse) {
|
|
9
|
+
const issues = zodError.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
|
|
10
|
+
super(`Response validation failed: ${issues}`);
|
|
11
|
+
this.name = "ResponseValidationError";
|
|
12
|
+
this.zodError = zodError;
|
|
13
|
+
this.rawResponse = rawResponse;
|
|
14
|
+
this.validationIssues = issues;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class HttpResponseError extends Error {
|
|
19
|
+
status;
|
|
20
|
+
body;
|
|
21
|
+
constructor(status, body) {
|
|
22
|
+
super(`HTTP error: ${status} - ${body}`);
|
|
23
|
+
this.name = "HttpResponseError";
|
|
24
|
+
this.status = status;
|
|
25
|
+
this.body = body;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function createApiClient(apiConfig) {
|
|
29
|
+
const request = async (endpointKey, input = {}) => {
|
|
30
|
+
const endpoint = apiConfig.endpoints[endpointKey];
|
|
31
|
+
if (!endpoint) {
|
|
32
|
+
throw new Error(`Unknown API endpoint: ${String(endpointKey)}`);
|
|
33
|
+
}
|
|
34
|
+
const { finalUrl, fullUrl } = buildUrl(endpoint, input.reqParams, input.reqQuery, apiConfig.baseApiUrls, apiConfig.defaultBaseApiUrl, input.url);
|
|
35
|
+
const endpointName = apiConfig.name ? `${apiConfig.name}.${String(endpointKey)}` : String(endpointKey);
|
|
36
|
+
const startedAt = Date.now();
|
|
37
|
+
let retryCount = 0;
|
|
38
|
+
let httpStatus = 0;
|
|
39
|
+
apiConfig.onRequestStart?.({ endpoint: endpointName, url: fullUrl });
|
|
40
|
+
try {
|
|
41
|
+
const config = {
|
|
42
|
+
method: endpoint.method,
|
|
43
|
+
url: finalUrl,
|
|
44
|
+
queryParams: buildQueryParams(endpoint, input.reqQuery),
|
|
45
|
+
body: buildRequestBody(endpoint, input.reqBody),
|
|
46
|
+
headers: buildHeaders(endpoint, input.reqHeaders, apiConfig.defaultHeaders ?? {}),
|
|
47
|
+
doNotEncodeQueryParams: endpoint.doNotEncodeQueryParams,
|
|
48
|
+
timeoutMs: apiConfig.timeoutMs,
|
|
49
|
+
fetchFn: apiConfig.fetch ?? fetch
|
|
50
|
+
};
|
|
51
|
+
const result = await makeRequestWithRetry(config, apiConfig.retries ?? 3, apiConfig.retryDelayMs ?? 300, (response) => validateAndTransformResponse(endpoint, response), apiConfig.shouldRetry ?? defaultShouldRetry, apiConfig.logger);
|
|
52
|
+
retryCount = result.retryCount;
|
|
53
|
+
httpStatus = result.httpStatus;
|
|
54
|
+
const responseTimeMs = Date.now() - startedAt;
|
|
55
|
+
const responseSizeMb = calculateSizeInMb(result.validatedData);
|
|
56
|
+
const requestSizeMb = calculateSizeInMb({
|
|
57
|
+
method: config.method,
|
|
58
|
+
url: fullUrl,
|
|
59
|
+
body: config.body,
|
|
60
|
+
headers: config.headers
|
|
61
|
+
});
|
|
62
|
+
apiConfig.onRequestEnd?.({
|
|
63
|
+
endpoint: endpointName,
|
|
64
|
+
url: fullUrl,
|
|
65
|
+
status: httpStatus,
|
|
66
|
+
retryCount,
|
|
67
|
+
responseTimeMs
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
data: result.validatedData,
|
|
71
|
+
requestSizeMb,
|
|
72
|
+
responseSizeMb,
|
|
73
|
+
requestSizeTotalMb: requestSizeMb + responseSizeMb,
|
|
74
|
+
responseTimeMs,
|
|
75
|
+
httpStatus,
|
|
76
|
+
retryCount
|
|
77
|
+
};
|
|
78
|
+
} catch (error) {
|
|
79
|
+
apiConfig.logger?.error?.(error);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return { request };
|
|
84
|
+
}
|
|
85
|
+
var createApiWrapper = createApiClient;
|
|
86
|
+
async function validateResponse(schema, data) {
|
|
87
|
+
try {
|
|
88
|
+
return await schema.parseAsync(data);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error instanceof ZodError) {
|
|
91
|
+
throw new ResponseValidationError(error, data);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function httpRequest(config) {
|
|
97
|
+
const queryString = config.doNotEncodeQueryParams ? Object.entries(config.queryParams).map(([key, value]) => `${key}=${value}`).join("&") : new URLSearchParams(config.queryParams).toString();
|
|
98
|
+
const fullUrl = queryString ? `${config.url}?${queryString}` : config.url;
|
|
99
|
+
const abortController = new AbortController;
|
|
100
|
+
const timeout = config.timeoutMs === undefined ? undefined : setTimeout(() => abortController.abort(), config.timeoutMs);
|
|
101
|
+
try {
|
|
102
|
+
return await config.fetchFn(fullUrl, {
|
|
103
|
+
method: config.method,
|
|
104
|
+
headers: config.headers,
|
|
105
|
+
body: buildFetchBody(config.body, config.headers),
|
|
106
|
+
signal: abortController.signal
|
|
107
|
+
});
|
|
108
|
+
} finally {
|
|
109
|
+
if (timeout)
|
|
110
|
+
clearTimeout(timeout);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function makeRequestWithRetry(config, maxRetries, retryDelayMs, validateFn, shouldRetry, logger) {
|
|
114
|
+
let retryCount = 0;
|
|
115
|
+
while (true) {
|
|
116
|
+
try {
|
|
117
|
+
logger?.debug?.("API request", config);
|
|
118
|
+
const response = await httpRequest(config);
|
|
119
|
+
if (response.status >= 400) {
|
|
120
|
+
if (retryCount < maxRetries && shouldRetry({ response, retryCount, maxRetries })) {
|
|
121
|
+
retryCount++;
|
|
122
|
+
await delay(retryDelayMs);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
throw new HttpResponseError(response.status, await response.text());
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
validatedData: await validateFn(response),
|
|
129
|
+
retryCount,
|
|
130
|
+
httpStatus: response.status
|
|
131
|
+
};
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error instanceof HttpResponseError)
|
|
134
|
+
throw error;
|
|
135
|
+
if (retryCount >= maxRetries || !shouldRetry({ error, retryCount, maxRetries })) {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
retryCount++;
|
|
139
|
+
await delay(retryDelayMs);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function validateAndTransformResponse(endpoint, response) {
|
|
144
|
+
const responseHeaders = getHeadersAsObject(response.headers);
|
|
145
|
+
const contentType = responseHeaders["content-type"] ?? "";
|
|
146
|
+
const responseData = contentType.includes("application/json") ? await response.json() : await response.text();
|
|
147
|
+
const transformedData = endpoint.resFormatter ? endpoint.resFormatter(responseData, responseHeaders) : responseData;
|
|
148
|
+
return validateResponse(endpoint.resSchema, transformedData);
|
|
149
|
+
}
|
|
150
|
+
function buildUrl(endpoint, params, reqQuery, baseApiUrls, defaultBaseApiUrl, url) {
|
|
151
|
+
if (url) {
|
|
152
|
+
const finalUrl2 = replacePathParams(url, params);
|
|
153
|
+
const queryString2 = new URLSearchParams(buildQueryParams(endpoint, reqQuery)).toString();
|
|
154
|
+
return {
|
|
155
|
+
finalUrl: finalUrl2,
|
|
156
|
+
fullUrl: queryString2 ? `${finalUrl2}?${queryString2}` : finalUrl2
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const defaultBaseUrl = defaultBaseApiUrl ?? Object.keys(baseApiUrls)[0];
|
|
160
|
+
if (!defaultBaseUrl) {
|
|
161
|
+
throw new Error("At least one base API URL is required");
|
|
162
|
+
}
|
|
163
|
+
const baseUrlKey = endpoint.baseApiUrl ?? defaultBaseUrl;
|
|
164
|
+
const configBaseUrl = baseApiUrls[baseUrlKey];
|
|
165
|
+
if (!configBaseUrl) {
|
|
166
|
+
throw new Error(`Unknown base API URL: ${baseUrlKey}`);
|
|
167
|
+
}
|
|
168
|
+
const baseUrl = typeof configBaseUrl === "function" ? configBaseUrl() : configBaseUrl;
|
|
169
|
+
const path = typeof endpoint.path === "function" ? endpoint.path(baseUrl) : endpoint.path;
|
|
170
|
+
const finalUrl = replacePathParams(`${baseUrl}${path}`, params);
|
|
171
|
+
const queryString = new URLSearchParams(buildQueryParams(endpoint, reqQuery)).toString();
|
|
172
|
+
return {
|
|
173
|
+
finalUrl,
|
|
174
|
+
fullUrl: queryString ? `${finalUrl}?${queryString}` : finalUrl
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function buildQueryParams(endpoint, query) {
|
|
178
|
+
const defaultParams = endpoint.reqDefaultQueryParams ?? {};
|
|
179
|
+
const parsedQuery = endpoint.reqQuerySchema ? endpoint.reqQuerySchema.parse(query) : query ?? {};
|
|
180
|
+
const merged = { ...defaultParams, ...parsedQuery };
|
|
181
|
+
return Object.fromEntries(Object.entries(merged).filter(([, value]) => value !== undefined).map(([key, value]) => [key, String(value)]));
|
|
182
|
+
}
|
|
183
|
+
function buildRequestBody(endpoint, body) {
|
|
184
|
+
if (!endpoint.reqBodySchema)
|
|
185
|
+
return body;
|
|
186
|
+
const parsedBody = endpoint.reqBodySchema.parse(body);
|
|
187
|
+
if (endpoint.reqBodyFormat !== "form-data") {
|
|
188
|
+
return parsedBody;
|
|
189
|
+
}
|
|
190
|
+
const formData = new FormData;
|
|
191
|
+
for (const [key, value] of Object.entries(parsedBody)) {
|
|
192
|
+
if (value == null)
|
|
193
|
+
continue;
|
|
194
|
+
formData.set(key, value instanceof Blob ? value : String(value));
|
|
195
|
+
}
|
|
196
|
+
return formData;
|
|
197
|
+
}
|
|
198
|
+
function buildHeaders(endpoint, headers, defaultHeaders) {
|
|
199
|
+
const contentType = endpoint.reqBodyFormat === "json" ? "application/json" : undefined;
|
|
200
|
+
const parsedHeaders = endpoint.reqHeadersSchema ? endpoint.reqHeadersSchema.parse(headers) : headers ?? {};
|
|
201
|
+
return {
|
|
202
|
+
...defaultHeaders,
|
|
203
|
+
...contentType && { "Content-Type": contentType },
|
|
204
|
+
...endpoint.defaultHeaders,
|
|
205
|
+
...parsedHeaders
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function buildFetchBody(body, headers) {
|
|
209
|
+
if (body === undefined || body === null)
|
|
210
|
+
return;
|
|
211
|
+
if (body instanceof FormData || body instanceof Blob)
|
|
212
|
+
return body;
|
|
213
|
+
if (headers["Content-Type"] === "application/json")
|
|
214
|
+
return JSON.stringify(body);
|
|
215
|
+
return body;
|
|
216
|
+
}
|
|
217
|
+
function replacePathParams(path, params) {
|
|
218
|
+
return path.replace(/:(\w+)/g, (_, key) => {
|
|
219
|
+
const value = params?.[key];
|
|
220
|
+
if (value === undefined) {
|
|
221
|
+
throw new Error(`Missing required path parameter: ${key}`);
|
|
222
|
+
}
|
|
223
|
+
return encodeURIComponent(String(value));
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function getHeadersAsObject(headers) {
|
|
227
|
+
const result = {};
|
|
228
|
+
headers.forEach((value, key) => {
|
|
229
|
+
result[key] = value;
|
|
230
|
+
});
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
function calculateSizeInMb(data) {
|
|
234
|
+
const sizeInBytes = new TextEncoder().encode(JSON.stringify(data)).length;
|
|
235
|
+
return sizeInBytes / (1024 * 1024);
|
|
236
|
+
}
|
|
237
|
+
function defaultShouldRetry({ error, response }) {
|
|
238
|
+
if (error)
|
|
239
|
+
return true;
|
|
240
|
+
return response ? response.status >= 500 : false;
|
|
241
|
+
}
|
|
242
|
+
function delay(ms) {
|
|
243
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
244
|
+
}
|
|
245
|
+
export {
|
|
246
|
+
createApiWrapper,
|
|
247
|
+
createApiClient,
|
|
248
|
+
ResponseValidationError,
|
|
249
|
+
HttpResponseError
|
|
250
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/gzip/index.ts
|
|
2
|
+
import pako from "pako";
|
|
3
|
+
function compressString(input) {
|
|
4
|
+
const uint8Array = new TextEncoder().encode(input);
|
|
5
|
+
const compressed = pako.deflate(uint8Array);
|
|
6
|
+
return btoa(String.fromCharCode.apply(null, compressed));
|
|
7
|
+
}
|
|
8
|
+
function decompressString(compressedString) {
|
|
9
|
+
const compressedData = new Uint8Array(atob(compressedString).split("").map((char) => char.charCodeAt(0)));
|
|
10
|
+
const decompressed = pako.inflate(compressedData);
|
|
11
|
+
return new TextDecoder().decode(decompressed);
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
decompressString,
|
|
15
|
+
compressString
|
|
16
|
+
};
|