hanzio 1.2.0 → 2.0.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.
Files changed (145) hide show
  1. package/README.md +786 -239
  2. package/dist/api-wrapper/builders.d.ts +4 -3
  3. package/dist/api-wrapper/create-client.d.ts +1 -1
  4. package/dist/api-wrapper/describe.d.ts +16 -0
  5. package/dist/api-wrapper/endpoint.d.ts +18 -0
  6. package/dist/api-wrapper/errors.d.ts +39 -2
  7. package/dist/api-wrapper/index.d.ts +5 -0
  8. package/dist/api-wrapper/otel.d.ts +44 -0
  9. package/dist/api-wrapper/paginate.d.ts +12 -0
  10. package/dist/api-wrapper/redaction.d.ts +2 -0
  11. package/dist/api-wrapper/shared.d.ts +16 -2
  12. package/dist/api-wrapper/testing.d.ts +23 -0
  13. package/dist/api-wrapper/transport.d.ts +4 -5
  14. package/dist/api-wrapper/types.d.ts +103 -18
  15. package/dist/array/index.d.ts +50 -4
  16. package/dist/cache/createCache.d.ts +31 -0
  17. package/dist/cache/index.d.ts +19 -2
  18. package/dist/color/contrast.d.ts +40 -0
  19. package/dist/color/index.d.ts +2 -0
  20. package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
  21. package/dist/cool-console-log/index.d.ts +34 -5
  22. package/dist/crypto/encoding.d.ts +18 -0
  23. package/dist/crypto/hash.d.ts +33 -0
  24. package/dist/crypto/index.d.ts +4 -0
  25. package/dist/crypto/sealer.d.ts +25 -0
  26. package/dist/crypto/webhook.d.ts +20 -0
  27. package/dist/date/index.d.ts +46 -2
  28. package/dist/emitter/index.d.ts +21 -0
  29. package/dist/error/index.d.ts +12 -0
  30. package/dist/format/index.d.ts +102 -0
  31. package/dist/function/index.d.ts +60 -0
  32. package/dist/guard/index.d.ts +20 -0
  33. package/dist/i18n/index.d.ts +37 -0
  34. package/dist/index-0zvtcbzy.js +224 -0
  35. package/dist/index-181e6vfw.js +29 -0
  36. package/dist/index-8r2vdzjg.js +87 -0
  37. package/dist/index-hy7kbb99.js +157 -0
  38. package/dist/{src/secrets/infisical.js → index-j1ybktbr.js} +500 -538
  39. package/dist/index-k9yx6bne.js +89 -0
  40. package/dist/index-kj1phgrx.js +169 -0
  41. package/dist/index-mnkcfyx8.js +78 -0
  42. package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
  43. package/dist/index-rp5pqghz.js +157 -0
  44. package/dist/index-srt06xd2.js +15 -0
  45. package/dist/index-wjyb9bsd.js +263 -0
  46. package/dist/index.d.ts +7 -7
  47. package/dist/jwt/index.d.ts +50 -2
  48. package/dist/math/index.d.ts +2 -2
  49. package/dist/math/linearRegressionTrend.d.ts +14 -1
  50. package/dist/math/scale.d.ts +8 -0
  51. package/dist/object/index.d.ts +19 -0
  52. package/dist/p-queue/index.d.ts +1 -1
  53. package/dist/promise/index.d.ts +97 -2
  54. package/dist/sitemap/index.d.ts +48 -6
  55. package/dist/src/api-wrapper/index.js +60 -1130
  56. package/dist/src/api-wrapper/testing.js +20 -0
  57. package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
  58. package/dist/src/crypto/index.js +195 -0
  59. package/dist/src/format/index.js +211 -0
  60. package/dist/src/i18n/index.js +36 -0
  61. package/dist/src/index.js +739 -3145
  62. package/dist/src/jwt/index.js +138 -92
  63. package/dist/src/p-queue/index.js +9 -437
  64. package/dist/src/secrets/index.js +44 -1428
  65. package/dist/src/secrets/vite.js +6 -39
  66. package/dist/src/secrets/worker.js +10 -273
  67. package/dist/src/sitemap/index.js +183 -511
  68. package/dist/src/storage/index.js +119 -0
  69. package/dist/src/zod/index.js +340 -167
  70. package/dist/state/index.d.ts +21 -0
  71. package/dist/storage/index.d.ts +58 -0
  72. package/dist/string/index.d.ts +75 -2
  73. package/dist/typedswitch/index.d.ts +31 -24
  74. package/dist/types/index.d.ts +11 -0
  75. package/dist/url/index.d.ts +50 -1
  76. package/dist/zod/index.d.ts +32 -3
  77. package/dist/zod/introspect.d.ts +19 -0
  78. package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
  79. package/dist/zod/zodMinMaxFilter.d.ts +22 -3
  80. package/dist/zod/zodToTypeString.d.ts +8 -3
  81. package/package.json +78 -7
  82. package/src/api-wrapper/actions-impl.ts +11 -72
  83. package/src/api-wrapper/builders.ts +32 -25
  84. package/src/api-wrapper/create-client.ts +171 -41
  85. package/src/api-wrapper/describe.ts +157 -0
  86. package/src/api-wrapper/endpoint.ts +26 -0
  87. package/src/api-wrapper/error-reporting.ts +7 -1
  88. package/src/api-wrapper/errors.ts +107 -2
  89. package/src/api-wrapper/headers.ts +11 -6
  90. package/src/api-wrapper/index.ts +5 -0
  91. package/src/api-wrapper/otel.ts +139 -0
  92. package/src/api-wrapper/paginate.ts +28 -0
  93. package/src/api-wrapper/redaction.ts +42 -0
  94. package/src/api-wrapper/shared.ts +21 -2
  95. package/src/api-wrapper/testing.ts +48 -0
  96. package/src/api-wrapper/transport.ts +41 -43
  97. package/src/api-wrapper/types.ts +185 -18
  98. package/src/array/index.ts +195 -42
  99. package/src/cache/createCache.ts +157 -0
  100. package/src/cache/index.ts +132 -147
  101. package/src/color/contrast.ts +163 -0
  102. package/src/color/index.ts +22 -0
  103. package/src/{math → color}/metricsSeriesColors.ts +47 -49
  104. package/src/cool-console-log/index.ts +128 -41
  105. package/src/crypto/encoding.ts +83 -0
  106. package/src/crypto/hash.ts +170 -0
  107. package/src/crypto/index.ts +26 -0
  108. package/src/crypto/sealer.ts +168 -0
  109. package/src/crypto/webhook.ts +86 -0
  110. package/src/date/index.ts +109 -34
  111. package/src/emitter/index.ts +82 -0
  112. package/src/error/index.ts +63 -0
  113. package/src/format/index.ts +389 -0
  114. package/src/function/index.ts +201 -0
  115. package/src/guard/index.ts +80 -0
  116. package/src/i18n/index.ts +125 -0
  117. package/src/index.ts +11 -7
  118. package/src/jwt/index.ts +199 -125
  119. package/src/math/index.ts +5 -10
  120. package/src/math/linearRegressionTrend.ts +33 -8
  121. package/src/math/scale.ts +17 -0
  122. package/src/object/index.ts +99 -0
  123. package/src/p-queue/index.ts +2 -0
  124. package/src/promise/index.ts +304 -9
  125. package/src/sitemap/index.ts +293 -130
  126. package/src/state/index.ts +133 -81
  127. package/src/storage/index.ts +187 -0
  128. package/src/string/index.ts +261 -23
  129. package/src/typedswitch/index.ts +58 -38
  130. package/src/types/index.ts +14 -0
  131. package/src/url/index.ts +146 -1
  132. package/src/zod/index.ts +105 -9
  133. package/src/zod/introspect.ts +103 -0
  134. package/src/zod/jsonToZodSchemaMapper.ts +44 -21
  135. package/src/zod/zodMinMaxFilter.ts +37 -13
  136. package/src/zod/zodToTypeString.ts +184 -181
  137. package/dist/api-wrapper/index.js +0 -250
  138. package/dist/gzip/index.d.ts +0 -2
  139. package/dist/gzip/index.js +0 -16
  140. package/dist/index.js +0 -1823
  141. package/dist/jwt/index.js +0 -19
  142. package/dist/sitemap/index.js +0 -552
  143. package/dist/src/gzip/index.js +0 -16
  144. package/dist/src/secrets/loaders.js +0 -1529
  145. package/dist/zod/index.js +0 -210
