kayto_ts 0.1.3 → 0.1.5

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.
@@ -0,0 +1,3 @@
1
+ import { type Api, type ClientConfig, type EndpointsMap } from "./types.js";
2
+ export type { Api, ClientConfig, ClientError, ClientHooks, EndpointsMap, EndpointOf, EndpointResult, FetchResult, RequestInput, RequestOptions, } from "./types.js";
3
+ export declare function clientApi<Endpoints extends EndpointsMap>(config?: ClientConfig): Api<Endpoints>;
package/dist/index.js ADDED
@@ -0,0 +1,114 @@
1
+ import {} from "./types.js";
2
+ import { buildPath, createBodyAndHeaders, createFetchSignal, HTTP_METHOD, makeClientError, resolveRequestUrl, safeResponseBody, } from "./utils.js";
3
+ export function clientApi(config = {}) {
4
+ const { baseUrl, onRequest, onResponse, responseInterceptor } = config;
5
+ const request = async (method, path, input) => {
6
+ const requestInput = input ?? {};
7
+ const { signal, cleanup, didTimeout } = createFetchSignal(requestInput);
8
+ const builtPath = buildPath(path, requestInput.params);
9
+ const resolvedPath = resolveRequestUrl(builtPath, baseUrl);
10
+ const { body, headers } = createBodyAndHeaders(requestInput);
11
+ const init = {
12
+ method: HTTP_METHOD[method],
13
+ signal,
14
+ headers,
15
+ body,
16
+ };
17
+ if (onRequest) {
18
+ try {
19
+ await onRequest({ method, path: resolvedPath, init });
20
+ }
21
+ catch (err) {
22
+ cleanup();
23
+ return {
24
+ ok: false,
25
+ error: makeClientError("hook", "onRequest hook failed", err),
26
+ };
27
+ }
28
+ }
29
+ const startedAt = Date.now();
30
+ let rawResponse;
31
+ try {
32
+ rawResponse = await fetch(resolvedPath, init);
33
+ }
34
+ catch (err) {
35
+ if (didTimeout()) {
36
+ return {
37
+ ok: false,
38
+ error: makeClientError("timeout", "Request timed out", err),
39
+ };
40
+ }
41
+ if (err instanceof DOMException && err.name === "AbortError") {
42
+ return {
43
+ ok: false,
44
+ error: makeClientError("aborted", "Request was aborted", err),
45
+ };
46
+ }
47
+ return {
48
+ ok: false,
49
+ error: makeClientError("network", "Network error before response", err),
50
+ };
51
+ }
52
+ finally {
53
+ cleanup();
54
+ }
55
+ let response = rawResponse;
56
+ if (responseInterceptor) {
57
+ try {
58
+ response = await responseInterceptor({
59
+ method,
60
+ path: resolvedPath,
61
+ response: rawResponse,
62
+ });
63
+ }
64
+ catch (err) {
65
+ return {
66
+ ok: false,
67
+ error: makeClientError("hook", "responseInterceptor failed", err),
68
+ response: rawResponse,
69
+ };
70
+ }
71
+ }
72
+ if (onResponse) {
73
+ try {
74
+ await onResponse({
75
+ method,
76
+ path: resolvedPath,
77
+ response,
78
+ durationMs: Date.now() - startedAt,
79
+ });
80
+ }
81
+ catch (err) {
82
+ return {
83
+ ok: false,
84
+ error: makeClientError("hook", "onResponse hook failed", err),
85
+ response,
86
+ };
87
+ }
88
+ }
89
+ if (!response.ok) {
90
+ return {
91
+ ok: false,
92
+ error: makeClientError("http", `Request failed with status ${response.status}`, undefined, response.status),
93
+ response,
94
+ };
95
+ }
96
+ const bodyResult = await safeResponseBody(response);
97
+ if (!bodyResult.ok) {
98
+ return {
99
+ ok: false,
100
+ error: makeClientError("parse", "Failed to parse response body", bodyResult.error),
101
+ response,
102
+ };
103
+ }
104
+ const result = bodyResult.result;
105
+ return { ok: true, result, response };
106
+ };
107
+ return {
108
+ get: (path, ...args) => request("get", path, args[0]),
109
+ post: (path, ...args) => request("post", path, args[0]),
110
+ put: (path, ...args) => request("put", path, args[0]),
111
+ patch: (path, ...args) => request("patch", path, args[0]),
112
+ delete: (path, ...args) => request("delete", path, args[0]),
113
+ };
114
+ }
@@ -0,0 +1,80 @@
1
+ export type EndpointsMap = {
2
+ get: Record<PropertyKey, unknown>;
3
+ post: Record<PropertyKey, unknown>;
4
+ put: Record<PropertyKey, unknown>;
5
+ patch: Record<PropertyKey, unknown>;
6
+ delete: Record<PropertyKey, unknown>;
7
+ };
8
+ export type EndpointOf<Endpoints extends EndpointsMap, Method extends keyof EndpointsMap, Path extends keyof EndpointsMap[Method]> = Endpoints[Method][Path];
9
+ export type EndpointParams<E> = E extends {
10
+ params: infer P;
11
+ } ? P : never;
12
+ export type EndpointBody<E> = E extends {
13
+ body: infer B;
14
+ } ? B : never;
15
+ export type EndpointResponses<E> = E extends {
16
+ responses: infer R;
17
+ } ? R : never;
18
+ export type EndpointResult<E> = [EndpointResponses<E>] extends [never] ? unknown : EndpointResponses<E>[keyof EndpointResponses<E>];
19
+ export type ErrorKind = "network" | "timeout" | "aborted" | "http" | "parse" | "hook";
20
+ export type ClientError = {
21
+ kind: ErrorKind;
22
+ message: string;
23
+ cause?: unknown;
24
+ status?: number;
25
+ };
26
+ export type FetchResult<R, E = ClientError> = {
27
+ ok: true;
28
+ result: R;
29
+ response: Response;
30
+ } | {
31
+ ok: false;
32
+ error: E;
33
+ response?: Response;
34
+ };
35
+ export type Result<R, E = string> = {
36
+ ok: true;
37
+ result: R;
38
+ } | {
39
+ ok: false;
40
+ error: E;
41
+ };
42
+ export type MaybePromise<T> = T | Promise<T>;
43
+ export type RequestOptions = {
44
+ signal?: AbortSignal;
45
+ timeoutMs?: number;
46
+ headers?: RequestInit["headers"];
47
+ };
48
+ export type RequestInput<E> = RequestOptions & ([EndpointParams<E>] extends [never] ? {} : {
49
+ params?: EndpointParams<E>;
50
+ }) & ([EndpointBody<E>] extends [never] ? {} : {
51
+ body: EndpointBody<E>;
52
+ });
53
+ export type RequestArgs<E> = [EndpointBody<E>] extends [never] ? [input?: RequestInput<E>] : [input: RequestInput<E>];
54
+ export type Api<Endpoints extends EndpointsMap> = {
55
+ [Method in keyof EndpointsMap]: <Path extends Extract<keyof Endpoints[Method], string>>(path: Path, ...args: RequestArgs<EndpointOf<Endpoints, Method, Path>>) => Promise<FetchResult<EndpointResult<EndpointOf<Endpoints, Method, Path>>>>;
56
+ };
57
+ export type RequestHookContext = {
58
+ method: keyof EndpointsMap;
59
+ path: string;
60
+ init: RequestInit;
61
+ };
62
+ export type ResponseHookContext = {
63
+ method: keyof EndpointsMap;
64
+ path: string;
65
+ response: Response;
66
+ durationMs: number;
67
+ };
68
+ export type ResponseInterceptorContext = {
69
+ method: keyof EndpointsMap;
70
+ path: string;
71
+ response: Response;
72
+ };
73
+ export type ClientHooks = {
74
+ onRequest?: (context: RequestHookContext) => MaybePromise<void>;
75
+ onResponse?: (context: ResponseHookContext) => MaybePromise<void>;
76
+ responseInterceptor?: (context: ResponseInterceptorContext) => MaybePromise<Response>;
77
+ };
78
+ export type ClientConfig = ClientHooks & {
79
+ baseUrl?: string;
80
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { type ClientError, type ErrorKind, type RequestInput, type RequestOptions, type Result } from "./types.js";
2
+ export declare const HTTP_METHOD: {
3
+ readonly get: "GET";
4
+ readonly post: "POST";
5
+ readonly put: "PUT";
6
+ readonly patch: "PATCH";
7
+ readonly delete: "DELETE";
8
+ };
9
+ export declare function makeClientError(kind: ErrorKind, message: string, cause?: unknown, status?: number): ClientError;
10
+ export declare function createFetchSignal(options: RequestOptions): {
11
+ signal?: AbortSignal;
12
+ cleanup: () => void;
13
+ didTimeout: () => boolean;
14
+ };
15
+ export declare function buildPath(path: string, params: unknown): string;
16
+ export declare function resolveRequestUrl(path: string, baseUrl?: string): string;
17
+ export declare function createBodyAndHeaders(input: RequestInput<unknown>): {
18
+ body?: NonNullable<RequestInit["body"]>;
19
+ headers: Headers;
20
+ };
21
+ export declare function safeResponseBody(response: Response): Promise<Result<unknown, unknown>>;
package/dist/utils.js ADDED
@@ -0,0 +1,139 @@
1
+ import {} from "./types.js";
2
+ export const HTTP_METHOD = {
3
+ get: "GET",
4
+ post: "POST",
5
+ put: "PUT",
6
+ patch: "PATCH",
7
+ delete: "DELETE",
8
+ };
9
+ export function makeClientError(kind, message, cause, status) {
10
+ return { kind, message, cause, status };
11
+ }
12
+ export function createFetchSignal(options) {
13
+ const { signal, timeoutMs } = options;
14
+ if (timeoutMs == null) {
15
+ return { signal, cleanup: () => { }, didTimeout: () => false };
16
+ }
17
+ let timedOut = false;
18
+ const controller = new AbortController();
19
+ const onAbort = () => {
20
+ controller.abort(signal?.reason ?? new DOMException("The operation was aborted", "AbortError"));
21
+ };
22
+ if (signal?.aborted) {
23
+ onAbort();
24
+ }
25
+ else if (signal) {
26
+ signal.addEventListener("abort", onAbort, { once: true });
27
+ }
28
+ const timeoutId = setTimeout(() => {
29
+ timedOut = true;
30
+ controller.abort(new DOMException(`Timed out after ${timeoutMs}ms`, "TimeoutError"));
31
+ }, timeoutMs);
32
+ const cleanup = () => {
33
+ clearTimeout(timeoutId);
34
+ if (signal) {
35
+ signal.removeEventListener("abort", onAbort);
36
+ }
37
+ };
38
+ return { signal: controller.signal, cleanup, didTimeout: () => timedOut };
39
+ }
40
+ function isRecord(value) {
41
+ return typeof value === "object" && value !== null;
42
+ }
43
+ function isAbsoluteUrl(path) {
44
+ return /^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(path) || path.startsWith("//");
45
+ }
46
+ function appendQueryToUrl(path, query) {
47
+ const absolute = isAbsoluteUrl(path);
48
+ const url = absolute ? new URL(path) : new URL(path, "http://localhost");
49
+ for (const [key, value] of Object.entries(query)) {
50
+ if (value == null) {
51
+ continue;
52
+ }
53
+ if (Array.isArray(value)) {
54
+ for (const item of value) {
55
+ if (item != null) {
56
+ url.searchParams.append(key, String(item));
57
+ }
58
+ }
59
+ continue;
60
+ }
61
+ url.searchParams.set(key, String(value));
62
+ }
63
+ if (absolute) {
64
+ return url.toString();
65
+ }
66
+ return `${url.pathname}${url.search}`;
67
+ }
68
+ export function buildPath(path, params) {
69
+ if (!isRecord(params)) {
70
+ return path;
71
+ }
72
+ let nextPath = path;
73
+ if (isRecord(params.path)) {
74
+ for (const [key, value] of Object.entries(params.path)) {
75
+ if (value != null) {
76
+ nextPath = nextPath.replaceAll("{" + key + "}", encodeURIComponent(String(value)));
77
+ }
78
+ }
79
+ }
80
+ if (isRecord(params.query)) {
81
+ return appendQueryToUrl(nextPath, params.query);
82
+ }
83
+ return nextPath;
84
+ }
85
+ export function resolveRequestUrl(path, baseUrl) {
86
+ if (!baseUrl || isAbsoluteUrl(path)) {
87
+ return path;
88
+ }
89
+ return new URL(path, baseUrl).toString();
90
+ }
91
+ function isRawBody(body) {
92
+ return (typeof body === "string"
93
+ || body instanceof Blob
94
+ || body instanceof FormData
95
+ || body instanceof URLSearchParams
96
+ || body instanceof ArrayBuffer
97
+ || ArrayBuffer.isView(body)
98
+ || body instanceof ReadableStream);
99
+ }
100
+ export function createBodyAndHeaders(input) {
101
+ const headers = new Headers(input.headers);
102
+ const body = "body" in input ? input.body : undefined;
103
+ if (body == null) {
104
+ return { body: undefined, headers };
105
+ }
106
+ if (isRawBody(body)) {
107
+ return { body, headers };
108
+ }
109
+ if (!headers.has("content-type")) {
110
+ headers.set("content-type", "application/json");
111
+ }
112
+ return { body: JSON.stringify(body), headers };
113
+ }
114
+ function isJsonContentType(contentType) {
115
+ return (contentType.includes("application/json")
116
+ || contentType.includes("application/problem+json")
117
+ || contentType.includes("+json"));
118
+ }
119
+ function isTextContentType(contentType) {
120
+ return contentType.startsWith("text/");
121
+ }
122
+ export async function safeResponseBody(response) {
123
+ const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
124
+ if (response.status === 204 || response.status === 205 || response.status === 304) {
125
+ return { ok: true, result: null };
126
+ }
127
+ try {
128
+ if (isJsonContentType(contentType)) {
129
+ return { ok: true, result: await response.json() };
130
+ }
131
+ if (isTextContentType(contentType)) {
132
+ return { ok: true, result: await response.text() };
133
+ }
134
+ return { ok: true, result: await response.blob() };
135
+ }
136
+ catch (err) {
137
+ return { ok: false, error: err };
138
+ }
139
+ }
package/package.json CHANGED
@@ -1,16 +1,32 @@
1
1
  {
2
2
  "name": "kayto_ts",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Type-safe HTTP client for working with kayto-generated endpoint schemas.",
5
5
  "repository": "https://github.com/vladislav-yemelyanov/kayto_ts",
6
6
  "homepage": "https://www.npmjs.com/package/kayto_ts",
7
- "module": "src/index.ts",
8
7
  "type": "module",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
9
22
  "scripts": {
23
+ "build": "tsc -p tsconfig.build.json",
24
+ "prepublishOnly": "npm run build",
10
25
  "test": "bun test"
11
26
  },
12
27
  "devDependencies": {
13
- "@types/bun": "latest"
28
+ "@types/bun": "latest",
29
+ "typescript": "^5.9.3"
14
30
  },
15
31
  "peerDependencies": {
16
32
  "typescript": "^5"
package/bun.lock DELETED
@@ -1,26 +0,0 @@
1
- {
2
- "lockfileVersion": 1,
3
- "configVersion": 1,
4
- "workspaces": {
5
- "": {
6
- "name": "kayto_ts",
7
- "devDependencies": {
8
- "@types/bun": "latest",
9
- },
10
- "peerDependencies": {
11
- "typescript": "^5",
12
- },
13
- },
14
- },
15
- "packages": {
16
- "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
17
-
18
- "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
19
-
20
- "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
21
-
22
- "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
23
-
24
- "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
25
- }
26
- }
package/publish.sh DELETED
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- cd "$(dirname "$0")"
5
-
6
- npm version patch --no-git-tag-version
7
- git add .
8
- git commit -m "upgrade"
9
- git push
10
- npm publish
package/src/index.ts DELETED
@@ -1,177 +0,0 @@
1
- import {
2
- type Api,
3
- type ClientConfig,
4
- type EndpointsMap,
5
- type EndpointOf,
6
- type FetchResult,
7
- type RequestInput,
8
- } from "./types";
9
- import {
10
- buildPath,
11
- createBodyAndHeaders,
12
- createFetchSignal,
13
- HTTP_METHOD,
14
- makeClientError,
15
- resolveRequestUrl,
16
- safeResponseBody,
17
- } from "./utils";
18
-
19
- export type {
20
- Api,
21
- ClientConfig,
22
- ClientError,
23
- ClientHooks,
24
- EndpointsMap,
25
- EndpointOf,
26
- FetchResult,
27
- RequestInput,
28
- RequestOptions,
29
- } from "./types";
30
-
31
- export function clientApi<Endpoints extends EndpointsMap>(
32
- config: ClientConfig = {},
33
- ): Api<Endpoints> {
34
- const { baseUrl, onRequest, onResponse, responseInterceptor } = config;
35
-
36
- const request = async <
37
- Method extends keyof EndpointsMap,
38
- Path extends Extract<keyof Endpoints[Method], string>,
39
- >(
40
- method: Method,
41
- path: Path,
42
- input?: RequestInput<EndpointOf<Endpoints, Method, Path>>,
43
- ): Promise<FetchResult<EndpointOf<Endpoints, Method, Path>>> => {
44
- const requestInput =
45
- input ?? ({} as RequestInput<EndpointOf<Endpoints, Method, Path>>);
46
- const { signal, cleanup, didTimeout } = createFetchSignal(requestInput);
47
- const builtPath = buildPath(
48
- path,
49
- (requestInput as { params?: unknown }).params,
50
- );
51
- const resolvedPath = resolveRequestUrl(builtPath, baseUrl);
52
- const { body, headers } = createBodyAndHeaders(
53
- requestInput as RequestInput<unknown>,
54
- );
55
- const init: RequestInit = {
56
- method: HTTP_METHOD[method],
57
- signal,
58
- headers,
59
- body,
60
- };
61
-
62
- if (onRequest) {
63
- try {
64
- await onRequest({ method, path: resolvedPath, init });
65
- } catch (err) {
66
- cleanup();
67
- return {
68
- ok: false,
69
- error: makeClientError("hook", "onRequest hook failed", err),
70
- };
71
- }
72
- }
73
-
74
- const startedAt = Date.now();
75
-
76
- let rawResponse: Response;
77
-
78
- try {
79
- rawResponse = await fetch(resolvedPath, init);
80
- } catch (err) {
81
- if (didTimeout()) {
82
- return {
83
- ok: false,
84
- error: makeClientError("timeout", "Request timed out", err),
85
- };
86
- }
87
-
88
- if (err instanceof DOMException && err.name === "AbortError") {
89
- return {
90
- ok: false,
91
- error: makeClientError("aborted", "Request was aborted", err),
92
- };
93
- }
94
-
95
- return {
96
- ok: false,
97
- error: makeClientError("network", "Network error before response", err),
98
- };
99
- } finally {
100
- cleanup();
101
- }
102
-
103
- let response = rawResponse;
104
-
105
- if (responseInterceptor) {
106
- try {
107
- response = await responseInterceptor({
108
- method,
109
- path: resolvedPath,
110
- response: rawResponse,
111
- });
112
- } catch (err) {
113
- return {
114
- ok: false,
115
- error: makeClientError("hook", "responseInterceptor failed", err),
116
- response: rawResponse,
117
- };
118
- }
119
- }
120
-
121
- if (onResponse) {
122
- try {
123
- await onResponse({
124
- method,
125
- path: resolvedPath,
126
- response,
127
- durationMs: Date.now() - startedAt,
128
- });
129
- } catch (err) {
130
- return {
131
- ok: false,
132
- error: makeClientError("hook", "onResponse hook failed", err),
133
- response,
134
- };
135
- }
136
- }
137
-
138
- if (!response.ok) {
139
- return {
140
- ok: false,
141
- error: makeClientError(
142
- "http",
143
- `Request failed with status ${response.status}`,
144
- undefined,
145
- response.status,
146
- ),
147
- response,
148
- };
149
- }
150
-
151
- const bodyResult = await safeResponseBody(response);
152
-
153
- if (!bodyResult.ok) {
154
- return {
155
- ok: false,
156
- error: makeClientError(
157
- "parse",
158
- "Failed to parse response body",
159
- bodyResult.error,
160
- ),
161
- response,
162
- };
163
- }
164
-
165
- const result = bodyResult.result as EndpointOf<Endpoints, Method, Path>;
166
-
167
- return { ok: true, result, response };
168
- };
169
-
170
- return {
171
- get: (path, ...args) => request("get", path, args[0]),
172
- post: (path, ...args) => request("post", path, args[0]),
173
- put: (path, ...args) => request("put", path, args[0]),
174
- patch: (path, ...args) => request("patch", path, args[0]),
175
- delete: (path, ...args) => request("delete", path, args[0]),
176
- };
177
- }
package/src/types.ts DELETED
@@ -1,95 +0,0 @@
1
- export type EndpointsMap = {
2
- get: Record<PropertyKey, unknown>;
3
- post: Record<PropertyKey, unknown>;
4
- put: Record<PropertyKey, unknown>;
5
- patch: Record<PropertyKey, unknown>;
6
- delete: Record<PropertyKey, unknown>;
7
- };
8
-
9
- export type EndpointOf<
10
- Endpoints extends EndpointsMap,
11
- Method extends keyof EndpointsMap,
12
- Path extends keyof EndpointsMap[Method],
13
- > = Endpoints[Method][Path];
14
-
15
- export type EndpointParams<E> = E extends { params: infer P } ? P : never;
16
- export type EndpointBody<E> = E extends { body: infer B } ? B : never;
17
-
18
- export type ErrorKind =
19
- | "network"
20
- | "timeout"
21
- | "aborted"
22
- | "http"
23
- | "parse"
24
- | "hook";
25
-
26
- export type ClientError = {
27
- kind: ErrorKind;
28
- message: string;
29
- cause?: unknown;
30
- status?: number;
31
- };
32
-
33
- export type FetchResult<R, E = ClientError> =
34
- | { ok: true; result: R; response: Response }
35
- | { ok: false; error: E; response?: Response };
36
-
37
- export type Result<R, E = string> =
38
- | { ok: true; result: R }
39
- | { ok: false; error: E };
40
-
41
- export type MaybePromise<T> = T | Promise<T>;
42
-
43
- export type RequestOptions = {
44
- signal?: AbortSignal;
45
- timeoutMs?: number;
46
- headers?: RequestInit["headers"];
47
- };
48
-
49
- export type RequestInput<E> = RequestOptions
50
- & ([EndpointParams<E>] extends [never] ? {} : { params?: EndpointParams<E> })
51
- & ([EndpointBody<E>] extends [never] ? {} : { body: EndpointBody<E> });
52
-
53
- export type RequestArgs<E> = [EndpointBody<E>] extends [never]
54
- ? [input?: RequestInput<E>]
55
- : [input: RequestInput<E>];
56
-
57
- export type Api<Endpoints extends EndpointsMap> = {
58
- [Method in keyof EndpointsMap]: <
59
- Path extends Extract<keyof Endpoints[Method], string>,
60
- >(
61
- path: Path,
62
- ...args: RequestArgs<EndpointOf<Endpoints, Method, Path>>
63
- ) => Promise<FetchResult<EndpointOf<Endpoints, Method, Path>>>;
64
- };
65
-
66
- export type RequestHookContext = {
67
- method: keyof EndpointsMap;
68
- path: string;
69
- init: RequestInit;
70
- };
71
-
72
- export type ResponseHookContext = {
73
- method: keyof EndpointsMap;
74
- path: string;
75
- response: Response;
76
- durationMs: number;
77
- };
78
-
79
- export type ResponseInterceptorContext = {
80
- method: keyof EndpointsMap;
81
- path: string;
82
- response: Response;
83
- };
84
-
85
- export type ClientHooks = {
86
- onRequest?: (context: RequestHookContext) => MaybePromise<void>;
87
- onResponse?: (context: ResponseHookContext) => MaybePromise<void>;
88
- responseInterceptor?: (
89
- context: ResponseInterceptorContext,
90
- ) => MaybePromise<Response>;
91
- };
92
-
93
- export type ClientConfig = ClientHooks & {
94
- baseUrl?: string;
95
- };
package/src/utils.ts DELETED
@@ -1,201 +0,0 @@
1
- import { type ClientError, type ErrorKind, type RequestInput, type RequestOptions, type Result } from "./types";
2
-
3
- export const HTTP_METHOD = {
4
- get: "GET",
5
- post: "POST",
6
- put: "PUT",
7
- patch: "PATCH",
8
- delete: "DELETE",
9
- } as const;
10
-
11
- export function makeClientError(
12
- kind: ErrorKind,
13
- message: string,
14
- cause?: unknown,
15
- status?: number,
16
- ): ClientError {
17
- return { kind, message, cause, status };
18
- }
19
-
20
- export function createFetchSignal(options: RequestOptions): {
21
- signal?: AbortSignal;
22
- cleanup: () => void;
23
- didTimeout: () => boolean;
24
- } {
25
- const { signal, timeoutMs } = options;
26
-
27
- if (timeoutMs == null) {
28
- return { signal, cleanup: () => {}, didTimeout: () => false };
29
- }
30
-
31
- let timedOut = false;
32
- const controller = new AbortController();
33
- const onAbort = () => {
34
- controller.abort(
35
- signal?.reason ?? new DOMException("The operation was aborted", "AbortError"),
36
- );
37
- };
38
-
39
- if (signal?.aborted) {
40
- onAbort();
41
- } else if (signal) {
42
- signal.addEventListener("abort", onAbort, { once: true });
43
- }
44
-
45
- const timeoutId = setTimeout(() => {
46
- timedOut = true;
47
- controller.abort(
48
- new DOMException(`Timed out after ${timeoutMs}ms`, "TimeoutError"),
49
- );
50
- }, timeoutMs);
51
-
52
- const cleanup = () => {
53
- clearTimeout(timeoutId);
54
-
55
- if (signal) {
56
- signal.removeEventListener("abort", onAbort);
57
- }
58
- };
59
-
60
- return { signal: controller.signal, cleanup, didTimeout: () => timedOut };
61
- }
62
-
63
- function isRecord(value: unknown): value is Record<string, unknown> {
64
- return typeof value === "object" && value !== null;
65
- }
66
-
67
- function isAbsoluteUrl(path: string): boolean {
68
- return /^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(path) || path.startsWith("//");
69
- }
70
-
71
- function appendQueryToUrl(path: string, query: Record<string, unknown>): string {
72
- const absolute = isAbsoluteUrl(path);
73
- const url = absolute ? new URL(path) : new URL(path, "http://localhost");
74
-
75
- for (const [key, value] of Object.entries(query)) {
76
- if (value == null) {
77
- continue;
78
- }
79
-
80
- if (Array.isArray(value)) {
81
- for (const item of value) {
82
- if (item != null) {
83
- url.searchParams.append(key, String(item));
84
- }
85
- }
86
-
87
- continue;
88
- }
89
-
90
- url.searchParams.set(key, String(value));
91
- }
92
-
93
- if (absolute) {
94
- return url.toString();
95
- }
96
-
97
- return `${url.pathname}${url.search}`;
98
- }
99
-
100
- export function buildPath(path: string, params: unknown): string {
101
- if (!isRecord(params)) {
102
- return path;
103
- }
104
-
105
- let nextPath = path;
106
-
107
- if (isRecord(params.path)) {
108
- for (const [key, value] of Object.entries(params.path)) {
109
- if (value != null) {
110
- nextPath = nextPath.replaceAll(
111
- "{" + key + "}",
112
- encodeURIComponent(String(value)),
113
- );
114
- }
115
- }
116
- }
117
-
118
- if (isRecord(params.query)) {
119
- return appendQueryToUrl(nextPath, params.query);
120
- }
121
-
122
- return nextPath;
123
- }
124
-
125
- export function resolveRequestUrl(path: string, baseUrl?: string): string {
126
- if (!baseUrl || isAbsoluteUrl(path)) {
127
- return path;
128
- }
129
-
130
- return new URL(path, baseUrl).toString();
131
- }
132
-
133
- function isRawBody(body: unknown): body is NonNullable<RequestInit["body"]> {
134
- return (
135
- typeof body === "string"
136
- || body instanceof Blob
137
- || body instanceof FormData
138
- || body instanceof URLSearchParams
139
- || body instanceof ArrayBuffer
140
- || ArrayBuffer.isView(body)
141
- || body instanceof ReadableStream
142
- );
143
- }
144
-
145
- export function createBodyAndHeaders(input: RequestInput<unknown>): {
146
- body?: NonNullable<RequestInit["body"]>;
147
- headers: Headers;
148
- } {
149
- const headers = new Headers(input.headers);
150
- const body = "body" in input ? (input as { body?: unknown }).body : undefined;
151
-
152
- if (body == null) {
153
- return { body: undefined, headers };
154
- }
155
-
156
- if (isRawBody(body)) {
157
- return { body, headers };
158
- }
159
-
160
- if (!headers.has("content-type")) {
161
- headers.set("content-type", "application/json");
162
- }
163
-
164
- return { body: JSON.stringify(body), headers };
165
- }
166
-
167
- function isJsonContentType(contentType: string): boolean {
168
- return (
169
- contentType.includes("application/json")
170
- || contentType.includes("application/problem+json")
171
- || contentType.includes("+json")
172
- );
173
- }
174
-
175
- function isTextContentType(contentType: string): boolean {
176
- return contentType.startsWith("text/");
177
- }
178
-
179
- export async function safeResponseBody(
180
- response: Response,
181
- ): Promise<Result<unknown, unknown>> {
182
- const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
183
-
184
- if (response.status === 204 || response.status === 205 || response.status === 304) {
185
- return { ok: true, result: null };
186
- }
187
-
188
- try {
189
- if (isJsonContentType(contentType)) {
190
- return { ok: true, result: await response.json() };
191
- }
192
-
193
- if (isTextContentType(contentType)) {
194
- return { ok: true, result: await response.text() };
195
- }
196
-
197
- return { ok: true, result: await response.blob() };
198
- } catch (err) {
199
- return { ok: false, error: err };
200
- }
201
- }
package/tsconfig.json DELETED
@@ -1,29 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- // Environment setup & latest features
4
- "lib": ["ESNext"],
5
- "target": "ESNext",
6
- "module": "Preserve",
7
- "moduleDetection": "force",
8
- "jsx": "react-jsx",
9
- "allowJs": true,
10
-
11
- // Bundler mode
12
- "moduleResolution": "bundler",
13
- "allowImportingTsExtensions": true,
14
- "verbatimModuleSyntax": true,
15
- "noEmit": true,
16
-
17
- // Best practices
18
- "strict": true,
19
- "skipLibCheck": true,
20
- "noFallthroughCasesInSwitch": true,
21
- "noUncheckedIndexedAccess": true,
22
- "noImplicitOverride": true,
23
-
24
- // Some stricter flags (disabled by default)
25
- "noUnusedLocals": false,
26
- "noUnusedParameters": false,
27
- "noPropertyAccessFromIndexSignature": false
28
- }
29
- }