hanzio 1.2.0 → 2.1.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 +786 -239
- package/dist/api-wrapper/builders.d.ts +4 -3
- package/dist/api-wrapper/create-client.d.ts +1 -1
- package/dist/api-wrapper/describe.d.ts +16 -0
- package/dist/api-wrapper/endpoint.d.ts +18 -0
- package/dist/api-wrapper/errors.d.ts +39 -2
- package/dist/api-wrapper/index.d.ts +5 -0
- package/dist/api-wrapper/otel.d.ts +44 -0
- package/dist/api-wrapper/paginate.d.ts +12 -0
- package/dist/api-wrapper/redaction.d.ts +2 -0
- package/dist/api-wrapper/shared.d.ts +16 -2
- package/dist/api-wrapper/testing.d.ts +23 -0
- package/dist/api-wrapper/transport.d.ts +4 -5
- package/dist/api-wrapper/types.d.ts +103 -18
- package/dist/array/index.d.ts +50 -4
- package/dist/cache/createCache.d.ts +31 -0
- package/dist/cache/index.d.ts +19 -2
- package/dist/color/contrast.d.ts +40 -0
- package/dist/color/index.d.ts +2 -0
- package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
- package/dist/cool-console-log/index.d.ts +34 -5
- package/dist/crypto/encoding.d.ts +18 -0
- package/dist/crypto/hash.d.ts +33 -0
- package/dist/crypto/index.d.ts +4 -0
- package/dist/crypto/sealer.d.ts +25 -0
- package/dist/crypto/webhook.d.ts +20 -0
- package/dist/date/index.d.ts +46 -2
- package/dist/emitter/index.d.ts +21 -0
- package/dist/error/index.d.ts +12 -0
- package/dist/format/index.d.ts +102 -0
- package/dist/function/index.d.ts +60 -0
- package/dist/guard/index.d.ts +20 -0
- package/dist/i18n/index.d.ts +37 -0
- package/dist/index-0zvtcbzy.js +224 -0
- package/dist/index-8r2vdzjg.js +87 -0
- package/dist/index-hy7kbb99.js +157 -0
- package/dist/index-hyt6c56h.js +290 -0
- package/dist/{src/secrets/infisical.js → index-j1ybktbr.js} +500 -538
- package/dist/index-k9yx6bne.js +89 -0
- package/dist/index-kj1phgrx.js +169 -0
- package/dist/index-ktynw3vs.js +37 -0
- package/dist/index-mnkcfyx8.js +78 -0
- package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
- package/dist/index-rp5pqghz.js +157 -0
- package/dist/index-w882zry3.js +29 -0
- package/dist/index.d.ts +7 -7
- package/dist/jwt/index.d.ts +50 -2
- package/dist/math/index.d.ts +2 -2
- package/dist/math/linearRegressionTrend.d.ts +14 -1
- package/dist/math/scale.d.ts +8 -0
- package/dist/object/index.d.ts +19 -0
- package/dist/p-queue/index.d.ts +1 -1
- package/dist/promise/index.d.ts +97 -2
- package/dist/secrets/core.d.ts +7 -0
- package/dist/secrets/index.d.ts +3 -1
- package/dist/secrets/public-config.d.ts +7 -0
- package/dist/secrets/vite.d.ts +1 -0
- package/dist/secrets/worker.d.ts +1 -0
- package/dist/sitemap/index.d.ts +48 -6
- package/dist/src/api-wrapper/index.js +60 -1130
- package/dist/src/api-wrapper/testing.js +20 -0
- package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
- package/dist/src/crypto/index.js +195 -0
- package/dist/src/format/index.js +211 -0
- package/dist/src/i18n/index.js +36 -0
- package/dist/src/index.js +739 -3145
- package/dist/src/jwt/index.js +138 -92
- package/dist/src/p-queue/index.js +9 -437
- package/dist/src/secrets/index.js +50 -1428
- package/dist/src/secrets/vite.js +10 -39
- package/dist/src/secrets/worker.js +14 -273
- package/dist/src/sitemap/index.js +183 -511
- package/dist/src/storage/index.js +119 -0
- package/dist/src/zod/index.js +340 -167
- package/dist/state/index.d.ts +21 -0
- package/dist/storage/index.d.ts +58 -0
- package/dist/string/index.d.ts +75 -2
- package/dist/typedswitch/index.d.ts +31 -24
- package/dist/types/index.d.ts +11 -0
- package/dist/url/index.d.ts +50 -1
- package/dist/zod/index.d.ts +32 -3
- package/dist/zod/introspect.d.ts +19 -0
- package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
- package/dist/zod/zodMinMaxFilter.d.ts +22 -3
- package/dist/zod/zodToTypeString.d.ts +8 -3
- package/package.json +78 -7
- package/src/api-wrapper/actions-impl.ts +11 -72
- package/src/api-wrapper/builders.ts +32 -25
- package/src/api-wrapper/create-client.ts +171 -41
- package/src/api-wrapper/describe.ts +157 -0
- package/src/api-wrapper/endpoint.ts +26 -0
- package/src/api-wrapper/error-reporting.ts +7 -1
- package/src/api-wrapper/errors.ts +107 -2
- package/src/api-wrapper/headers.ts +11 -6
- package/src/api-wrapper/index.ts +5 -0
- package/src/api-wrapper/otel.ts +139 -0
- package/src/api-wrapper/paginate.ts +28 -0
- package/src/api-wrapper/redaction.ts +42 -0
- package/src/api-wrapper/shared.ts +21 -2
- package/src/api-wrapper/testing.ts +48 -0
- package/src/api-wrapper/transport.ts +41 -43
- package/src/api-wrapper/types.ts +185 -18
- package/src/array/index.ts +195 -42
- package/src/cache/createCache.ts +157 -0
- package/src/cache/index.ts +132 -147
- package/src/color/contrast.ts +163 -0
- package/src/color/index.ts +22 -0
- package/src/{math → color}/metricsSeriesColors.ts +47 -49
- package/src/cool-console-log/index.ts +128 -41
- package/src/crypto/encoding.ts +83 -0
- package/src/crypto/hash.ts +170 -0
- package/src/crypto/index.ts +26 -0
- package/src/crypto/sealer.ts +168 -0
- package/src/crypto/webhook.ts +86 -0
- package/src/date/index.ts +109 -34
- package/src/emitter/index.ts +82 -0
- package/src/error/index.ts +63 -0
- package/src/format/index.ts +389 -0
- package/src/function/index.ts +201 -0
- package/src/guard/index.ts +80 -0
- package/src/i18n/index.ts +125 -0
- package/src/index.ts +11 -7
- package/src/jwt/index.ts +199 -125
- package/src/math/index.ts +5 -10
- package/src/math/linearRegressionTrend.ts +33 -8
- package/src/math/scale.ts +17 -0
- package/src/object/index.ts +99 -0
- package/src/p-queue/index.ts +2 -0
- package/src/promise/index.ts +304 -9
- package/src/secrets/core.ts +72 -16
- package/src/secrets/index.ts +16 -4
- package/src/secrets/public-config.ts +47 -0
- package/src/secrets/vite.ts +2 -0
- package/src/secrets/worker.ts +2 -0
- package/src/sitemap/index.ts +293 -130
- package/src/state/index.ts +133 -81
- package/src/storage/index.ts +187 -0
- package/src/string/index.ts +261 -23
- package/src/typedswitch/index.ts +58 -38
- package/src/types/index.ts +14 -0
- package/src/url/index.ts +146 -1
- package/src/zod/index.ts +105 -9
- package/src/zod/introspect.ts +103 -0
- package/src/zod/jsonToZodSchemaMapper.ts +44 -21
- package/src/zod/zodMinMaxFilter.ts +37 -13
- package/src/zod/zodToTypeString.ts +184 -181
- package/dist/api-wrapper/index.js +0 -250
- package/dist/gzip/index.d.ts +0 -2
- package/dist/gzip/index.js +0 -16
- package/dist/index.js +0 -1823
- package/dist/jwt/index.js +0 -19
- package/dist/sitemap/index.js +0 -552
- package/dist/src/gzip/index.js +0 -16
- package/dist/src/secrets/loaders.js +0 -1529
- package/dist/zod/index.js +0 -210
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { ZodError, type z } from 'zod'
|
|
2
|
+
import { anySignal, promiseTimeout } from '../promise'
|
|
3
|
+
import { parseRetryAfter } from '../url'
|
|
2
4
|
import type {
|
|
3
5
|
ApiEndpoint,
|
|
6
|
+
FetchLike,
|
|
4
7
|
OnResponseContext,
|
|
5
8
|
OnRetryContext,
|
|
6
9
|
RetryContext
|
|
7
10
|
} from './types'
|
|
8
|
-
import type { ApiErrorContext, HttpMethod } from './shared'
|
|
11
|
+
import type { ApiErrorContext, HttpMethod, RequestMeta } from './shared'
|
|
9
12
|
import {
|
|
13
|
+
ApiResponseError,
|
|
10
14
|
ConfigError,
|
|
11
15
|
HttpResponseError,
|
|
12
16
|
isNonRetryableApiError,
|
|
@@ -20,6 +24,7 @@ import {
|
|
|
20
24
|
getHeadersAsObject,
|
|
21
25
|
hasJsonContentType
|
|
22
26
|
} from './headers'
|
|
27
|
+
import { redactUrl } from './redaction'
|
|
23
28
|
import { makeJsonParseZodError } from './zod-issues'
|
|
24
29
|
|
|
25
30
|
export type RequestConfig = {
|
|
@@ -28,7 +33,7 @@ export type RequestConfig = {
|
|
|
28
33
|
body: unknown
|
|
29
34
|
headers: Record<string, string>
|
|
30
35
|
timeoutMs?: number
|
|
31
|
-
fetchFn:
|
|
36
|
+
fetchFn: FetchLike
|
|
32
37
|
userSignal?: AbortSignal
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -57,7 +62,7 @@ export async function httpRequest(
|
|
|
57
62
|
? undefined
|
|
58
63
|
: setTimeout(() => timeoutController.abort(), config.timeoutMs)
|
|
59
64
|
|
|
60
|
-
const signal =
|
|
65
|
+
const signal = anySignal(timeoutController.signal, config.userSignal)
|
|
61
66
|
|
|
62
67
|
try {
|
|
63
68
|
return await config.fetchFn(config.fullUrl, {
|
|
@@ -100,27 +105,6 @@ function isDomAbortError(error: unknown): boolean {
|
|
|
100
105
|
)
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
export function mergeSignals(
|
|
104
|
-
a: AbortSignal,
|
|
105
|
-
b: AbortSignal | undefined
|
|
106
|
-
): AbortSignal {
|
|
107
|
-
if (!b) return a
|
|
108
|
-
const anyFn = (
|
|
109
|
-
AbortSignal as unknown as {
|
|
110
|
-
any?: (signals: AbortSignal[]) => AbortSignal
|
|
111
|
-
}
|
|
112
|
-
).any
|
|
113
|
-
if (typeof anyFn === 'function') return anyFn([a, b])
|
|
114
|
-
|
|
115
|
-
const controller = new AbortController()
|
|
116
|
-
const onAbort = () => controller.abort()
|
|
117
|
-
if (a.aborted) controller.abort()
|
|
118
|
-
else a.addEventListener('abort', onAbort, { once: true })
|
|
119
|
-
if (b.aborted) controller.abort()
|
|
120
|
-
else b.addEventListener('abort', onAbort, { once: true })
|
|
121
|
-
return controller.signal
|
|
122
|
-
}
|
|
123
|
-
|
|
124
108
|
export async function readErrorResponse(response: Response): Promise<{
|
|
125
109
|
body: string
|
|
126
110
|
headers: Record<string, string>
|
|
@@ -143,17 +127,8 @@ export function getRetryDelay(params: {
|
|
|
143
127
|
const maxDelay = params.maxRetryDelayMs ?? 30_000
|
|
144
128
|
const { retryDelay, attempt, response } = params
|
|
145
129
|
if (response && (response.status === 429 || response.status === 503)) {
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
148
|
-
const seconds = Number(retryAfter)
|
|
149
|
-
if (Number.isFinite(seconds)) {
|
|
150
|
-
return Math.min(Math.max(0, seconds * 1000), maxDelay)
|
|
151
|
-
}
|
|
152
|
-
const dateMs = Date.parse(retryAfter)
|
|
153
|
-
if (!Number.isNaN(dateMs)) {
|
|
154
|
-
return Math.min(Math.max(0, dateMs - Date.now()), maxDelay)
|
|
155
|
-
}
|
|
156
|
-
}
|
|
130
|
+
const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'))
|
|
131
|
+
if (retryAfterMs !== undefined) return Math.min(retryAfterMs, maxDelay)
|
|
157
132
|
}
|
|
158
133
|
const delayMs =
|
|
159
134
|
typeof retryDelay === 'function' ? retryDelay(attempt) : retryDelay
|
|
@@ -182,7 +157,7 @@ type MakeRequestArgs<TValidated> = {
|
|
|
182
157
|
endpointName: string
|
|
183
158
|
method: HttpMethod
|
|
184
159
|
fullUrl: string
|
|
185
|
-
meta?:
|
|
160
|
+
meta?: RequestMeta
|
|
186
161
|
onResponse?: (context: OnResponseContext) => void | Promise<void>
|
|
187
162
|
onRetry?: (context: OnRetryContext) => void | Promise<void>
|
|
188
163
|
}
|
|
@@ -215,7 +190,13 @@ export async function makeRequestWithRetry<TValidated>(
|
|
|
215
190
|
while (true) {
|
|
216
191
|
let response: Response | undefined
|
|
217
192
|
try {
|
|
218
|
-
|
|
193
|
+
// Headers and body stay out of debug logs; they may carry credentials.
|
|
194
|
+
logger?.debug?.('API request', {
|
|
195
|
+
endpoint: endpointName,
|
|
196
|
+
method,
|
|
197
|
+
url: redactUrl(fullUrl),
|
|
198
|
+
attempt: retryCount
|
|
199
|
+
})
|
|
219
200
|
response = await httpRequest(config, (over) =>
|
|
220
201
|
ctx({ attempt: retryCount, ...over })
|
|
221
202
|
)
|
|
@@ -250,8 +231,10 @@ export async function makeRequestWithRetry<TValidated>(
|
|
|
250
231
|
delayMs,
|
|
251
232
|
nextAttempt: retryCount + 1
|
|
252
233
|
})
|
|
234
|
+
await sleepBeforeRetry(delayMs, config.userSignal, () =>
|
|
235
|
+
ctx({ attempt: retryCount })
|
|
236
|
+
)
|
|
253
237
|
retryCount++
|
|
254
|
-
await delay(delayMs)
|
|
255
238
|
continue
|
|
256
239
|
}
|
|
257
240
|
|
|
@@ -271,7 +254,11 @@ export async function makeRequestWithRetry<TValidated>(
|
|
|
271
254
|
httpStatus: response.status
|
|
272
255
|
}
|
|
273
256
|
} catch (error) {
|
|
274
|
-
|
|
257
|
+
// ApiResponseError is not retried by default, but a custom shouldRetry may opt in.
|
|
258
|
+
if (
|
|
259
|
+
isNonRetryableApiError(error) &&
|
|
260
|
+
!(error instanceof ApiResponseError)
|
|
261
|
+
) {
|
|
275
262
|
throw error
|
|
276
263
|
}
|
|
277
264
|
|
|
@@ -298,8 +285,10 @@ export async function makeRequestWithRetry<TValidated>(
|
|
|
298
285
|
delayMs,
|
|
299
286
|
nextAttempt: retryCount + 1
|
|
300
287
|
})
|
|
288
|
+
await sleepBeforeRetry(delayMs, config.userSignal, () =>
|
|
289
|
+
ctx({ attempt: retryCount })
|
|
290
|
+
)
|
|
301
291
|
retryCount++
|
|
302
|
-
await delay(delayMs)
|
|
303
292
|
}
|
|
304
293
|
}
|
|
305
294
|
}
|
|
@@ -326,7 +315,8 @@ export async function validateAndTransformResponse<T extends z.ZodType>(
|
|
|
326
315
|
throw new ResponseValidationError(
|
|
327
316
|
makeJsonParseZodError(error, text),
|
|
328
317
|
text,
|
|
329
|
-
ctx()
|
|
318
|
+
ctx(),
|
|
319
|
+
{ invalidJson: true }
|
|
330
320
|
)
|
|
331
321
|
}
|
|
332
322
|
} else {
|
|
@@ -345,6 +335,14 @@ export function defaultShouldRetry(ctx: RetryContext): boolean {
|
|
|
345
335
|
return shouldAttemptRetryOnHttp(ctx)
|
|
346
336
|
}
|
|
347
337
|
|
|
348
|
-
function
|
|
349
|
-
|
|
338
|
+
async function sleepBeforeRetry(
|
|
339
|
+
ms: number,
|
|
340
|
+
signal: AbortSignal | undefined,
|
|
341
|
+
ctx: () => ApiErrorContext
|
|
342
|
+
): Promise<void> {
|
|
343
|
+
try {
|
|
344
|
+
await promiseTimeout(ms, { signal })
|
|
345
|
+
} catch (error) {
|
|
346
|
+
throw new RequestAbortedError(ctx(), error)
|
|
347
|
+
}
|
|
350
348
|
}
|
package/src/api-wrapper/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { z } from 'zod'
|
|
2
|
+
import type { Result } from '../promise'
|
|
2
3
|
import type { ApiError } from './errors'
|
|
3
4
|
import type {
|
|
4
5
|
ApiErrorContext,
|
|
@@ -6,7 +7,8 @@ import type {
|
|
|
6
7
|
HttpMethod,
|
|
7
8
|
PathParams,
|
|
8
9
|
QueryParams,
|
|
9
|
-
RequestBodyFormat
|
|
10
|
+
RequestBodyFormat,
|
|
11
|
+
RequestMeta
|
|
10
12
|
} from './shared'
|
|
11
13
|
|
|
12
14
|
export interface ApiEndpoint<
|
|
@@ -26,10 +28,11 @@ export interface ApiEndpoint<
|
|
|
26
28
|
baseApiUrl?: string
|
|
27
29
|
reqBodyFormat?: RequestBodyFormat
|
|
28
30
|
defaultHeaders?: Record<string, string>
|
|
31
|
+
/** Reshapes the raw body before `resSchema` parses it, so it returns the schema's input type. */
|
|
29
32
|
resFormatter?: (
|
|
30
33
|
data: unknown,
|
|
31
34
|
headers: Record<string, string>
|
|
32
|
-
) => z.
|
|
35
|
+
) => z.input<TResponse>
|
|
33
36
|
reqDefaultQueryParams?: QueryParams
|
|
34
37
|
doNotEncodeQueryParams?: boolean
|
|
35
38
|
}
|
|
@@ -72,7 +75,7 @@ export type OnRequestContext = {
|
|
|
72
75
|
url: string
|
|
73
76
|
headers: Record<string, string>
|
|
74
77
|
body: unknown
|
|
75
|
-
meta?:
|
|
78
|
+
meta?: RequestMeta
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
export type OnResponseContext = {
|
|
@@ -81,7 +84,7 @@ export type OnResponseContext = {
|
|
|
81
84
|
url: string
|
|
82
85
|
response: Response
|
|
83
86
|
attempt: number
|
|
84
|
-
meta?:
|
|
87
|
+
meta?: RequestMeta
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
export type ActionCache = {
|
|
@@ -98,7 +101,7 @@ export type ActionCache = {
|
|
|
98
101
|
|
|
99
102
|
export type ActionInvokeOptions = {
|
|
100
103
|
signal?: AbortSignal
|
|
101
|
-
meta?:
|
|
104
|
+
meta?: RequestMeta
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
// biome-ignore lint/suspicious/noExplicitAny: TApi defaults to any so plain ApiAction stays open
|
|
@@ -106,7 +109,7 @@ export type ActionContext<TInput, TApi = any> = {
|
|
|
106
109
|
input: TInput
|
|
107
110
|
api: TApi
|
|
108
111
|
signal?: AbortSignal
|
|
109
|
-
meta?:
|
|
112
|
+
meta?: RequestMeta
|
|
110
113
|
logger?: Pick<Console, 'debug' | 'error'>
|
|
111
114
|
cache: ActionCache
|
|
112
115
|
}
|
|
@@ -114,6 +117,8 @@ export type ActionContext<TInput, TApi = any> = {
|
|
|
114
117
|
// biome-ignore lint/suspicious/noExplicitAny: variance-friendly bounds
|
|
115
118
|
export interface ApiAction<TInput = any, TOutput = any> {
|
|
116
119
|
handler: (ctx: ActionContext<TInput>) => Promise<TOutput> | TOutput
|
|
120
|
+
/** Validates the caller's input at runtime; `handler` receives the parsed output. */
|
|
121
|
+
readonly input?: z.ZodType
|
|
117
122
|
/** When true, the action accepts only optional `{ signal, meta }`; input is always `undefined`. */
|
|
118
123
|
readonly noRuntimeInput?: boolean
|
|
119
124
|
}
|
|
@@ -122,7 +127,18 @@ type ActionDefinition<TInput, TOutput, TApi> = {
|
|
|
122
127
|
handler: (ctx: ActionContext<TInput, TApi>) => Promise<TOutput> | TOutput
|
|
123
128
|
}
|
|
124
129
|
|
|
130
|
+
type SchemaActionDefinition<TSchema extends z.ZodType, TOutput, TApi> = {
|
|
131
|
+
input: TSchema
|
|
132
|
+
handler: (
|
|
133
|
+
ctx: ActionContext<z.output<TSchema>, TApi>
|
|
134
|
+
) => Promise<TOutput> | TOutput
|
|
135
|
+
}
|
|
136
|
+
|
|
125
137
|
export type DefineAction<TApi> = {
|
|
138
|
+
/** Input validated by a Zod schema: callers pass `z.input`, the handler gets `z.output`. */
|
|
139
|
+
<TSchema extends z.ZodType, TOutput>(
|
|
140
|
+
def: SchemaActionDefinition<TSchema, TOutput, TApi>
|
|
141
|
+
): ApiAction<z.input<TSchema>, TOutput>
|
|
126
142
|
<TOutput>(
|
|
127
143
|
def: ActionDefinition<undefined, TOutput, TApi>
|
|
128
144
|
): ApiAction<undefined, TOutput>
|
|
@@ -140,24 +156,68 @@ export type ActionArgs<A> = [ActionInput<A>] extends [undefined]
|
|
|
140
156
|
? [options?: ActionInvokeOptions]
|
|
141
157
|
: [input: ActionInput<A>, options?: ActionInvokeOptions]
|
|
142
158
|
|
|
159
|
+
// Tail-recursive (accumulator) to stay under TypeScript's recursion limit.
|
|
160
|
+
type CharsOf<
|
|
161
|
+
S extends string,
|
|
162
|
+
Acc extends string = never
|
|
163
|
+
> = S extends `${infer C}${infer Rest}` ? CharsOf<Rest, Acc | C> : Acc
|
|
164
|
+
type IdentStart =
|
|
165
|
+
CharsOf<'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'>
|
|
166
|
+
type WordChar = IdentStart | CharsOf<'0123456789'>
|
|
167
|
+
type TakeWord<
|
|
168
|
+
S extends string,
|
|
169
|
+
Acc extends string = ''
|
|
170
|
+
> = S extends `${infer C}${infer Rest}`
|
|
171
|
+
? C extends WordChar
|
|
172
|
+
? TakeWord<Rest, `${Acc}${C}`>
|
|
173
|
+
: Acc
|
|
174
|
+
: Acc
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* `:name` placeholders in a literal path, matching the runtime rule (a letter
|
|
178
|
+
* or `_`, then word characters, so ports like `:8080` don't count).
|
|
179
|
+
* `never` for non-literal paths.
|
|
180
|
+
*/
|
|
181
|
+
export type PathParamNames<TPath> = TPath extends string
|
|
182
|
+
? string extends TPath
|
|
183
|
+
? never
|
|
184
|
+
: TPath extends `${string}:${infer Rest}`
|
|
185
|
+
?
|
|
186
|
+
| (Rest extends `${IdentStart}${string}` ? TakeWord<Rest> : never)
|
|
187
|
+
| PathParamNames<Rest>
|
|
188
|
+
: never
|
|
189
|
+
: never
|
|
190
|
+
|
|
191
|
+
type EndpointPathParamNames<TEndpoint extends ApiEndpoint> = PathParamNames<
|
|
192
|
+
TEndpoint['path']
|
|
193
|
+
>
|
|
194
|
+
|
|
143
195
|
type SchemaInputKeys<TEndpoint extends ApiEndpoint> =
|
|
144
196
|
| (TEndpoint['reqBodySchema'] extends z.ZodType ? 'reqBody' : never)
|
|
145
|
-
| (TEndpoint['reqParamsSchema'] extends z.ZodType ? 'reqParams' : never)
|
|
146
197
|
| (TEndpoint['reqQuerySchema'] extends z.ZodType ? 'reqQuery' : never)
|
|
147
198
|
| (TEndpoint['reqHeadersSchema'] extends z.ZodType ? 'reqHeaders' : never)
|
|
199
|
+
| (TEndpoint['reqParamsSchema'] extends z.ZodType
|
|
200
|
+
? 'reqParams'
|
|
201
|
+
: [EndpointPathParamNames<TEndpoint>] extends [never]
|
|
202
|
+
? never
|
|
203
|
+
: 'reqParams')
|
|
148
204
|
|
|
205
|
+
// Callers pass what the schemas accept (`z.input`): fields with `.default()`
|
|
206
|
+
// are optional and transforms take their pre-transform type.
|
|
149
207
|
type SchemaInputs<TEndpoint extends ApiEndpoint> = {
|
|
150
208
|
reqBody: TEndpoint['reqBodySchema'] extends z.ZodType
|
|
151
|
-
? z.
|
|
209
|
+
? z.input<TEndpoint['reqBodySchema']>
|
|
152
210
|
: unknown
|
|
153
211
|
reqParams: TEndpoint['reqParamsSchema'] extends z.ZodType
|
|
154
|
-
? z.
|
|
155
|
-
:
|
|
212
|
+
? z.input<TEndpoint['reqParamsSchema']>
|
|
213
|
+
: [EndpointPathParamNames<TEndpoint>] extends [never]
|
|
214
|
+
? PathParams
|
|
215
|
+
: Record<EndpointPathParamNames<TEndpoint>, string | number>
|
|
156
216
|
reqQuery: TEndpoint['reqQuerySchema'] extends z.ZodType
|
|
157
|
-
? z.
|
|
217
|
+
? z.input<TEndpoint['reqQuerySchema']>
|
|
158
218
|
: QueryParams
|
|
159
219
|
reqHeaders: TEndpoint['reqHeadersSchema'] extends z.ZodType
|
|
160
|
-
? z.
|
|
220
|
+
? z.input<TEndpoint['reqHeadersSchema']>
|
|
161
221
|
: Record<string, string>
|
|
162
222
|
}
|
|
163
223
|
|
|
@@ -166,8 +226,8 @@ type CommonRequestOptions = {
|
|
|
166
226
|
signal?: AbortSignal
|
|
167
227
|
timeoutMs?: number
|
|
168
228
|
retries?: number
|
|
169
|
-
fetch?:
|
|
170
|
-
meta?:
|
|
229
|
+
fetch?: FetchLike
|
|
230
|
+
meta?: RequestMeta
|
|
171
231
|
}
|
|
172
232
|
|
|
173
233
|
export type RequestInput<TEndpoint extends ApiEndpoint> = Pick<
|
|
@@ -190,21 +250,72 @@ export type ApiClient<
|
|
|
190
250
|
<K extends keyof T>(
|
|
191
251
|
endpointKey: K,
|
|
192
252
|
...args: RequestArgs<T[K]>
|
|
193
|
-
): Promise<ApiWrapperResponse<
|
|
253
|
+
): Promise<ApiWrapperResponse<EndpointResponse<T[K]>>>
|
|
194
254
|
<K extends keyof A>(
|
|
195
255
|
actionKey: K,
|
|
196
256
|
...args: ActionArgs<A[K]>
|
|
197
257
|
): Promise<ActionOutput<A[K]>>
|
|
198
258
|
}
|
|
199
259
|
cache: ActionCache
|
|
260
|
+
/**
|
|
261
|
+
* The same endpoints and actions, resolving to `Result` instead of throwing
|
|
262
|
+
* `ApiError`s. Other errors (bugs in hooks or middleware) still throw.
|
|
263
|
+
*/
|
|
264
|
+
safe: {
|
|
265
|
+
[K in keyof T]: (
|
|
266
|
+
...args: RequestArgs<T[K]>
|
|
267
|
+
) => Promise<Result<ApiWrapperResponse<EndpointResponse<T[K]>>, ApiError>>
|
|
268
|
+
} & {
|
|
269
|
+
[K in keyof A]: (
|
|
270
|
+
...args: ActionArgs<A[K]>
|
|
271
|
+
) => Promise<Result<ActionOutput<A[K]>, ApiError>>
|
|
272
|
+
}
|
|
200
273
|
} & {
|
|
201
274
|
[K in keyof T]: (
|
|
202
275
|
...args: RequestArgs<T[K]>
|
|
203
|
-
) => Promise<ApiWrapperResponse<
|
|
276
|
+
) => Promise<ApiWrapperResponse<EndpointResponse<T[K]>>>
|
|
204
277
|
} & {
|
|
205
278
|
[K in keyof A]: (...args: ActionArgs<A[K]>) => Promise<ActionOutput<A[K]>>
|
|
206
279
|
}
|
|
207
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Any client, for helpers that accept whichever client they're given. Call
|
|
283
|
+
* endpoints through `request` or `safe`; results are `unknown`.
|
|
284
|
+
*/
|
|
285
|
+
export type AnyApiClient = {
|
|
286
|
+
// Method syntax keeps the key parameter bivariant, so any client fits.
|
|
287
|
+
// biome-ignore lint/suspicious/noExplicitAny: accepts every client's argument shapes
|
|
288
|
+
request(key: string, ...args: any[]): Promise<unknown>
|
|
289
|
+
cache: ActionCache
|
|
290
|
+
// biome-ignore lint/suspicious/noExplicitAny: accepts every client's argument shapes
|
|
291
|
+
safe: Record<string, (...args: any[]) => Promise<Result<unknown, ApiError>>>
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Parsed response data of an endpoint definition (`z.output` of `resSchema`). */
|
|
295
|
+
export type EndpointResponse<TEndpoint extends ApiEndpoint> = z.output<
|
|
296
|
+
TEndpoint['resSchema']
|
|
297
|
+
>
|
|
298
|
+
|
|
299
|
+
/** Request input for an endpoint definition (`reqBody`, `reqParams`, …). */
|
|
300
|
+
export type EndpointRequest<TEndpoint extends ApiEndpoint> =
|
|
301
|
+
RequestInput<TEndpoint>
|
|
302
|
+
|
|
303
|
+
/** Response data of a client method: `InferResponse<typeof api, 'usersGet'>`. */
|
|
304
|
+
export type InferResponse<
|
|
305
|
+
TClient,
|
|
306
|
+
K extends keyof TClient
|
|
307
|
+
> = TClient[K] extends (...args: never[]) => Promise<infer R>
|
|
308
|
+
? R extends ApiWrapperResponse<infer D>
|
|
309
|
+
? D
|
|
310
|
+
: R
|
|
311
|
+
: never
|
|
312
|
+
|
|
313
|
+
/** First argument of a client method: `InferRequest<typeof api, 'usersGet'>`. */
|
|
314
|
+
export type InferRequest<
|
|
315
|
+
TClient,
|
|
316
|
+
K extends keyof TClient
|
|
317
|
+
> = TClient[K] extends (...args: infer P) => unknown ? NonNullable<P[0]> : never
|
|
318
|
+
|
|
208
319
|
export type ActionsFactoryHelpers<
|
|
209
320
|
T extends Record<string, ApiEndpoint>,
|
|
210
321
|
A extends Record<string, ApiAction> = Record<string, never>
|
|
@@ -213,6 +324,48 @@ export type ActionsFactoryHelpers<
|
|
|
213
324
|
defineAction: DefineAction<ApiClient<T, A>>
|
|
214
325
|
}
|
|
215
326
|
|
|
327
|
+
/** Any fetch-compatible function; avoids Bun's `typeof fetch` extras like `preconnect`. */
|
|
328
|
+
export type FetchLike = (
|
|
329
|
+
input: string | URL | Request,
|
|
330
|
+
init?: RequestInit
|
|
331
|
+
) => Promise<Response>
|
|
332
|
+
|
|
333
|
+
export type ApiMiddlewareContext = {
|
|
334
|
+
/** The client's `name`, when set. */
|
|
335
|
+
client?: string
|
|
336
|
+
/** The endpoint key, e.g. `usersGet`. */
|
|
337
|
+
endpoint: string
|
|
338
|
+
method: HttpMethod
|
|
339
|
+
/** Full URL including the query string. */
|
|
340
|
+
url: string
|
|
341
|
+
/** Outgoing headers. Mutate before `next()` to add headers (e.g. `traceparent`). */
|
|
342
|
+
headers: Record<string, string>
|
|
343
|
+
meta?: RequestMeta
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Wraps one logical request: `onRequest`, every retry attempt, validation and
|
|
348
|
+
* `checkResponse`. Call `next()` to continue; errors it throws are `ApiError`s
|
|
349
|
+
* that were already redacted and passed to `onError`.
|
|
350
|
+
*/
|
|
351
|
+
export type ApiMiddleware = (
|
|
352
|
+
context: ApiMiddlewareContext,
|
|
353
|
+
next: () => Promise<ApiWrapperResponse<unknown>>
|
|
354
|
+
) => Promise<ApiWrapperResponse<unknown>>
|
|
355
|
+
|
|
356
|
+
export type CheckResponseContext = {
|
|
357
|
+
endpoint: string
|
|
358
|
+
method: HttpMethod
|
|
359
|
+
url: string
|
|
360
|
+
httpStatus: number
|
|
361
|
+
attempt: number
|
|
362
|
+
meta?: RequestMeta
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export type DefaultHeaders =
|
|
366
|
+
| Record<string, string>
|
|
367
|
+
| (() => Record<string, string> | Promise<Record<string, string>>)
|
|
368
|
+
|
|
216
369
|
export interface ApiClientConfig<
|
|
217
370
|
T extends Record<string, ApiEndpoint>,
|
|
218
371
|
A extends Record<string, ApiAction> = Record<string, never>
|
|
@@ -221,14 +374,26 @@ export interface ApiClientConfig<
|
|
|
221
374
|
baseApiUrls: Record<string, BaseApiUrl>
|
|
222
375
|
defaultBaseApiUrl?: string
|
|
223
376
|
endpoints: T
|
|
224
|
-
|
|
377
|
+
/** Static headers, or a function resolved on every request (e.g. auth from a secret store). */
|
|
378
|
+
defaultHeaders?: DefaultHeaders
|
|
225
379
|
timeoutMs?: number
|
|
226
380
|
retries?: number
|
|
227
381
|
retryDelayMs?: number | ((attempt: number) => number)
|
|
228
382
|
maxRetryDelayMs?: number
|
|
229
383
|
shouldRetry?: (context: RetryContext) => boolean
|
|
230
384
|
logger?: Pick<Console, 'debug' | 'error'>
|
|
231
|
-
fetch?:
|
|
385
|
+
fetch?: FetchLike
|
|
386
|
+
/** Middleware around each whole request, in array order (first is outermost). */
|
|
387
|
+
use?: ApiMiddleware[]
|
|
388
|
+
/**
|
|
389
|
+
* Runs after `resSchema` validation on every attempt. Throw to reject the
|
|
390
|
+
* response; non-`ApiError` throws become `ApiResponseError`, which the
|
|
391
|
+
* default retry policy does not retry.
|
|
392
|
+
*/
|
|
393
|
+
checkResponse?: (
|
|
394
|
+
data: unknown,
|
|
395
|
+
context: CheckResponseContext
|
|
396
|
+
) => void | Promise<void>
|
|
232
397
|
onRequest?: (
|
|
233
398
|
context: OnRequestContext
|
|
234
399
|
) => RequestInterceptorResult | Promise<RequestInterceptorResult>
|
|
@@ -250,6 +415,8 @@ export type ApiClientConfigWithActions<
|
|
|
250
415
|
}
|
|
251
416
|
|
|
252
417
|
export type {
|
|
418
|
+
ApiRequestMeta,
|
|
419
|
+
RequestMeta,
|
|
253
420
|
BaseApiUrl,
|
|
254
421
|
HttpMethod,
|
|
255
422
|
PathParams,
|