@@ -4,20 +4,21 @@ import type { ApiEndpoint } from './types';
4
4
  export type BuildUrlOptions = {
5
5
  endpoint: ApiEndpoint;
6
6
  params: PathParams | undefined;
7
- queryParams: Record<string, string>;
7
+ queryParams: QueryParams;
8
8
  baseApiUrls: Record<string, BaseApiUrl>;
9
9
  defaultBaseApiUrl: string | undefined;
10
10
  url: string | undefined;
11
11
  doNotEncodeQueryParams?: boolean;
12
12
  configError: (msg: string) => ConfigError;
13
13
  };
14
- export declare function formatQueryString(queryParams: Record<string, string>, doNotEncode?: boolean): string;
15
14
  export declare function buildUrl(options: BuildUrlOptions): {
16
15
  finalUrl: string;
17
16
  fullUrl: string;
18
17
  };
19
18
  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>;
19
+ export declare function buildQueryParams(endpoint: ApiEndpoint, query: QueryParams | undefined, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): QueryParams;
21
20
  export declare function buildRequestBody(endpoint: ApiEndpoint, body: unknown, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): unknown;
22
21
  export declare function buildHeaders(endpoint: ApiEndpoint, headers: Record<string, string> | undefined, defaultHeaders: Record<string, string>, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): Record<string, string>;
22
+ /** Joins with exactly one `/`; empty paths and query-only paths are appended as-is. */
23
+ export declare function joinUrlPath(base: string, path: string): string;
23
24
  export declare function replacePathParams(path: string, params: PathParams | undefined, configError: (msg: string) => ConfigError): string;
@@ -1,6 +1,6 @@
1
1
  import type { ApiAction, ApiClient, ApiClientConfigNoActions, ApiClientConfigWithActions, ApiEndpoint } from './types';
2
2
  export interface CreateApiClientFn {
3
- <T extends Record<string, ApiEndpoint>>(apiConfig: ApiClientConfigNoActions<T>): ApiClient<T, Record<string, never>>;
4
3
  <T extends Record<string, ApiEndpoint>, A extends Record<string, ApiAction>>(apiConfig: ApiClientConfigWithActions<T, A>): ApiClient<T, A>;
4
+ <T extends Record<string, ApiEndpoint>>(apiConfig: ApiClientConfigNoActions<T>): ApiClient<T, Record<string, never>>;
5
5
  }
6
6
  export declare const createApiClient: CreateApiClientFn;
@@ -0,0 +1,16 @@
1
+ import { type ApiError } from './errors';
2
+ /**
3
+ * A multi-line, human-readable summary of an `ApiError` that says up front
4
+ * whether the server, the caller's input, or the response shape is at fault.
5
+ * The client logs this via `logger.error` for every failed request.
6
+ *
7
+ * @example
8
+ * ✗ github.usersGet failed: output validation failed: the response doesn't match resSchema
9
+ * GET https://api.github.com/users/1 · attempt 1/4 · 212ms
10
+ * [1] id: Expected number, received string
11
+ * value: "oops"
12
+ * fix: update resSchema (or resFormatter), or mark changed fields optional
13
+ */
14
+ export declare function describeApiError(error: ApiError): string;
15
+ /** Flat, primitive attributes for structured loggers and log processors. */
16
+ export declare function apiErrorLogAttributes(error: ApiError): Record<string, string | number | boolean>;
@@ -0,0 +1,18 @@
1
+ import type { z } from 'zod';
2
+ import type { ApiEndpoint } from './types';
3
+ /**
4
+ * Defines an endpoint with full inference, as an alternative to
5
+ * `satisfies ApiEndpoint`: `resFormatter` must return what `resSchema`
6
+ * accepts, and a literal `path` types its `:params` (making `reqParams`
7
+ * required when there's no `reqParamsSchema`).
8
+ *
9
+ * @example
10
+ * export const usersGet = defineEndpoint({
11
+ * method: 'GET',
12
+ * path: '/users/:userId',
13
+ * resSchema: User
14
+ * })
15
+ */
16
+ export declare function defineEndpoint<const TEndpoint extends ApiEndpoint>(endpoint: TEndpoint & {
17
+ resFormatter?: (data: unknown, headers: Record<string, string>) => z.input<TEndpoint['resSchema']>;
18
+ }): TEndpoint;
@@ -1,9 +1,22 @@
1
1
  import type { ZodError } from 'zod';
