hanzio 1.0.10 → 1.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.
- package/README.md +70 -4
- package/dist/api-wrapper/actions-impl.d.ts +4 -0
- package/dist/api-wrapper/builders.d.ts +23 -0
- package/dist/api-wrapper/create-client.d.ts +6 -0
- package/dist/api-wrapper/error-reporting.d.ts +7 -0
- package/dist/api-wrapper/errors.d.ts +64 -0
- package/dist/api-wrapper/headers.d.ts +10 -0
- package/dist/api-wrapper/index.d.ts +5 -84
- package/dist/api-wrapper/redaction.d.ts +2 -0
- package/dist/api-wrapper/shared.d.ts +16 -0
- package/dist/api-wrapper/size.d.ts +1 -0
- package/dist/api-wrapper/test-helpers.d.ts +6 -0
- package/dist/api-wrapper/transport.d.ts +55 -0
- package/dist/api-wrapper/types.d.ts +154 -0
- package/dist/api-wrapper/zod-issues.d.ts +17 -0
- package/dist/cache/index.d.ts +26 -0
- package/dist/secrets/core.d.ts +47 -0
- package/dist/secrets/deadline.d.ts +6 -0
- package/dist/secrets/environment.d.ts +9 -0
- package/dist/secrets/errors.d.ts +11 -0
- package/dist/secrets/index.d.ts +14 -23
- package/dist/secrets/infisical-cli.d.ts +6 -0
- package/dist/secrets/infisical-config.d.ts +17 -0
- package/dist/secrets/infisical-session.d.ts +4 -0
- package/dist/secrets/infisical.d.ts +14 -2
- package/dist/secrets/loaders.d.ts +9 -0
- package/dist/secrets/vite.d.ts +7 -3
- package/dist/secrets/worker-loader.d.ts +9 -0
- package/dist/secrets/worker.d.ts +3 -0
- package/dist/src/api-wrapper/index.js +1059 -177
- package/dist/src/index.js +2807 -1397
- package/dist/src/jwt/index.js +2 -2
- package/dist/src/p-queue/index.js +3 -3
- package/dist/src/secrets/index.js +1610 -407
- package/dist/src/secrets/infisical.js +1352 -214
- package/dist/src/secrets/loaders.js +1529 -0
- package/dist/src/secrets/vite.js +33 -11
- package/dist/src/secrets/worker.js +275 -0
- package/dist/src/sitemap/index.js +1 -1
- package/dist/src/zod/index.js +7 -7
- package/dist/typedswitch/index.d.ts +9 -5
- package/package.json +12 -3
- package/src/api-wrapper/actions-impl.ts +100 -0
- package/src/api-wrapper/builders.ts +200 -0
- package/src/api-wrapper/create-client.ts +393 -0
- package/src/api-wrapper/error-reporting.ts +31 -0
- package/src/api-wrapper/errors.ts +231 -0
- package/src/api-wrapper/headers.ts +79 -0
- package/src/api-wrapper/index.ts +5 -492
- package/src/api-wrapper/redaction.ts +62 -0
- package/src/api-wrapper/shared.ts +25 -0
- package/src/api-wrapper/size.ts +19 -0
- package/src/api-wrapper/test-helpers.ts +25 -0
- package/src/api-wrapper/transport.ts +350 -0
- package/src/api-wrapper/types.ts +259 -0
- package/src/api-wrapper/zod-issues.ts +157 -0
- package/src/cache/index.ts +228 -61
- package/src/secrets/core.ts +348 -0
- package/src/secrets/deadline.ts +69 -0
- package/src/secrets/environment.ts +69 -0
- package/src/secrets/errors.ts +29 -0
- package/src/secrets/index.ts +81 -140
- package/src/secrets/infisical-cli.ts +99 -0
- package/src/secrets/infisical-config.ts +70 -0
- package/src/secrets/infisical-session.ts +42 -0
- package/src/secrets/infisical.ts +219 -35
- package/src/secrets/loaders.ts +43 -0
- package/src/secrets/vite.ts +44 -15
- package/src/secrets/worker-loader.ts +56 -0
- package/src/secrets/worker.ts +4 -0
- package/src/typedswitch/index.ts +80 -69
package/README.md
CHANGED
|
@@ -218,26 +218,51 @@ await promiseTimeout(250)
|
|
|
218
218
|
`defineSecretSet` loads a typed set of required secrets from `process.env` or
|
|
219
219
|
Infisical before the rest of the program starts.
|
|
220
220
|
|
|
221
|
+
When both `INFISICAL_CLIENT_ID` and `INFISICAL_CLIENT_SECRET` are set, it uses
|
|
222
|
+
Infisical's HTTP API. If either is missing, it uses your signed-in Infisical CLI
|
|
223
|
+
session. For local development, install the CLI and run
|
|
224
|
+
`infisical login --domain=https://eu.infisical.com` (or your configured
|
|
225
|
+
`siteUrl`). See the [secrets documentation](src/secrets/README.md) for details.
|
|
226
|
+
|
|
221
227
|
```ts
|
|
222
228
|
import { defineSecretSet } from 'hanzio/secrets'
|
|
223
229
|
|
|
224
230
|
export const backendSecrets = await defineSecretSet(
|
|
225
231
|
['DATABASE_URL', 'APP_SECRET'] as const,
|
|
226
|
-
{
|
|
232
|
+
{
|
|
233
|
+
organizationId: 'infisical-organization-id',
|
|
234
|
+
projectId: 'infisical-project-id'
|
|
235
|
+
}
|
|
227
236
|
)
|
|
228
237
|
|
|
229
238
|
const databaseUrl = backendSecrets.secret('DATABASE_URL')
|
|
230
239
|
```
|
|
231
240
|
|
|
241
|
+
The required `organizationId` scopes the CLI session per secret set, allowing projects
|
|
242
|
+
in different organizations to run together without switching the active CLI
|
|
243
|
+
organization. This flow reads the CLI user token and fetches secrets over HTTP;
|
|
244
|
+
tokens remain in memory. Use `writeToProcessEnv: false` for multiple sets in one
|
|
245
|
+
process. Both `organizationId` and `projectId` are required for Infisical loading.
|
|
246
|
+
|
|
232
247
|
For Vite config, `viteSecretSetPlugin(secretSet)` exposes the loaded values as
|
|
233
248
|
`import.meta.env.*` replacements. Use `getViteDefine(secretSet)` if you need the
|
|
234
|
-
raw `define` object.
|
|
235
|
-
|
|
249
|
+
raw `define` object. Vite helpers accept only `VITE_` keys by default; use an
|
|
250
|
+
explicit `publicKeys` list to select public values from a mixed set. Import
|
|
251
|
+
these helpers from `hanzio/secrets/vite`.
|
|
252
|
+
|
|
253
|
+
The helper supports `auth: 'auto' | 'http' | 'cli'`, `secretPath`, deadlines,
|
|
254
|
+
cancellation, optional schema validation, per-key source metadata, and
|
|
255
|
+
`writeToProcessEnv: false` for isolated sets. Use `hanzio/secrets/worker` for
|
|
256
|
+
Cloudflare bindings without Node compatibility. See the
|
|
257
|
+
[migration notes](src/secrets/README.md#migration-notes) for stricter environment
|
|
258
|
+
validation and browser exposure rules.
|
|
236
259
|
|
|
237
260
|
### Cache
|
|
238
261
|
|
|
239
262
|
`cacheFunction` wraps sync or async functions with TTL caching and request
|
|
240
|
-
deduplication.
|
|
263
|
+
deduplication. With `refreshInBackground: true`, stale entries are served
|
|
264
|
+
immediately while a background refresh updates the cache
|
|
265
|
+
(stale-while-revalidate).
|
|
241
266
|
|
|
242
267
|
```ts
|
|
243
268
|
import { cacheFunction } from 'hanzio'
|
|
@@ -252,6 +277,47 @@ const user = await getUser('user_1')
|
|
|
252
277
|
getUser.clearCache()
|
|
253
278
|
```
|
|
254
279
|
|
|
280
|
+
#### External stores (e.g. Redis)
|
|
281
|
+
|
|
282
|
+
By default entries live in an in-memory `Map`. Pass a `store` to keep entries
|
|
283
|
+
in an external cache instead. A store implements `get`, `set`, and `delete`
|
|
284
|
+
(plus an optional `clear`, used by `clearCache`); hanzio never imports the
|
|
285
|
+
underlying client, so any backend works. Serialization is the adapter's
|
|
286
|
+
responsibility, and the `ttlMs` passed to `set` maps to the store's native
|
|
287
|
+
expiry. External stores are only supported for async functions.
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { cacheFunction, type CacheStore, type CacheStoreEntry } from 'hanzio'
|
|
291
|
+
import Redis from 'ioredis'
|
|
292
|
+
|
|
293
|
+
const redis = new Redis()
|
|
294
|
+
|
|
295
|
+
const redisStore: CacheStore = {
|
|
296
|
+
get: async (key) => {
|
|
297
|
+
const raw = await redis.get(key)
|
|
298
|
+
return raw ? (JSON.parse(raw) as CacheStoreEntry) : undefined
|
|
299
|
+
},
|
|
300
|
+
set: async (key, entry, ttlMs) => {
|
|
301
|
+
await redis.set(key, JSON.stringify(entry), 'PX', ttlMs)
|
|
302
|
+
},
|
|
303
|
+
delete: async (key) => {
|
|
304
|
+
await redis.del(key)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const getUser = cacheFunction({
|
|
309
|
+
name: 'getUser',
|
|
310
|
+
fn: async (id: string) => ({ id, name: 'Ada' }),
|
|
311
|
+
cacheTimeMs: 60_000,
|
|
312
|
+
store: redisStore,
|
|
313
|
+
onStoreError: (error) => console.error('cache store failed', error)
|
|
314
|
+
})
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Store failures never break the cached function: on a failed `get` or `set`
|
|
318
|
+
the wrapped function falls back to calling `fn` directly, and the optional
|
|
319
|
+
`onStoreError` callback is invoked.
|
|
320
|
+
|
|
255
321
|
### State Machine
|
|
256
322
|
|
|
257
323
|
`SimpleStateMachine` models typed states, actions, validation, and action
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ConfigError } from './errors';
|
|
2
|
+
import type { ApiErrorContext, BaseApiUrl, PathParams, QueryParams } from './shared';
|
|
3
|
+
import type { ApiEndpoint } from './types';
|
|
4
|
+
export type BuildUrlOptions = {
|
|
5
|
+
endpoint: ApiEndpoint;
|
|
6
|
+
params: PathParams | undefined;
|
|
7
|
+
queryParams: Record<string, string>;
|
|
8
|
+
baseApiUrls: Record<string, BaseApiUrl>;
|
|
9
|
+
defaultBaseApiUrl: string | undefined;
|
|
10
|
+
url: string | undefined;
|
|
11
|
+
doNotEncodeQueryParams?: boolean;
|
|
12
|
+
configError: (msg: string) => ConfigError;
|
|
13
|
+
};
|
|
14
|
+
export declare function formatQueryString(queryParams: Record<string, string>, doNotEncode?: boolean): string;
|
|
15
|
+
export declare function buildUrl(options: BuildUrlOptions): {
|
|
16
|
+
finalUrl: string;
|
|
17
|
+
fullUrl: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function buildPathParams(endpoint: ApiEndpoint, params: PathParams | undefined, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): PathParams | undefined;
|
|
20
|
+
export declare function buildQueryParams(endpoint: ApiEndpoint, query: QueryParams | undefined, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): Record<string, string>;
|
|
21
|
+
export declare function buildRequestBody(endpoint: ApiEndpoint, body: unknown, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): unknown;
|
|
22
|
+
export declare function buildHeaders(endpoint: ApiEndpoint, headers: Record<string, string> | undefined, defaultHeaders: Record<string, string>, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): Record<string, string>;
|
|
23
|
+
export declare function replacePathParams(path: string, params: PathParams | undefined, configError: (msg: string) => ConfigError): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ApiAction, ApiClient, ApiClientConfigNoActions, ApiClientConfigWithActions, ApiEndpoint } from './types';
|
|
2
|
+
export interface CreateApiClientFn {
|
|
3
|
+
<T extends Record<string, ApiEndpoint>>(apiConfig: ApiClientConfigNoActions<T>): ApiClient<T, Record<string, never>>;
|
|
4
|
+
<T extends Record<string, ApiEndpoint>, A extends Record<string, ApiAction>>(apiConfig: ApiClientConfigWithActions<T, A>): ApiClient<T, A>;
|
|
5
|
+
}
|
|
6
|
+
export declare const createApiClient: CreateApiClientFn;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ApiError } from './errors';
|
|
2
|
+
import type { ApiClientConfig } from './types';
|
|
3
|
+
import type { ApiErrorContext } from './shared';
|
|
4
|
+
type ErrorHookConfig = Pick<ApiClientConfig<never, never>, 'logger' | 'onError' | 'redact'>;
|
|
5
|
+
export declare function applyErrorContextRedaction(ctx: ApiErrorContext, config: Pick<ApiClientConfig<never, never>, 'redact'>): ApiErrorContext;
|
|
6
|
+
export declare function reportApiError(err: ApiError, config: ErrorHookConfig): Promise<ApiError>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ZodError } from 'zod';
|
|
2
|
+
import type { ApiErrorContext } from './shared';
|
|
3
|
+
import { type FormattedZodIssue } from './zod-issues';
|
|
4
|
+
export declare class ApiError extends Error {
|
|
5
|
+
context: ApiErrorContext;
|
|
6
|
+
cause?: unknown;
|
|
7
|
+
constructor(message: string, context: ApiErrorContext, options?: {
|
|
8
|
+
cause?: unknown;
|
|
9
|
+
});
|
|
10
|
+
toJSON(): Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
export declare class HttpResponseError extends ApiError {
|
|
13
|
+
readonly status: number;
|
|
14
|
+
readonly body: string;
|
|
15
|
+
readonly bodyJson?: unknown;
|
|
16
|
+
readonly responseHeaders: Record<string, string>;
|
|
17
|
+
readonly requestId?: string;
|
|
18
|
+
constructor(status: number, body: string, responseHeaders: Record<string, string>, context: ApiErrorContext);
|
|
19
|
+
toJSON(): Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export declare class ResponseValidationError extends ApiError {
|
|
22
|
+
readonly zodError: ZodError;
|
|
23
|
+
readonly rawResponse: unknown;
|
|
24
|
+
readonly issues: FormattedZodIssue[];
|
|
25
|
+
readonly validationIssues: string;
|
|
26
|
+
constructor(zodError: ZodError, rawResponse: unknown, context: ApiErrorContext);
|
|
27
|
+
toJSON(): Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
export type RequestValidationTarget = 'body' | 'query' | 'params' | 'headers';
|
|
30
|
+
export declare class RequestValidationError extends ApiError {
|
|
31
|
+
readonly zodError: ZodError;
|
|
32
|
+
readonly target: RequestValidationTarget;
|
|
33
|
+
readonly rawInput: unknown;
|
|
34
|
+
readonly issues: FormattedZodIssue[];
|
|
35
|
+
readonly validationIssues: string;
|
|
36
|
+
constructor(zodError: ZodError, target: RequestValidationTarget, rawInput: unknown, context: ApiErrorContext);
|
|
37
|
+
toJSON(): Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
export declare class RequestTimeoutError extends ApiError {
|
|
40
|
+
readonly timeoutMs: number;
|
|
41
|
+
constructor(timeoutMs: number, context: ApiErrorContext);
|
|
42
|
+
}
|
|
43
|
+
export declare class NetworkError extends ApiError {
|
|
44
|
+
constructor(message: string, context: ApiErrorContext, cause?: unknown);
|
|
45
|
+
}
|
|
46
|
+
export declare class RequestAbortedError extends ApiError {
|
|
47
|
+
constructor(context: ApiErrorContext, cause?: unknown);
|
|
48
|
+
}
|
|
49
|
+
export declare class ConfigError extends ApiError {
|
|
50
|
+
constructor(message: string, context: ApiErrorContext);
|
|
51
|
+
}
|
|
52
|
+
export declare class ActionError extends ApiError {
|
|
53
|
+
constructor(message: string, context: ApiErrorContext, cause?: unknown);
|
|
54
|
+
}
|
|
55
|
+
export declare const isApiError: (e: unknown) => e is ApiError;
|
|
56
|
+
export declare const isHttpResponseError: (e: unknown) => e is HttpResponseError;
|
|
57
|
+
export declare const isResponseValidationError: (e: unknown) => e is ResponseValidationError;
|
|
58
|
+
export declare const isRequestValidationError: (e: unknown) => e is RequestValidationError;
|
|
59
|
+
export declare const isRequestTimeoutError: (e: unknown) => e is RequestTimeoutError;
|
|
60
|
+
export declare const isNetworkError: (e: unknown) => e is NetworkError;
|
|
61
|
+
export declare const isRequestAbortedError: (e: unknown) => e is RequestAbortedError;
|
|
62
|
+
export declare const isConfigError: (e: unknown) => e is ConfigError;
|
|
63
|
+
export declare const isActionError: (e: unknown) => e is ActionError;
|
|
64
|
+
export declare function isNonRetryableApiError(error: unknown): boolean;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ConfigError } from './errors';
|
|
2
|
+
/**
|
|
3
|
+
* Normalize headers to canonical case so lookups are deterministic. Later
|
|
4
|
+
* occurrences of the same header (case-insensitive) win.
|
|
5
|
+
*/
|
|
6
|
+
export declare function normalizeHeaders(headers: Record<string, string>): Record<string, string>;
|
|
7
|
+
export declare function getHeader(headers: Record<string, string>, name: string): string | undefined;
|
|
8
|
+
export declare function hasJsonContentType(headers: Record<string, string>): boolean;
|
|
9
|
+
export declare function buildFetchBody(body: unknown, headers: Record<string, string>, configError?: (msg: string) => ConfigError): RequestInit['body'];
|
|
10
|
+
export declare function getHeadersAsObject(headers: Headers): Record<string, string>;
|
|
@@ -1,84 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export type QueryParams = Record<string, string | number | boolean | undefined>;
|
|
7
|
-
export declare class ResponseValidationError extends Error {
|
|
8
|
-
readonly zodError: ZodError;
|
|
9
|
-
readonly rawResponse: unknown;
|
|
10
|
-
readonly validationIssues: string;
|
|
11
|
-
constructor(zodError: ZodError, rawResponse: unknown);
|
|
12
|
-
}
|
|
13
|
-
export declare class HttpResponseError extends Error {
|
|
14
|
-
readonly status: number;
|
|
15
|
-
readonly body: string;
|
|
16
|
-
constructor(status: number, body: string);
|
|
17
|
-
}
|
|
18
|
-
export interface ApiEndpoint<TReqBody extends z.ZodType | undefined = z.ZodType | undefined, TReqParams extends z.ZodType | undefined = z.ZodType | undefined, TReqQuery extends z.ZodType | undefined = z.ZodType | undefined, TReqHeaders extends z.ZodType | undefined = z.ZodType | undefined, TResponse extends z.ZodType = z.ZodType> {
|
|
19
|
-
method: HttpMethod;
|
|
20
|
-
path: string | ((baseUrl: string) => string);
|
|
21
|
-
reqBodySchema?: TReqBody;
|
|
22
|
-
reqParamsSchema?: TReqParams;
|
|
23
|
-
reqQuerySchema?: TReqQuery;
|
|
24
|
-
reqHeadersSchema?: TReqHeaders;
|
|
25
|
-
resSchema: TResponse;
|
|
26
|
-
baseApiUrl?: string;
|
|
27
|
-
reqBodyFormat?: RequestBodyFormat;
|
|
28
|
-
defaultHeaders?: Record<string, string>;
|
|
29
|
-
resFormatter?: (data: unknown, headers: Record<string, string>) => z.infer<TResponse>;
|
|
30
|
-
reqDefaultQueryParams?: QueryParams;
|
|
31
|
-
doNotEncodeQueryParams?: boolean;
|
|
32
|
-
}
|
|
33
|
-
export interface ApiWrapperResponse<T> {
|
|
34
|
-
data: T;
|
|
35
|
-
requestSizeMb: number;
|
|
36
|
-
responseSizeMb: number;
|
|
37
|
-
requestSizeTotalMb: number;
|
|
38
|
-
responseTimeMs: number;
|
|
39
|
-
httpStatus: number;
|
|
40
|
-
retryCount: number;
|
|
41
|
-
}
|
|
42
|
-
export type RetryContext = {
|
|
43
|
-
error?: unknown;
|
|
44
|
-
response?: Response;
|
|
45
|
-
retryCount: number;
|
|
46
|
-
maxRetries: number;
|
|
47
|
-
};
|
|
48
|
-
export interface ApiClientConfig<T extends Record<string, ApiEndpoint>> {
|
|
49
|
-
name?: string;
|
|
50
|
-
baseApiUrls: Record<string, BaseApiUrl>;
|
|
51
|
-
defaultBaseApiUrl?: string;
|
|
52
|
-
endpoints: T;
|
|
53
|
-
defaultHeaders?: Record<string, string>;
|
|
54
|
-
timeoutMs?: number;
|
|
55
|
-
retries?: number;
|
|
56
|
-
retryDelayMs?: number;
|
|
57
|
-
shouldRetry?: (context: RetryContext) => boolean;
|
|
58
|
-
logger?: Pick<Console, 'debug' | 'error'>;
|
|
59
|
-
fetch?: typeof fetch;
|
|
60
|
-
onRequestStart?: (context: {
|
|
61
|
-
endpoint: string;
|
|
62
|
-
url: string;
|
|
63
|
-
}) => void;
|
|
64
|
-
onRequestEnd?: (context: {
|
|
65
|
-
endpoint: string;
|
|
66
|
-
url: string;
|
|
67
|
-
status: number;
|
|
68
|
-
retryCount: number;
|
|
69
|
-
responseTimeMs: number;
|
|
70
|
-
}) => void;
|
|
71
|
-
}
|
|
72
|
-
type RequestInput<TEndpoint extends ApiEndpoint> = {
|
|
73
|
-
reqBody?: InferOptionalSchema<TEndpoint['reqBodySchema']>;
|
|
74
|
-
reqParams?: InferOptionalSchema<TEndpoint['reqParamsSchema']>;
|
|
75
|
-
reqQuery?: InferOptionalSchema<TEndpoint['reqQuerySchema']>;
|
|
76
|
-
reqHeaders?: InferOptionalSchema<TEndpoint['reqHeadersSchema']>;
|
|
77
|
-
url?: string;
|
|
78
|
-
};
|
|
79
|
-
type InferOptionalSchema<TSchema> = TSchema extends z.ZodType ? z.infer<TSchema> : undefined;
|
|
80
|
-
export declare function createApiClient<T extends Record<string, ApiEndpoint>>(apiConfig: ApiClientConfig<T>): {
|
|
81
|
-
request: <K extends keyof T>(endpointKey: K, input?: RequestInput<T[K]>) => Promise<ApiWrapperResponse<z.TypeOf<T[K]["resSchema"]>>>;
|
|
82
|
-
};
|
|
83
|
-
export declare const createApiWrapper: typeof createApiClient;
|
|
84
|
-
export {};
|
|
1
|
+
export type { FormattedZodIssue } from './zod-issues';
|
|
2
|
+
export * from './types';
|
|
3
|
+
export * from './errors';
|
|
4
|
+
export { createApiClient } from './create-client';
|
|
5
|
+
export { defineAction } from './actions-impl';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
|
2
|
+
export type RequestBodyFormat = 'json' | 'form-data';
|
|
3
|
+
export type BaseApiUrl = string | (() => string);
|
|
4
|
+
export type PathParams = Record<string, string | number>;
|
|
5
|
+
export type QueryParams = Record<string, string | number | boolean | undefined>;
|
|
6
|
+
export type ApiErrorContext = {
|
|
7
|
+
endpoint: string;
|
|
8
|
+
method: HttpMethod;
|
|
9
|
+
url: string;
|
|
10
|
+
attempt: number;
|
|
11
|
+
maxRetries: number;
|
|
12
|
+
elapsedMs: number;
|
|
13
|
+
requestHeaders?: Record<string, string>;
|
|
14
|
+
requestBody?: unknown;
|
|
15
|
+
meta?: Record<string, unknown>;
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function calculateSizeInMb(data: unknown): number;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const originalFetch: typeof fetch;
|
|
2
|
+
export declare const createMockResponse: (body: unknown, options?: {
|
|
3
|
+
status?: number | undefined;
|
|
4
|
+
contentType?: string | undefined;
|
|
5
|
+
}) => Response;
|
|
6
|
+
export declare const mockFetch: (fn: (url: string, options?: RequestInit | undefined) => Promise<Response>) => void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type z } from 'zod';
|
|
2
|
+
import type { ApiEndpoint, OnResponseContext, OnRetryContext, RetryContext } from './types';
|
|
3
|
+
import type { ApiErrorContext, HttpMethod } from './shared';
|
|
4
|
+
export type RequestConfig = {
|
|
5
|
+
method: HttpMethod;
|
|
6
|
+
fullUrl: string;
|
|
7
|
+
body: unknown;
|
|
8
|
+
headers: Record<string, string>;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
fetchFn: typeof fetch;
|
|
11
|
+
userSignal?: AbortSignal;
|
|
12
|
+
};
|
|
13
|
+
export declare function validateResponse<T extends z.ZodType>(schema: T, data: unknown, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): Promise<z.infer<T>>;
|
|
14
|
+
export declare function httpRequest(config: RequestConfig, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): Promise<Response>;
|
|
15
|
+
export declare function mergeSignals(a: AbortSignal, b: AbortSignal | undefined): AbortSignal;
|
|
16
|
+
export declare function readErrorResponse(response: Response): Promise<{
|
|
17
|
+
body: string;
|
|
18
|
+
headers: Record<string, string>;
|
|
19
|
+
status: number;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function getRetryDelay(params: {
|
|
22
|
+
retryDelay: number | ((attempt: number) => number);
|
|
23
|
+
attempt: number;
|
|
24
|
+
response?: Response;
|
|
25
|
+
maxRetryDelayMs?: number;
|
|
26
|
+
}): number;
|
|
27
|
+
export declare function shouldAttemptRetryOnError(error: unknown): boolean;
|
|
28
|
+
export declare function shouldAttemptRetryOnHttp(retryCtx: RetryContext): boolean;
|
|
29
|
+
type MakeRequestArgs<TValidated> = {
|
|
30
|
+
config: RequestConfig;
|
|
31
|
+
maxRetries: number;
|
|
32
|
+
retryDelay: number | ((attempt: number) => number);
|
|
33
|
+
maxRetryDelayMs?: number;
|
|
34
|
+
validateFn: (response: Response, attempt: number) => Promise<TValidated>;
|
|
35
|
+
shouldRetry: (context: RetryContext) => boolean;
|
|
36
|
+
logger?: Pick<Console, 'debug'>;
|
|
37
|
+
ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext;
|
|
38
|
+
endpointName: string;
|
|
39
|
+
method: HttpMethod;
|
|
40
|
+
fullUrl: string;
|
|
41
|
+
meta?: Record<string, unknown>;
|
|
42
|
+
onResponse?: (context: OnResponseContext) => void | Promise<void>;
|
|
43
|
+
onRetry?: (context: OnRetryContext) => void | Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
export declare function makeRequestWithRetry<TValidated>(args: MakeRequestArgs<TValidated>): Promise<{
|
|
46
|
+
validatedData: TValidated;
|
|
47
|
+
retryCount: number;
|
|
48
|
+
httpStatus: number;
|
|
49
|
+
}>;
|
|
50
|
+
export declare function validateAndTransformResponse<T extends z.ZodType>(endpoint: ApiEndpoint<z.ZodType | undefined, z.ZodType | undefined, z.ZodType | undefined, z.ZodType | undefined, T>, response: Response, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): Promise<{
|
|
51
|
+
data: z.infer<T>;
|
|
52
|
+
responseSizeBytes: number;
|
|
53
|
+
}>;
|
|
54
|
+
export declare function defaultShouldRetry(ctx: RetryContext): boolean;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { ApiError } from './errors';
|
|
3
|
+
import type { ApiErrorContext, BaseApiUrl, HttpMethod, PathParams, QueryParams, RequestBodyFormat } from './shared';
|
|
4
|
+
export interface ApiEndpoint<TReqBody extends z.ZodType | undefined = z.ZodType | undefined, TReqParams extends z.ZodType | undefined = z.ZodType | undefined, TReqQuery extends z.ZodType | undefined = z.ZodType | undefined, TReqHeaders extends z.ZodType | undefined = z.ZodType | undefined, TResponse extends z.ZodType = z.ZodType> {
|
|
5
|
+
method: HttpMethod;
|
|
6
|
+
path: string | ((baseUrl: string) => string);
|
|
7
|
+
reqBodySchema?: TReqBody;
|
|
8
|
+
reqParamsSchema?: TReqParams;
|
|
9
|
+
reqQuerySchema?: TReqQuery;
|
|
10
|
+
reqHeadersSchema?: TReqHeaders;
|
|
11
|
+
resSchema: TResponse;
|
|
12
|
+
baseApiUrl?: string;
|
|
13
|
+
reqBodyFormat?: RequestBodyFormat;
|
|
14
|
+
defaultHeaders?: Record<string, string>;
|
|
15
|
+
resFormatter?: (data: unknown, headers: Record<string, string>) => z.infer<TResponse>;
|
|
16
|
+
reqDefaultQueryParams?: QueryParams;
|
|
17
|
+
doNotEncodeQueryParams?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface ApiWrapperResponse<T> {
|
|
20
|
+
data: T;
|
|
21
|
+
requestBodySizeMb: number;
|
|
22
|
+
responseSizeMb: number;
|
|
23
|
+
responseTimeMs: number;
|
|
24
|
+
httpStatus: number;
|
|
25
|
+
retryCount: number;
|
|
26
|
+
}
|
|
27
|
+
export type RetryContext = {
|
|
28
|
+
error?: unknown;
|
|
29
|
+
response?: Response;
|
|
30
|
+
retryCount: number;
|
|
31
|
+
maxRetries: number;
|
|
32
|
+
endpoint?: string;
|
|
33
|
+
method?: HttpMethod;
|
|
34
|
+
url?: string;
|
|
35
|
+
};
|
|
36
|
+
export type OnRetryContext = RetryContext & {
|
|
37
|
+
delayMs: number;
|
|
38
|
+
nextAttempt: number;
|
|
39
|
+
};
|
|
40
|
+
export type RequestInterceptorResult = {
|
|
41
|
+
headers?: Record<string, string>;
|
|
42
|
+
body?: unknown;
|
|
43
|
+
} | void | undefined;
|
|
44
|
+
export type OnRequestContext = {
|
|
45
|
+
endpoint: string;
|
|
46
|
+
method: HttpMethod;
|
|
47
|
+
url: string;
|
|
48
|
+
headers: Record<string, string>;
|
|
49
|
+
body: unknown;
|
|
50
|
+
meta?: Record<string, unknown>;
|
|
51
|
+
};
|
|
52
|
+
export type OnResponseContext = {
|
|
53
|
+
endpoint: string;
|
|
54
|
+
method: HttpMethod;
|
|
55
|
+
url: string;
|
|
56
|
+
response: Response;
|
|
57
|
+
attempt: number;
|
|
58
|
+
meta?: Record<string, unknown>;
|
|
59
|
+
};
|
|
60
|
+
export type ActionCache = {
|
|
61
|
+
<T>(key: string, fn: () => Promise<T> | T, options?: {
|
|
62
|
+
ttlMs?: number;
|
|
63
|
+
}): Promise<T>;
|
|
64
|
+
get<T>(key: string): T | undefined;
|
|
65
|
+
set<T>(key: string, value: T, options?: {
|
|
66
|
+
ttlMs?: number;
|
|
67
|
+
}): void;
|
|
68
|
+
invalidate(key: string): void;
|
|
69
|
+
clear(): void;
|
|
70
|
+
};
|
|
71
|
+
export type ActionInvokeOptions = {
|
|
72
|
+
signal?: AbortSignal;
|
|
73
|
+
meta?: Record<string, unknown>;
|
|
74
|
+
};
|
|
75
|
+
export type ActionContext<TInput, TApi = any> = {
|
|
76
|
+
input: TInput;
|
|
77
|
+
api: TApi;
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
meta?: Record<string, unknown>;
|
|
80
|
+
logger?: Pick<Console, 'debug' | 'error'>;
|
|
81
|
+
cache: ActionCache;
|
|
82
|
+
};
|
|
83
|
+
export interface ApiAction<TInput = any, TOutput = any> {
|
|
84
|
+
handler: (ctx: ActionContext<TInput>) => Promise<TOutput> | TOutput;
|
|
85
|
+
/** When true, the action accepts only optional `{ signal, meta }`; input is always `undefined`. */
|
|
86
|
+
readonly noRuntimeInput?: boolean;
|
|
87
|
+
}
|
|
88
|
+
type ActionDefinition<TInput, TOutput, TApi> = {
|
|
89
|
+
handler: (ctx: ActionContext<TInput, TApi>) => Promise<TOutput> | TOutput;
|
|
90
|
+
};
|
|
91
|
+
export type DefineAction<TApi> = {
|
|
92
|
+
<TOutput>(def: ActionDefinition<undefined, TOutput, TApi>): ApiAction<undefined, TOutput>;
|
|
93
|
+
<TInput>(): <TOutput>(def: ActionDefinition<TInput, TOutput, TApi>) => ApiAction<TInput, TOutput>;
|
|
94
|
+
};
|
|
95
|
+
type ActionInput<A> = A extends ApiAction<infer I, any> ? I : never;
|
|
96
|
+
export type ActionOutput<A> = A extends ApiAction<any, infer O> ? O : never;
|
|
97
|
+
export type ActionArgs<A> = [ActionInput<A>] extends [undefined] ? [options?: ActionInvokeOptions] : [input: ActionInput<A>, options?: ActionInvokeOptions];
|
|
98
|
+
type SchemaInputKeys<TEndpoint extends ApiEndpoint> = (TEndpoint['reqBodySchema'] extends z.ZodType ? 'reqBody' : never) | (TEndpoint['reqParamsSchema'] extends z.ZodType ? 'reqParams' : never) | (TEndpoint['reqQuerySchema'] extends z.ZodType ? 'reqQuery' : never) | (TEndpoint['reqHeadersSchema'] extends z.ZodType ? 'reqHeaders' : never);
|
|
99
|
+
type SchemaInputs<TEndpoint extends ApiEndpoint> = {
|
|
100
|
+
reqBody: TEndpoint['reqBodySchema'] extends z.ZodType ? z.infer<TEndpoint['reqBodySchema']> : unknown;
|
|
101
|
+
reqParams: TEndpoint['reqParamsSchema'] extends z.ZodType ? z.infer<TEndpoint['reqParamsSchema']> : PathParams;
|
|
102
|
+
reqQuery: TEndpoint['reqQuerySchema'] extends z.ZodType ? z.infer<TEndpoint['reqQuerySchema']> : QueryParams;
|
|
103
|
+
reqHeaders: TEndpoint['reqHeadersSchema'] extends z.ZodType ? z.infer<TEndpoint['reqHeadersSchema']> : Record<string, string>;
|
|
104
|
+
};
|
|
105
|
+
type CommonRequestOptions = {
|
|
106
|
+
url?: string;
|
|
107
|
+
signal?: AbortSignal;
|
|
108
|
+
timeoutMs?: number;
|
|
109
|
+
retries?: number;
|
|
110
|
+
fetch?: typeof fetch;
|
|
111
|
+
meta?: Record<string, unknown>;
|
|
112
|
+
};
|
|
113
|
+
export type RequestInput<TEndpoint extends ApiEndpoint> = Pick<SchemaInputs<TEndpoint>, SchemaInputKeys<TEndpoint>> & Partial<Omit<SchemaInputs<TEndpoint>, SchemaInputKeys<TEndpoint>>> & CommonRequestOptions;
|
|
114
|
+
type RequestArgs<TEndpoint extends ApiEndpoint> = SchemaInputKeys<TEndpoint> extends never ? [input?: RequestInput<TEndpoint>] : [input: RequestInput<TEndpoint>];
|
|
115
|
+
export type ApiClient<T extends Record<string, ApiEndpoint>, A extends Record<string, ApiAction> = Record<string, never>> = {
|
|
116
|
+
request: {
|
|
117
|
+
<K extends keyof T>(endpointKey: K, ...args: RequestArgs<T[K]>): Promise<ApiWrapperResponse<z.infer<T[K]['resSchema']>>>;
|
|
118
|
+
<K extends keyof A>(actionKey: K, ...args: ActionArgs<A[K]>): Promise<ActionOutput<A[K]>>;
|
|
119
|
+
};
|
|
120
|
+
cache: ActionCache;
|
|
121
|
+
} & {
|
|
122
|
+
[K in keyof T]: (...args: RequestArgs<T[K]>) => Promise<ApiWrapperResponse<z.infer<T[K]['resSchema']>>>;
|
|
123
|
+
} & {
|
|
124
|
+
[K in keyof A]: (...args: ActionArgs<A[K]>) => Promise<ActionOutput<A[K]>>;
|
|
125
|
+
};
|
|
126
|
+
export type ActionsFactoryHelpers<T extends Record<string, ApiEndpoint>, A extends Record<string, ApiAction> = Record<string, never>> = {
|
|
127
|
+
api: ApiClient<T, A>;
|
|
128
|
+
defineAction: DefineAction<ApiClient<T, A>>;
|
|
129
|
+
};
|
|
130
|
+
export interface ApiClientConfig<T extends Record<string, ApiEndpoint>, A extends Record<string, ApiAction> = Record<string, never>> {
|
|
131
|
+
name?: string;
|
|
132
|
+
baseApiUrls: Record<string, BaseApiUrl>;
|
|
133
|
+
defaultBaseApiUrl?: string;
|
|
134
|
+
endpoints: T;
|
|
135
|
+
defaultHeaders?: Record<string, string>;
|
|
136
|
+
timeoutMs?: number;
|
|
137
|
+
retries?: number;
|
|
138
|
+
retryDelayMs?: number | ((attempt: number) => number);
|
|
139
|
+
maxRetryDelayMs?: number;
|
|
140
|
+
shouldRetry?: (context: RetryContext) => boolean;
|
|
141
|
+
logger?: Pick<Console, 'debug' | 'error'>;
|
|
142
|
+
fetch?: typeof fetch;
|
|
143
|
+
onRequest?: (context: OnRequestContext) => RequestInterceptorResult | Promise<RequestInterceptorResult>;
|
|
144
|
+
onResponse?: (context: OnResponseContext) => void | Promise<void>;
|
|
145
|
+
onError?: (error: ApiError) => void | Promise<void>;
|
|
146
|
+
onRetry?: (context: OnRetryContext) => void | Promise<void>;
|
|
147
|
+
redact?: (context: ApiErrorContext) => ApiErrorContext;
|
|
148
|
+
actions?: A | ((helpers: ActionsFactoryHelpers<T, A>) => A);
|
|
149
|
+
}
|
|
150
|
+
export type ApiClientConfigNoActions<T extends Record<string, ApiEndpoint>> = Omit<ApiClientConfig<T, Record<string, never>>, 'actions'>;
|
|
151
|
+
export type ApiClientConfigWithActions<T extends Record<string, ApiEndpoint>, A extends Record<string, ApiAction>> = Omit<ApiClientConfig<T, Record<string, never>>, 'actions'> & {
|
|
152
|
+
actions: A | ((helpers: ActionsFactoryHelpers<T, A>) => A);
|
|
153
|
+
};
|
|
154
|
+
export type { BaseApiUrl, HttpMethod, PathParams, QueryParams, RequestBodyFormat, ApiErrorContext } from './shared';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ZodError } from 'zod';
|
|
2
|
+
export type FormattedZodIssue = {
|
|
3
|
+
path: string;
|
|
4
|
+
pathArray: (string | number)[];
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
expected?: string;
|
|
8
|
+
received?: string;
|
|
9
|
+
value?: unknown;
|
|
10
|
+
valuePreview?: string;
|
|
11
|
+
extra?: Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
export declare function formatZodIssues(error: ZodError, rawData: unknown): FormattedZodIssue[];
|
|
14
|
+
export declare function renderIssueSummary(issues: FormattedZodIssue[]): string;
|
|
15
|
+
export declare function previewValue(value: unknown, maxLen?: number): string;
|
|
16
|
+
export declare function makeJsonParseZodError(error: unknown, text: string): ZodError;
|
|
17
|
+
export declare function tryParseJson(body: string): unknown;
|
package/dist/cache/index.d.ts
CHANGED
|
@@ -3,6 +3,23 @@ type CachedFunction<T extends AnyFunction> = {
|
|
|
3
3
|
(...args: Parameters<T>): ReturnType<T>;
|
|
4
4
|
clearCache: () => void;
|
|
5
5
|
};
|
|
6
|
+
export type CacheStoreEntry = {
|
|
7
|
+
value: unknown;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Contract for external cache stores (e.g. a Redis adapter).
|
|
12
|
+
* Serialization is the adapter's responsibility; this library never imports
|
|
13
|
+
* the underlying client.
|
|
14
|
+
*/
|
|
15
|
+
export type CacheStore = {
|
|
16
|
+
get: (key: string) => Promise<CacheStoreEntry | undefined>;
|
|
17
|
+
/** `ttlMs` maps to the store's native expiry (e.g. Redis `PX`). */
|
|
18
|
+
set: (key: string, entry: CacheStoreEntry, ttlMs: number) => Promise<void>;
|
|
19
|
+
delete: (key: string) => Promise<void>;
|
|
20
|
+
/** Optional; called by clearCache when defined. */
|
|
21
|
+
clear?: () => Promise<void>;
|
|
22
|
+
};
|
|
6
23
|
export type CacheFunctionOptions<T extends AnyFunction> = {
|
|
7
24
|
name: string;
|
|
8
25
|
fn: T;
|
|
@@ -14,6 +31,15 @@ export type CacheFunctionOptions<T extends AnyFunction> = {
|
|
|
14
31
|
onBackgroundRefreshError?: (error: unknown, context: {
|
|
15
32
|
name: string;
|
|
16
33
|
}) => void;
|
|
34
|
+
/**
|
|
35
|
+
* External cache store (e.g. a Redis adapter). Only supported for async
|
|
36
|
+
* functions; when omitted the built-in in-memory cache is used.
|
|
37
|
+
*/
|
|
38
|
+
store?: ReturnType<T> extends Promise<unknown> ? CacheStore : never;
|
|
39
|
+
/** Called when a store get/set/delete/clear fails (default: silent). */
|
|
40
|
+
onStoreError?: (error: unknown, context: {
|
|
41
|
+
name: string;
|
|
42
|
+
}) => void;
|
|
17
43
|
};
|
|
18
44
|
export declare function cacheFunction<T extends AnyFunction>(options: CacheFunctionOptions<T>): CachedFunction<T>;
|
|
19
45
|
export {};
|