2
2
  import type { ApiErrorContext } from './shared';
3
3
  import { type FormattedZodIssue } from './zod-issues';
4
+ /**
5
+ * Where a request failed, for logs and alerts:
6
+ * - `server_error`: the API returned 5xx
7
+ * - `client_error`: the API rejected the request (4xx: auth, params, body)
8
+ * - `input_validation`: the caller's data failed a request schema; nothing was sent
9
+ * - `output_validation`: the response wasn't JSON or didn't match `resSchema`
10
+ * - `response_rejected`: `checkResponse` rejected a valid response
11
+ * - `network` / `timeout` / `aborted`: the request didn't complete
12
+ * - `config`: the client or call is misconfigured
13
+ * - `action`: a composite action handler threw
14
+ */
15
+ export type ApiErrorCategory = 'server_error' | 'client_error' | 'input_validation' | 'output_validation' | 'response_rejected' | 'network' | 'timeout' | 'aborted' | 'config' | 'action' | 'unknown';
4
16
  export declare class ApiError extends Error {
5
17
  context: ApiErrorContext;
6
18
  cause?: unknown;
19
+ get category(): ApiErrorCategory;
7
20
  constructor(message: string, context: ApiErrorContext, options?: {
8
21
  cause?: unknown;
9
22
  });
@@ -16,6 +29,7 @@ export declare class HttpResponseError extends ApiError {
16
29
  readonly responseHeaders: Record<string, string>;
17
30
  readonly requestId?: string;
18
31
  constructor(status: number, body: string, responseHeaders: Record<string, string>, context: ApiErrorContext);
32
+ get category(): ApiErrorCategory;
19
33
  toJSON(): Record<string, unknown>;
20
34
  }
21
35
  export declare class ResponseValidationError extends ApiError {
@@ -23,10 +37,16 @@ export declare class ResponseValidationError extends ApiError {
23
37
  readonly rawResponse: unknown;
24
38
  readonly issues: FormattedZodIssue[];
25
39
  readonly validationIssues: string;
26
- constructor(zodError: ZodError, rawResponse: unknown, context: ApiErrorContext);
40
+ /** True when the body wasn't valid JSON (rather than failing `resSchema`). */
41
+ readonly invalidJson: boolean;
42
+ constructor(zodError: ZodError, rawResponse: unknown, context: ApiErrorContext, options?: {
43
+ invalidJson?: boolean;
44
+ });
45
+ get category(): ApiErrorCategory;
27
46
  toJSON(): Record<string, unknown>;
28
47
  }
29
- export type RequestValidationTarget = 'body' | 'query' | 'params' | 'headers';
48
+ /** Which request schema failed; `input` is an action's input schema. */
49
+ export type RequestValidationTarget = 'body' | 'query' | 'params' | 'headers' | 'input';
30
50
  export declare class RequestValidationError extends ApiError {
31
51
  readonly zodError: ZodError;
32
52
  readonly target: RequestValidationTarget;
@@ -34,23 +54,39 @@ export declare class RequestValidationError extends ApiError {
34
54
  readonly issues: FormattedZodIssue[];
35
55
  readonly validationIssues: string;
36
56
  constructor(zodError: ZodError, target: RequestValidationTarget, rawInput: unknown, context: ApiErrorContext);
57
+ get category(): ApiErrorCategory;
37
58
  toJSON(): Record<string, unknown>;
38
59
  }
39
60
  export declare class RequestTimeoutError extends ApiError {
40
61
  readonly timeoutMs: number;
41
62
  constructor(timeoutMs: number, context: ApiErrorContext);
63
+ get category(): ApiErrorCategory;
42
64
  }
43
65
  export declare class NetworkError extends ApiError {
44
66
  constructor(message: string, context: ApiErrorContext, cause?: unknown);
67
+ get category(): ApiErrorCategory;
45
68
  }
46
69
  export declare class RequestAbortedError extends ApiError {
47
70
  constructor(context: ApiErrorContext, cause?: unknown);
71
+ get category(): ApiErrorCategory;
48
72
  }
49
73
  export declare class ConfigError extends ApiError {
50
74
  constructor(message: string, context: ApiErrorContext);
75
+ get category(): ApiErrorCategory;
76
+ }
77
+ /**
78
+ * Thrown when the client's `checkResponse` rejects a response that passed
79
+ * schema validation (e.g. HTTP 200 with `{ success: false }`).
80
+ */
81
+ export declare class ApiResponseError extends ApiError {
82
+ readonly data: unknown;
83
+ constructor(message: string, data: unknown, context: ApiErrorContext, cause?: unknown);
84
+ get category(): ApiErrorCategory;
85
+ toJSON(): Record<string, unknown>;
51
86
  }
52
87
  export declare class ActionError extends ApiError {
53
88
  constructor(message: string, context: ApiErrorContext, cause?: unknown);
89
+ get category(): ApiErrorCategory;
54
90
  }
55
91
  export declare const isApiError: (e: unknown) => e is ApiError;
56
92
  export declare const isHttpResponseError: (e: unknown) => e is HttpResponseError;
@@ -61,4 +97,5 @@ export declare const isNetworkError: (e: unknown) => e is NetworkError;
61
97
  export declare const isRequestAbortedError: (e: unknown) => e is RequestAbortedError;
62
98
  export declare const isConfigError: (e: unknown) => e is ConfigError;
63
99
  export declare const isActionError: (e: unknown) => e is ActionError;
100
+ export declare const isApiResponseError: (e: unknown) => e is ApiResponseError;
64
101
  export declare function isNonRetryableApiError(error: unknown): boolean;
@@ -3,3 +3,8 @@ export * from './types';
3
3
  export * from './errors';
4
4
  export { createApiClient } from './create-client';
5
5
  export { defineAction } from './actions-impl';
6
+ export { type PaginateOptions, paginate } from './paginate';
7
+ export { defaultShouldRetry } from './transport';
8
+ export { apiErrorLogAttributes, describeApiError } from './describe';
9
+ export * from './otel';
10
+ export { defineEndpoint } from './endpoint';
@@ -0,0 +1,44 @@
1
+ import type { ApiMiddleware, ApiMiddlewareContext } from './types';
2
+ export type OtelAttributes = Record<string, string | number | boolean | undefined>;
3
+ export type OtelSpanLike = {
4
+ setAttributes(attributes: OtelAttributes): unknown;
5
+ setStatus(status: {
6
+ code: number;
7
+ message?: string;
8
+ }): unknown;
9
+ recordException(exception: Error | string): unknown;
10
+ end(): void;
11
+ };
12
+ export type OtelTracerLike = {
13
+ startActiveSpan<F extends (span: OtelSpanLike) => unknown>(name: string, options: {
14
+ kind?: number;
15
+ attributes?: OtelAttributes;
16
+ }, fn: F): ReturnType<F>;
17
+ };
18
+ export type OtelMiddlewareOptions = {
19
+ /** Default: `client.endpoint`, or the endpoint key when the client has no `name`. */
20
+ spanName?: (context: ApiMiddlewareContext) => string;
21
+ /** Extra attributes set when the span starts. */
22
+ attributes?: (context: ApiMiddlewareContext) => OtelAttributes;
23
+ /**
24
+ * `url.full` keeps the query string with sensitive values redacted
25
+ * (default). Set `false` to drop the query string entirely.
26
+ */
27
+ includeQuery?: boolean;
28
+ /**
29
+ * Adds trace-context headers to the outgoing request, e.g.
30
+ * `(headers) => propagation.inject(context.active(), headers)`. Runs inside
31
+ * the active span. Not needed when fetch is already auto-instrumented.
32
+ */
33
+ inject?: (headers: Record<string, string>) => void;
34
+ };
35
+ /**
36
+ * One CLIENT span per logical request (all retries, validation and
37
+ * `checkResponse`), with OpenTelemetry HTTP semantic-convention attributes.
38
+ * It's the active span, so auto-instrumented fetch attempts become children.
39
+ *
40
+ * @example
41
+ * import { trace } from '@opentelemetry/api'
42
+ * createApiClient({ ...config, use: [otelMiddleware(trace.getTracer('app'))] })
43
+ */
44
+ export declare function otelMiddleware(tracer: OtelTracerLike, options?: OtelMiddlewareOptions): ApiMiddleware;
@@ -0,0 +1,12 @@
1
+ export type PaginateOptions = {
2
+ /** Stop after this many pages (default: no limit). */
3
+ maxPages?: number;
4
+ /** Checked before each page fetch; throws `signal.reason` when aborted. */
5
+ signal?: AbortSignal;
6
+ };
7
+ /**
8
+ * Yields pages until `getNext` returns `null`/`undefined`. The first call to
9
+ * `fetchPage` gets `undefined` as cursor. Pages are fetched lazily, so
10
+ * breaking out of `for await` stops further requests.
11
+ */
12
+ export declare function paginate<TPage, TCursor>(fetchPage: (cursor: TCursor | undefined) => Promise<TPage>, getNext: (page: TPage) => TCursor | null | undefined, options?: PaginateOptions): AsyncGenerator<TPage, void, undefined>;
@@ -1,2 +1,4 @@
1
1
  import type { ApiErrorContext } from './shared';
2
+ /** Replaces values of sensitive query parameters (`?api_key=…`) in a URL. */
3
+ export declare function redactUrl(url: string): string;
2
4
  export declare function defaultRedactContext(ctx: ApiErrorContext): ApiErrorContext;
@@ -1,8 +1,22 @@
1
+ import type { UrlQueryParams } from '../url';
1
2
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
2
3
  export type RequestBodyFormat = 'json' | 'form-data';
3
4
  export type BaseApiUrl = string | (() => string);
4
5
  export type PathParams = Record<string, string | number>;
5
- export type QueryParams = Record<string, string | number | boolean | undefined>;
6
+ /** Array values repeat the key (`a=1&a=2`); `null`/`undefined` are skipped. */
7
+ export type QueryParams = UrlQueryParams;
8
+ /**
9
+ * Augment to type `meta` on requests, actions, hooks, middleware and errors:
10
+ *
11
+ * @example
12
+ * declare module 'hanzio/api-wrapper' {
13
+ * interface ApiRequestMeta { traceId: string; tenantId?: string }
14
+ * }
15
+ */
16
+ export interface ApiRequestMeta {
17
+ }
18
+ /** `ApiRequestMeta` once augmented, otherwise any record. */
19
+ export type RequestMeta = keyof ApiRequestMeta extends never ? Record<string, unknown> : ApiRequestMeta;
6
20
  export type ApiErrorContext = {
7
21
  endpoint: string;
8
22
  method: HttpMethod;
@@ -12,5 +26,5 @@ export type ApiErrorContext = {
12
26
  elapsedMs: number;
13
27
  requestHeaders?: Record<string, string>;
14
28
  requestBody?: unknown;
15
- meta?: Record<string, unknown>;
29
+ meta?: RequestMeta;
16
30
  };
@@ -0,0 +1,23 @@
1
+ import type { FetchLike } from './types';
2
+ export type MockFetch = FetchLike & {
3
+ /** Every request received, in order. Bodies are still readable. */
4
+ readonly calls: Request[];
5
+ };
6
+ /**
7
+ * A fetch stand-in for tests: pass it as the client's `fetch` option instead
8
+ * of replacing `globalThis.fetch`. Test-runner agnostic.
9
+ *
10
+ * @example
11
+ * const fetch = createMockFetch((request) =>
12
+ * request.url.endsWith('/users/1')
13
+ * ? jsonResponse({ id: 1 })
14
+ * : jsonResponse({ error: 'not found' }, { status: 404 })
15
+ * )
16
+ * const api = createApiClient({ ...config, fetch })
17
+ */
18
+ export declare function createMockFetch(handler: (request: Request, callIndex: number) => Response | Promise<Response>): MockFetch;
19
+ /** A JSON `Response` with `content-type: application/json`. */
20
+ export declare function jsonResponse(body: unknown, init?: {
21
+ status?: number;
22
+ headers?: Record<string, string>;
23
+ }): Response;
@@ -1,18 +1,17 @@
1
1
  import { type z } from 'zod';
2
- import type { ApiEndpoint, OnResponseContext, OnRetryContext, RetryContext } from './types';
3
- import type { ApiErrorContext, HttpMethod } from './shared';
2
+ import type { ApiEndpoint, FetchLike, OnResponseContext, OnRetryContext, RetryContext } from './types';
3
+ import type { ApiErrorContext, HttpMethod, RequestMeta } from './shared';
4
4
  export type RequestConfig = {
5
5
  method: HttpMethod;
6
6
  fullUrl: string;
7
7
  body: unknown;
8
8
  headers: Record<string, string>;
9
9
  timeoutMs?: number;
10
- fetchFn: typeof fetch;
10
+ fetchFn: FetchLike;
11
11
  userSignal?: AbortSignal;
12
12
  };
13
13
  export declare function validateResponse<T extends z.ZodType>(schema: T, data: unknown, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): Promise<z.infer<T>>;
14
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
15
  export declare function readErrorResponse(response: Response): Promise<{
17
16
  body: string;
18
17
  headers: Record<string, string>;
@@ -38,7 +37,7 @@ type MakeRequestArgs<TValidated> = {
38
37
  endpointName: string;
39
38
  method: HttpMethod;
40
39
  fullUrl: string;
41
- meta?: Record<string, unknown>;
40
+ meta?: RequestMeta;
42
41
  onResponse?: (context: OnResponseContext) => void | Promise<void>;
43
42
  onRetry?: (context: OnRetryContext) => void | Promise<void>;
44
43
  };
@@ -1,6 +1,7 @@
1
1
  import type { z } from 'zod';
2
+ import type { Result } from '../promise';
2
3
  import type { ApiError } from './errors';
3
- import type { ApiErrorContext, BaseApiUrl, HttpMethod, PathParams, QueryParams, RequestBodyFormat } from './shared';
4
+ import type { ApiErrorContext, BaseApiUrl, HttpMethod, PathParams, QueryParams, RequestBodyFormat, RequestMeta } from './shared';
4
5
  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
6
  method: HttpMethod;
6
7
  path: string | ((baseUrl: string) => string);
@@ -12,7 +13,8 @@ export interface ApiEndpoint<TReqBody extends z.ZodType | undefined = z.ZodType
12
13
  baseApiUrl?: string;
13
14
  reqBodyFormat?: RequestBodyFormat;
14
15
  defaultHeaders?: Record<string, string>;
15
- resFormatter?: (data: unknown, headers: Record<string, string>) => z.infer<TResponse>;
16
+ /** Reshapes the raw body before `resSchema` parses it, so it returns the schema's input type. */
17
+ resFormatter?: (data: unknown, headers: Record<string, string>) => z.input<TResponse>;
16
18
  reqDefaultQueryParams?: QueryParams;
17
19
  doNotEncodeQueryParams?: boolean;
18
20
  }
@@ -47,7 +49,7 @@ export type OnRequestContext = {
47
49
  url: string;
48
50
  headers: Record<string, string>;
49
51
  body: unknown;
50
- meta?: Record<string, unknown>;
52
+ meta?: RequestMeta;
51
53
  };
52
54
  export type OnResponseContext = {
53
55
  endpoint: string;
@@ -55,7 +57,7 @@ export type OnResponseContext = {
55
57
  url: string;
56
58
  response: Response;
57
59
  attempt: number;
58
- meta?: Record<string, unknown>;
60
+ meta?: RequestMeta;
59
61
  };
60
62
  export type ActionCache = {
61
63
  <T>(key: string, fn: () => Promise<T> | T, options?: {
@@ -70,76 +72,159 @@ export type ActionCache = {
70
72
  };
71
73
  export type ActionInvokeOptions = {
72
74
  signal?: AbortSignal;
73
- meta?: Record<string, unknown>;
75
+ meta?: RequestMeta;
74
76
  };
75
77
  export type ActionContext<TInput, TApi = any> = {
76
78
  input: TInput;
77
79
  api: TApi;
78
80
  signal?: AbortSignal;
79
- meta?: Record<string, unknown>;
81
+ meta?: RequestMeta;
80
82
  logger?: Pick<Console, 'debug' | 'error'>;
81
83
  cache: ActionCache;
82
84
  };
83
85
  export interface ApiAction<TInput = any, TOutput = any> {
84
86
  handler: (ctx: ActionContext<TInput>) => Promise<TOutput> | TOutput;
87
+ /** Validates the caller's input at runtime; `handler` receives the parsed output. */
88
+ readonly input?: z.ZodType;
85
89
  /** When true, the action accepts only optional `{ signal, meta }`; input is always `undefined`. */
86
90
  readonly noRuntimeInput?: boolean;
87
91
  }
88
92
  type ActionDefinition<TInput, TOutput, TApi> = {
89
93
  handler: (ctx: ActionContext<TInput, TApi>) => Promise<TOutput> | TOutput;
90
94
  };
95
+ type SchemaActionDefinition<TSchema extends z.ZodType, TOutput, TApi> = {
96
+ input: TSchema;
97
+ handler: (ctx: ActionContext<z.output<TSchema>, TApi>) => Promise<TOutput> | TOutput;
98
+ };
91
99
  export type DefineAction<TApi> = {
100
+ /** Input validated by a Zod schema: callers pass `z.input`, the handler gets `z.output`. */
101
+ <TSchema extends z.ZodType, TOutput>(def: SchemaActionDefinition<TSchema, TOutput, TApi>): ApiAction<z.input<TSchema>, TOutput>;
92
102
  <TOutput>(def: ActionDefinition<undefined, TOutput, TApi>): ApiAction<undefined, TOutput>;
93
103
  <TInput>(): <TOutput>(def: ActionDefinition<TInput, TOutput, TApi>) => ApiAction<TInput, TOutput>;
94
104
  };
95
105
  type ActionInput<A> = A extends ApiAction<infer I, any> ? I : never;
96
106
  export type ActionOutput<A> = A extends ApiAction<any, infer O> ? O : never;
97
107
  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);
108
+ type CharsOf<S extends string, Acc extends string = never> = S extends `${infer C}${infer Rest}` ? CharsOf<Rest, Acc | C> : Acc;
109
+ type IdentStart = CharsOf<'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'>;
110
+ type WordChar = IdentStart | CharsOf<'0123456789'>;
111
+ type TakeWord<S extends string, Acc extends string = ''> = S extends `${infer C}${infer Rest}` ? C extends WordChar ? TakeWord<Rest, `${Acc}${C}`> : Acc : Acc;
112
+ /**
113
+ * `:name` placeholders in a literal path, matching the runtime rule (a letter
114
+ * or `_`, then word characters, so ports like `:8080` don't count).
115
+ * `never` for non-literal paths.
116
+ */
117
+ export type PathParamNames<TPath> = TPath extends string ? string extends TPath ? never : TPath extends `${string}:${infer Rest}` ? (Rest extends `${IdentStart}${string}` ? TakeWord<Rest> : never) | PathParamNames<Rest> : never : never;
118
+ type EndpointPathParamNames<TEndpoint extends ApiEndpoint> = PathParamNames<TEndpoint['path']>;
119
+ type SchemaInputKeys<TEndpoint extends ApiEndpoint> = (TEndpoint['reqBodySchema'] extends z.ZodType ? 'reqBody' : never) | (TEndpoint['reqQuerySchema'] extends z.ZodType ? 'reqQuery' : never) | (TEndpoint['reqHeadersSchema'] extends z.ZodType ? 'reqHeaders' : never) | (TEndpoint['reqParamsSchema'] extends z.ZodType ? 'reqParams' : [EndpointPathParamNames<TEndpoint>] extends [never] ? never : 'reqParams');
99
120
  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>;
121
+ reqBody: TEndpoint['reqBodySchema'] extends z.ZodType ? z.input<TEndpoint['reqBodySchema']> : unknown;
122
+ reqParams: TEndpoint['reqParamsSchema'] extends z.ZodType ? z.input<TEndpoint['reqParamsSchema']> : [EndpointPathParamNames<TEndpoint>] extends [never] ? PathParams : Record<EndpointPathParamNames<TEndpoint>, string | number>;
123
+ reqQuery: TEndpoint['reqQuerySchema'] extends z.ZodType ? z.input<TEndpoint['reqQuerySchema']> : QueryParams;
124
+ reqHeaders: TEndpoint['reqHeadersSchema'] extends z.ZodType ? z.input<TEndpoint['reqHeadersSchema']> : Record<string, string>;
104
125
  };
105
126
  type CommonRequestOptions = {
106
127
  url?: string;
107
128
  signal?: AbortSignal;
108
129
  timeoutMs?: number;
109
130
  retries?: number;
110
- fetch?: typeof fetch;
111
- meta?: Record<string, unknown>;
131
+ fetch?: FetchLike;
132
+ meta?: RequestMeta;
112
133
  };
113
134
  export type RequestInput<TEndpoint extends ApiEndpoint> = Pick<SchemaInputs<TEndpoint>, SchemaInputKeys<TEndpoint>> & Partial<Omit<SchemaInputs<TEndpoint>, SchemaInputKeys<TEndpoint>>> & CommonRequestOptions;
114
135
  type RequestArgs<TEndpoint extends ApiEndpoint> = SchemaInputKeys<TEndpoint> extends never ? [input?: RequestInput<TEndpoint>] : [input: RequestInput<TEndpoint>];
115
136
  export type ApiClient<T extends Record<string, ApiEndpoint>, A extends Record<string, ApiAction> = Record<string, never>> = {
116
137
  request: {
117
- <K extends keyof T>(endpointKey: K, ...args: RequestArgs<T[K]>): Promise<ApiWrapperResponse<z.infer<T[K]['resSchema']>>>;
138
+ <K extends keyof T>(endpointKey: K, ...args: RequestArgs<T[K]>): Promise<ApiWrapperResponse<EndpointResponse<T[K]>>>;
118
139
  <K extends keyof A>(actionKey: K, ...args: ActionArgs<A[K]>): Promise<ActionOutput<A[K]>>;
119
140
  };
120
141
  cache: ActionCache;
142
+ /**
143
+ * The same endpoints and actions, resolving to `Result` instead of throwing
144
+ * `ApiError`s. Other errors (bugs in hooks or middleware) still throw.
145
+ */
146
+ safe: {
147
+ [K in keyof T]: (...args: RequestArgs<T[K]>) => Promise<Result<ApiWrapperResponse<EndpointResponse<T[K]>>, ApiError>>;
148
+ } & {
149
+ [K in keyof A]: (...args: ActionArgs<A[K]>) => Promise<Result<ActionOutput<A[K]>, ApiError>>;
150
+ };
121
151
  } & {
122
- [K in keyof T]: (...args: RequestArgs<T[K]>) => Promise<ApiWrapperResponse<z.infer<T[K]['resSchema']>>>;
152
+ [K in keyof T]: (...args: RequestArgs<T[K]>) => Promise<ApiWrapperResponse<EndpointResponse<T[K]>>>;
123
153
  } & {
124
154
  [K in keyof A]: (...args: ActionArgs<A[K]>) => Promise<ActionOutput<A[K]>>;
125
155
  };
156
+ /**
157
+ * Any client, for helpers that accept whichever client they're given. Call
158
+ * endpoints through `request` or `safe`; results are `unknown`.
159
+ */
160
+ export type AnyApiClient = {
161
+ request(key: string, ...args: any[]): Promise<unknown>;
162
+ cache: ActionCache;
163
+ safe: Record<string, (...args: any[]) => Promise<Result<unknown, ApiError>>>;
164
+ };
165
+ /** Parsed response data of an endpoint definition (`z.output` of `resSchema`). */
166
+ export type EndpointResponse<TEndpoint extends ApiEndpoint> = z.output<TEndpoint['resSchema']>;
167
+ /** Request input for an endpoint definition (`reqBody`, `reqParams`, …). */
168
+ export type EndpointRequest<TEndpoint extends ApiEndpoint> = RequestInput<TEndpoint>;
169
+ /** Response data of a client method: `InferResponse<typeof api, 'usersGet'>`. */
170
+ export type InferResponse<TClient, K extends keyof TClient> = TClient[K] extends (...args: never[]) => Promise<infer R> ? R extends ApiWrapperResponse<infer D> ? D : R : never;
171
+ /** First argument of a client method: `InferRequest<typeof api, 'usersGet'>`. */
172
+ export type InferRequest<TClient, K extends keyof TClient> = TClient[K] extends (...args: infer P) => unknown ? NonNullable<P[0]> : never;
126
173
  export type ActionsFactoryHelpers<T extends Record<string, ApiEndpoint>, A extends Record<string, ApiAction> = Record<string, never>> = {
127
174
  api: ApiClient<T, A>;
128
175
  defineAction: DefineAction<ApiClient<T, A>>;
129
176
  };
177
+ /** Any fetch-compatible function; avoids Bun's `typeof fetch` extras like `preconnect`. */
178
+ export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
179
+ export type ApiMiddlewareContext = {
180
+ /** The client's `name`, when set. */
181
+ client?: string;
182
+ /** The endpoint key, e.g. `usersGet`. */
183
+ endpoint: string;
184
+ method: HttpMethod;
185
+ /** Full URL including the query string. */
186
+ url: string;
187
+ /** Outgoing headers. Mutate before `next()` to add headers (e.g. `traceparent`). */
188
+ headers: Record<string, string>;
189
+ meta?: RequestMeta;
190
+ };
191
+ /**
192
+ * Wraps one logical request: `onRequest`, every retry attempt, validation and
193
+ * `checkResponse`. Call `next()` to continue; errors it throws are `ApiError`s
194
+ * that were already redacted and passed to `onError`.
195
+ */
196
+ export type ApiMiddleware = (context: ApiMiddlewareContext, next: () => Promise<ApiWrapperResponse<unknown>>) => Promise<ApiWrapperResponse<unknown>>;
197
+ export type CheckResponseContext = {
198
+ endpoint: string;
199
+ method: HttpMethod;
200
+ url: string;
201
+ httpStatus: number;
202
+ attempt: number;
203
+ meta?: RequestMeta;
204
+ };
205
+ export type DefaultHeaders = Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
130
206
  export interface ApiClientConfig<T extends Record<string, ApiEndpoint>, A extends Record<string, ApiAction> = Record<string, never>> {
131
207
  name?: string;
132
208
  baseApiUrls: Record<string, BaseApiUrl>;
133
209
  defaultBaseApiUrl?: string;
134
210
  endpoints: T;
135
- defaultHeaders?: Record<string, string>;
211
+ /** Static headers, or a function resolved on every request (e.g. auth from a secret store). */
212
+ defaultHeaders?: DefaultHeaders;
136
213
  timeoutMs?: number;
137
214
  retries?: number;
138
215
  retryDelayMs?: number | ((attempt: number) => number);
139
216
  maxRetryDelayMs?: number;
140
217
  shouldRetry?: (context: RetryContext) => boolean;
141
218
  logger?: Pick<Console, 'debug' | 'error'>;
142
- fetch?: typeof fetch;
219
+ fetch?: FetchLike;
220
+ /** Middleware around each whole request, in array order (first is outermost). */
221
+ use?: ApiMiddleware[];
222
+ /**
223
+ * Runs after `resSchema` validation on every attempt. Throw to reject the
224
+ * response; non-`ApiError` throws become `ApiResponseError`, which the
225
+ * default retry policy does not retry.
226
+ */
227
+ checkResponse?: (data: unknown, context: CheckResponseContext) => void | Promise<void>;
143
228
  onRequest?: (context: OnRequestContext) => RequestInterceptorResult | Promise<RequestInterceptorResult>;
144
229
  onResponse?: (context: OnResponseContext) => void | Promise<void>;
145
230
  onError?: (error: ApiError) => void | Promise<void>;
@@ -151,4 +236,4 @@ export type ApiClientConfigNoActions<T extends Record<string, ApiEndpoint>> = Om
151
236
  export type ApiClientConfigWithActions<T extends Record<string, ApiEndpoint>, A extends Record<string, ApiAction>> = Omit<ApiClientConfig<T, Record<string, never>>, 'actions'> & {
152
237
  actions: A | ((helpers: ActionsFactoryHelpers<T, A>) => A);
153
238
  };
154
- export type { BaseApiUrl, HttpMethod, PathParams, QueryParams, RequestBodyFormat, ApiErrorContext } from './shared';
239
+ export type { ApiRequestMeta, RequestMeta, BaseApiUrl, HttpMethod, PathParams, QueryParams, RequestBodyFormat, ApiErrorContext } from './shared';
@@ -1,22 +1,68 @@
1
1
  export interface ArrayDifference<TCurrent, TNext> {
2
2
  new: TNext[];
3
+ /** Items present in both arrays (`update` + `unchanged`). */
3
4
  upsert: TNext[];
4
5
  delete: TCurrent[];
5
6
  }
7
+ export interface ArrayDifferenceWithChanges<TCurrent, TNext> extends ArrayDifference<TCurrent, TNext> {
8
+ /** Matched items where `isEqual` returned false. */
9
+ update: TNext[];
10
+ /** Matched items where `isEqual` returned true. */
11
+ unchanged: TNext[];
12
+ }
6
13
  type PropertyKeyValue = string | number | symbol;
7
14
  type NonNullableValue<T> = T extends null | undefined ? never : T;
8
15
  type ItemsMatchingKeyValue<T, TKey extends keyof T, TValue> = [
9
16
  Extract<T, Record<TKey, TValue>>
10
17
  ] extends [never] ? T : Extract<T, Record<TKey, TValue>>;
11
18
  export declare function chunkArray<T>(array: readonly T[], chunkSize: number): T[][];
12
- export declare function findArrayDifferenceByKey<TCurrent, TNext, TKey extends keyof TCurrent & keyof TNext>(current: readonly TCurrent[], next: readonly TNext[], key: TKey): ArrayDifference<TCurrent, TNext>;
19
+ type DifferenceKey<TCurrent, TNext> = (keyof TCurrent & keyof TNext) | ((item: TCurrent | TNext) => unknown);
20
+ /**
21
+ * Splits `next` into items that are new or also in `current` (`upsert`), and
22
+ * lists the `current` items missing from `next` (`delete`). Items match by a
23
+ * shared key name or a key function; key values are compared as strings.
24
+ * Pass `isEqual` to also split `upsert` into `update` and `unchanged`.
25
+ */
26
+ export declare function findArrayDifferenceByKey<TCurrent, TNext>(current: readonly TCurrent[], next: readonly TNext[], key: DifferenceKey<TCurrent, TNext>, options: {
27
+ isEqual: (current: TCurrent, next: TNext) => boolean;
28
+ }): ArrayDifferenceWithChanges<TCurrent, TNext>;
29
+ export declare function findArrayDifferenceByKey<TCurrent, TNext>(current: readonly TCurrent[], next: readonly TNext[], key: DifferenceKey<TCurrent, TNext>, options?: {
30
+ isEqual?: (current: TCurrent, next: TNext) => boolean;
31
+ }): ArrayDifference<TCurrent, TNext>;
13
32
  export declare function getUniqueValues<T>(array: readonly T[]): T[];
14
33
  export declare function compact<T>(array: readonly T[]): NonNullableValue<T>[];
15
- export declare function getUniqueValuesByKey<T, TKey extends keyof T>(array: readonly T[], key: TKey): T[];
16
- export declare function groupBy<T, TKey extends keyof T, TGroupKey extends Extract<T[TKey], PropertyKeyValue>>(array: readonly T[], key: TKey): Record<TGroupKey, T[]>;
17
- export declare function groupBy<T, TGroupKey extends PropertyKeyValue>(array: readonly T[], keyFn: (item: T) => TGroupKey): Record<TGroupKey, T[]>;
34
+ export declare function getUniqueValuesByKey<T>(array: readonly T[], keyOrFn: keyof T | ((item: T) => unknown)): T[];
35
+ export declare function groupBy<T, TKey extends keyof T, TGroupKey extends Extract<T[TKey], PropertyKeyValue>>(array: readonly T[], key: TKey): Partial<Record<TGroupKey, T[]>>;
36
+ export declare function groupBy<T, TGroupKey extends PropertyKeyValue>(array: readonly T[], keyFn: (item: T) => TGroupKey): Partial<Record<TGroupKey, T[]>>;
37
+ export declare function countBy<T, TKey extends keyof T, TGroupKey extends Extract<T[TKey], PropertyKeyValue>>(array: readonly T[], key: TKey): Partial<Record<TGroupKey, number>>;
38
+ export declare function countBy<T, TGroupKey extends PropertyKeyValue>(array: readonly T[], keyFn: (item: T) => TGroupKey): Partial<Record<TGroupKey, number>>;
18
39
  export declare function keyBy<T, TKey extends keyof T, TRecordKey extends Extract<T[TKey], PropertyKeyValue>>(array: readonly T[], key: TKey): Record<TRecordKey, T>;
19
40
  export declare function keyBy<T, TRecordKey extends PropertyKeyValue>(array: readonly T[], keyFn: (item: T) => TRecordKey): Record<TRecordKey, T>;
20
41
  export declare function partition<T>(array: readonly T[], predicate: (item: T, index: number) => boolean): [matched: T[], unmatched: T[]];
21
42
  export declare function pickItemsInArray<T, TKey extends keyof T, const TValue extends readonly T[TKey][]>(array: readonly T[], key: TKey, values: TValue): ItemsMatchingKeyValue<T, TKey, TValue[number]>[];
43
+ /** Wraps a single value in an array; `null`/`undefined` become `[]`. Arrays are copied. */
44
+ export declare function toArray<T>(value: T | readonly T[] | null | undefined): T[];
45
+ type NumericKey<T> = {
46
+ [K in keyof T]: T[K] extends number | null | undefined ? K : never;
47
+ }[keyof T];
48
+ /** Sums a numeric property or callback result; nullish values count as 0. */
49
+ export declare function sumBy<T>(array: readonly T[], fn: (item: T) => number | null | undefined): number;
50
+ export declare function sumBy<T>(array: readonly T[], key: NumericKey<T>): number;
51
+ type SortSelector<T> = keyof T | ((item: T) => unknown);
52
+ export type SortCriterion<T> = SortSelector<T> | {
53
+ by: SortSelector<T>;
54
+ order: 'asc' | 'desc';
55
+ };
56
+ /**
57
+ * Returns a sorted copy. Each criterion is a key, a callback, or
58
+ * `{ by, order }`; later criteria break ties. The sort is stable, and
59
+ * `null`/`undefined` values always go last. Without criteria the items
60
+ * themselves are compared.
61
+ */
62
+ export declare function sortBy<T>(array: readonly T[], ...criteria: SortCriterion<T>[]): T[];
63
+ /**
64
+ * Numbers from `start` up to (not including) `end`. With one argument it
65
+ * counts from 0. `step` defaults to 1, or -1 when `end < start`.
66
+ */
67
+ export declare function range(start: number, end?: number, step?: number): number[];
22
68
  export {};