@zap-studio/fetch 0.1.0 → 0.1.2
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/CHANGELOG.md +12 -0
- package/README.md +169 -0
- package/dist/errors/index.d.mts +10 -0
- package/dist/errors/index.d.mts.map +1 -0
- package/dist/errors/index.mjs +3 -0
- package/dist/errors-BCMOymYz.mjs +17 -0
- package/dist/errors-BCMOymYz.mjs.map +1 -0
- package/dist/index-Dz58_HD6.d.mts +18 -0
- package/dist/index-Dz58_HD6.d.mts.map +1 -0
- package/dist/index.d.mts +60 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +86 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types/index.d.mts +2 -0
- package/dist/types/index.mjs +1 -0
- package/dist/utils/index.d.mts +11 -0
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +4 -0
- package/dist/utils-CZ-1Z2-1.mjs +53 -0
- package/dist/utils-CZ-1Z2-1.mjs.map +1 -0
- package/package.json +16 -9
- package/src/errors/index.ts +0 -11
- package/src/index.ts +0 -126
- package/src/types/index.ts +0 -23
- package/src/utils/index.ts +0 -104
- package/tests/index.test.ts +0 -974
- package/tests/utils/index.test.ts +0 -355
- package/tsconfig.json +0 -5
- package/tsdown.config.ts +0 -3
- package/vitest.config.ts +0 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @zap-studio/fetch
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 69c2b21: Change `safeFetch` to `$fetch` syntax and make sure `safeFetch` can also be used for legacy
|
|
8
|
+
|
|
9
|
+
## 0.1.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 5f1812b: Change files field in package.json to distribute only necessary artifacts
|
|
14
|
+
|
|
3
15
|
## 0.1.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# @zap-studio/fetch
|
|
2
|
+
|
|
3
|
+
A type-safe fetch wrapper with Zod validation for TypeScript.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎯 **Type-safe requests** with automatic type inference
|
|
8
|
+
- 🛡️ **Runtime validation** using Zod schemas
|
|
9
|
+
- 🔄 **Automatic content-type** detection and handling
|
|
10
|
+
- ⚡️ **Convenient API methods** (GET, POST, PUT, PATCH, DELETE)
|
|
11
|
+
- 📦 **Multiple response types** (JSON, text, blob, arrayBuffer)
|
|
12
|
+
- 🚨 **Custom error handling** with FetchError class
|
|
13
|
+
- 📘 **Full TypeScript support** with zero configuration
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @zap-studio/fetch
|
|
19
|
+
#or
|
|
20
|
+
npm install @zap-studio/fetch
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import { api } from "@zap-studio/fetch";
|
|
28
|
+
|
|
29
|
+
// Define your schema
|
|
30
|
+
const UserSchema = z.object({
|
|
31
|
+
id: z.number(),
|
|
32
|
+
name: z.string(),
|
|
33
|
+
email: z.email(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Make a type-safe request
|
|
37
|
+
const user = await api.get(
|
|
38
|
+
"https://api.example.com/users/1",
|
|
39
|
+
UserSchema
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// user is fully typed and validated! ✨
|
|
43
|
+
console.log(user.name); // TypeScript knows this is a string
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## API
|
|
47
|
+
|
|
48
|
+
### `api.get(url, schema, options?)`
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const user = await api.get("/api/users/1", UserSchema);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `api.post(url, schema, body?, options?)`
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const newUser = await api.post("/api/users", UserSchema, {
|
|
58
|
+
name: "John Doe",
|
|
59
|
+
email: "john@example.com",
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### `api.put(url, schema, body?, options?)`
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const updated = await api.put("/api/users/1", UserSchema, {
|
|
67
|
+
name: "Jane Doe",
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `api.patch(url, schema, body?, options?)`
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const patched = await api.patch("/api/users/1", UserSchema, {
|
|
75
|
+
email: "newemail@example.com",
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `api.delete(url, schema, options?)`
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
const deleted = await api.delete("/api/users/1", UserSchema);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Advanced Usage
|
|
86
|
+
|
|
87
|
+
### Using `$fetch` directly
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { $fetch } from "@zap-studio/fetch";
|
|
91
|
+
|
|
92
|
+
const result = await $fetch(
|
|
93
|
+
"https://api.example.com/users/1",
|
|
94
|
+
UserSchema,
|
|
95
|
+
{
|
|
96
|
+
method: "GET",
|
|
97
|
+
headers: {
|
|
98
|
+
"Authorization": "Bearer token",
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Custom response types
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// Get text response
|
|
108
|
+
const text = await $fetch(
|
|
109
|
+
"/api/data",
|
|
110
|
+
z.string(),
|
|
111
|
+
{ responseType: "text" }
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Get blob response
|
|
115
|
+
const blob = await $fetch(
|
|
116
|
+
"/api/file",
|
|
117
|
+
z.instanceof(Blob),
|
|
118
|
+
{ responseType: "blob" }
|
|
119
|
+
);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Error handling
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { FetchError } from "@zap-studio/fetch/errors";
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const user = await api.get("/api/users/1", UserSchema);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error instanceof FetchError) {
|
|
131
|
+
console.error(`HTTP ${error.status}: ${error.statusText}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Flexible validation
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Throw on validation error (default)
|
|
140
|
+
const user = await $fetch(url, UserSchema, {
|
|
141
|
+
throwOnValidationError: true,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Return validation result without throwing
|
|
145
|
+
const result = await $fetch(url, UserSchema, {
|
|
146
|
+
throwOnValidationError: false,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (result.success) {
|
|
150
|
+
console.log(result.data);
|
|
151
|
+
} else {
|
|
152
|
+
console.error(result.error);
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Why @zap-studio/fetch?
|
|
157
|
+
|
|
158
|
+
**Before:**
|
|
159
|
+
```typescript
|
|
160
|
+
const response = await fetch("/api/users/1");
|
|
161
|
+
const data = await response.json();
|
|
162
|
+
const user = data as User; // 😱 Unsafe type assertion
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**After:**
|
|
166
|
+
```typescript
|
|
167
|
+
const user = await api.get("/api/users/1", UserSchema);
|
|
168
|
+
// ✨ Typed, validated, and safe!
|
|
169
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/errors/index.d.ts
|
|
2
|
+
declare class FetchError extends Error {
|
|
3
|
+
status: number;
|
|
4
|
+
statusText: string;
|
|
5
|
+
response: Response;
|
|
6
|
+
constructor(message: string, status: number, statusText: string, response: Response);
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { FetchError };
|
|
10
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/errors/index.ts"],"sourcesContent":[],"mappings":";cAAa,UAAA,SAAmB,KAAA;EAAnB,MAAA,EAAA,MAAW;EAGZ,UAAA,EAAA,MAAA;EAME,QAAA,EANF,QAME;EATkB,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,EAAA,UAAA,EAAA,MAAA,EAAA,QAAA,EASlB,QATkB"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/errors/index.ts
|
|
2
|
+
var FetchError = class extends Error {
|
|
3
|
+
status;
|
|
4
|
+
statusText;
|
|
5
|
+
response;
|
|
6
|
+
constructor(message, status, statusText, response) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "FetchError";
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.statusText = statusText;
|
|
11
|
+
this.response = response;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { FetchError as t };
|
|
17
|
+
//# sourceMappingURL=errors-BCMOymYz.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors-BCMOymYz.mjs","names":[],"sources":["../src/errors/index.ts"],"sourcesContent":["export class FetchError extends Error {\n status: number;\n statusText: string;\n response: Response;\n\n constructor(\n message: string,\n status: number,\n statusText: string,\n response: Response\n ) {\n super(message);\n this.name = \"FetchError\";\n this.status = status;\n this.statusText = statusText;\n this.response = response;\n }\n}\n"],"mappings":";AAAA,IAAa,aAAb,cAAgC,MAAM;CACpC;CACA;CACA;CAEA,YACE,SACA,QACA,YACA,UACA;AACA,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,SAAS;AACd,OAAK,aAAa;AAClB,OAAK,WAAW"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/types/index.d.ts
|
|
2
|
+
type FetchConfig<TBody> = Omit<RequestInit, "body"> & {
|
|
3
|
+
body?: TBody;
|
|
4
|
+
throwOnValidationError?: boolean;
|
|
5
|
+
};
|
|
6
|
+
type ResponseType = "arrayBuffer" | "blob" | "bytes" | "clone" | "formData" | "json" | "text";
|
|
7
|
+
type ResponseTypeMap = {
|
|
8
|
+
arrayBuffer: ArrayBuffer;
|
|
9
|
+
blob: Blob;
|
|
10
|
+
bytes: Uint8Array;
|
|
11
|
+
clone: Response;
|
|
12
|
+
formData: FormData;
|
|
13
|
+
json: unknown;
|
|
14
|
+
text: string;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { ResponseType as n, ResponseTypeMap as r, FetchConfig as t };
|
|
18
|
+
//# sourceMappingURL=index-Dz58_HD6.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-Dz58_HD6.d.mts","names":[],"sources":["../src/types/index.ts"],"sourcesContent":[],"mappings":";KAAY,qBAAqB,KAAK;EAA1B,IAAA,CAAA,EACH,KADG;EAA0B,sBAAA,CAAA,EAAA,OAAA;CAAL;AACxB,KAIG,YAAA,GAJH,aAAA,GAAA,MAAA,GAAA,OAAA,GAAA,OAAA,GAAA,UAAA,GAAA,MAAA,GAAA,MAAA;AAAK,KAaF,eAAA,GAbE;EAIF,WAAA,EAUG,WAVS;EASZ,IAAA,EAEJ,IAFI;EACG,KAAA,EAEN,UAFM;EACP,KAAA,EAEC,QAFD;EACC,QAAA,EAEG,QAFH;EACA,IAAA,EAAA,OAAA;EACG,IAAA,EAAA,MAAA;CAAQ"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { n as ResponseType, t as FetchConfig } from "./index-Dz58_HD6.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/index.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Type-safe fetch wrapper with Zod validation
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { z } from "zod";
|
|
11
|
+
* import { $fetch } from "@zap-studio/fetch";
|
|
12
|
+
*
|
|
13
|
+
* const UserSchema = z.object({
|
|
14
|
+
* id: z.number(),
|
|
15
|
+
* name: z.string(),
|
|
16
|
+
* email: z.string().email(),
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* async function getUser(userId: number) {
|
|
20
|
+
* const user = await $fetch(
|
|
21
|
+
* `https://api.example.com/users/${userId}`,
|
|
22
|
+
* UserSchema,
|
|
23
|
+
* { method: "GET" }
|
|
24
|
+
* );
|
|
25
|
+
* return user; // user is typed as { id: number; name: string; email: string; }
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
declare function $fetch<TResponse, TBody = unknown, TResponseType extends ResponseType = "json">(resource: string, responseSchema: z.ZodType<TResponse>, config?: FetchConfig<TBody> & {
|
|
29
|
+
throwOnValidationError?: boolean;
|
|
30
|
+
responseType?: TResponseType;
|
|
31
|
+
}): Promise<TResponse | z.ZodSafeParseSuccess<TResponse> | z.ZodSafeParseError<TResponse>>;
|
|
32
|
+
declare const safeFetch: typeof $fetch;
|
|
33
|
+
/**
|
|
34
|
+
* Convenience methods for common HTTP verbs
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* import { z } from "zod";
|
|
38
|
+
* import { api } from "@zap-studio/fetch";
|
|
39
|
+
*
|
|
40
|
+
* const PostSchema = z.object({
|
|
41
|
+
* id: z.number(),
|
|
42
|
+
* title: z.string(),
|
|
43
|
+
* content: z.string(),
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* async function fetchPost(postId: number) {
|
|
47
|
+
* const post = await api.get(`https://api.example.com/posts/${postId}`, PostSchema);
|
|
48
|
+
* return post; // post is typed as { id: number; title: string; content: string; }
|
|
49
|
+
* }
|
|
50
|
+
*/
|
|
51
|
+
declare const api: {
|
|
52
|
+
get: <TResponse>(resource: string, schema: z.ZodType<TResponse>, options?: Omit<RequestInit, "method" | "body">) => Promise<TResponse | z.ZodSafeParseSuccess<TResponse> | z.ZodSafeParseError<TResponse>>;
|
|
53
|
+
post: <TResponse, TBody = unknown>(resource: string, schema: z.ZodType<TResponse>, body?: TBody, options?: Omit<RequestInit, "method" | "body">) => Promise<TResponse | z.ZodSafeParseSuccess<TResponse> | z.ZodSafeParseError<TResponse>>;
|
|
54
|
+
put: <TResponse, TBody = unknown>(resource: string, schema: z.ZodType<TResponse>, body?: TBody, options?: Omit<RequestInit, "method" | "body">) => Promise<TResponse | z.ZodSafeParseSuccess<TResponse> | z.ZodSafeParseError<TResponse>>;
|
|
55
|
+
patch: <TResponse, TBody = unknown>(resource: string, schema: z.ZodType<TResponse>, body?: TBody, options?: Omit<RequestInit, "method" | "body">) => Promise<TResponse | z.ZodSafeParseSuccess<TResponse> | z.ZodSafeParseError<TResponse>>;
|
|
56
|
+
delete: <TResponse>(resource: string, schema: z.ZodType<TResponse>, options?: Omit<RequestInit, "method" | "body">) => Promise<TResponse | z.ZodSafeParseSuccess<TResponse> | z.ZodSafeParseError<TResponse>>;
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
export { $fetch, api, safeFetch };
|
|
60
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;AA2BA;;;;;;;;;;;;;;AA+CA;AAoBA;;;;;AAIkD,iBAvE5B,MAuE4B,CAAA,SAAA,EAAA,QAAA,OAAA,EAAA,sBApE1B,YAoE0B,GAAA,MAAA,CAAA,CAAA,QAAA,EAAA,MAAA,EAAA,cAAA,EAjEhC,CAAA,CAAE,OAiE8B,CAjEtB,SAiEsB,CAAA,EAAA,MAAA,CAAA,EAhEvC,WAgEuC,CAhE3B,KAgE2B,CAAA,GAAA;EAAA,sBAAA,CAAA,EAAA,OAAA;EAAA,YAAA,CAAA,EA9D/B,aA8D+B;CAAA,CAAA,EA5D/C,OA4D+C,CA3DhD,SA2DgD,GA3DpC,CAAA,CAAE,mBA2DkC,CA3Dd,SA2Dc,CAAA,GA3DD,CAAA,CAAE,iBA2DD,CA3DmB,SA2DnB,CAAA,CAAA;AAAA,cAxBrC,SAwBqC,EAAA,OAxB5B,MAwB4B;;;;;;;;;;;;;;;;;;;AAcA,cAlBrC,GAkBqC,EAAA;EAAA,GAAA,EAAA,CAAA,SAAA,CAAA,CAAA,QAAA,EAAA,MAAA,EAAA,MAAA,EAftC,CAAA,CAAE,OAeoC,CAf5B,SAe4B,CAAA,EAAA,OAAA,CAAA,EAdpC,IAcoC,CAd/B,WAc+B,EAAA,QAAA,GAAA,MAAA,CAAA,EAAA,GAdA,OAcA,CAdA,SAcA,GAdA,CAAA,CAAA,mBAcA,CAdA,SAcA,CAAA,GAdA,CAAA,CAAA,iBAcA,CAdA,SAcA,CAAA,CAAA;EAAA,IAAA,EAAA,CAAA,SAAA,EAAA,QAAA,OAAA,CAAA,CAAA,QAAA,EAAA,MAAA,EAAA,MAAA,EATtC,CAAA,CAAE,OASoC,CAT5B,SAS4B,CAAA,EAAA,IAAA,CAAA,EARvC,KAQuC,EAAA,OAAA,CAAA,EAPpC,IAOoC,CAP/B,WAO+B,EAAA,QAAA,GAAA,MAAA,CAAA,EAAA,GAPA,OAOA,CAPA,SAOA,GAPA,CAAA,CAAA,mBAOA,CAPA,SAOA,CAAA,GAPA,CAAA,CAAA,iBAOA,CAPA,SAOA,CAAA,CAAA;EAAA,GAAA,EAAA,CAAA,SAAA,EAAA,QAAA,OAAA,CAAA,CAAA,QAAA,EAAA,MAAA,EAAA,MAAA,EAFtC,CAAA,CAAE,OAEoC,CAF5B,SAE4B,CAAA,EAAA,IAAA,CAAA,EADvC,KACuC,EAAA,OAAA,CAAA,EAApC,IAAoC,CAA/B,WAA+B,EAAA,QAAA,GAAA,MAAA,CAAA,EAAA,GAAA,OAAA,CAAA,SAAA,GAAA,CAAA,CAAA,mBAAA,CAAA,SAAA,CAAA,GAAA,CAAA,CAAA,iBAAA,CAAA,SAAA,CAAA,CAAA;EAAA,KAAA,EAAA,CAAA,SAAA,EAAA,QAAA,OAAA,CAAA,CAAA,QAAA,EAAA,MAAA,EAAA,MAAA,EAKtC,CAAA,CAAE,OALoC,CAK5B,SAL4B,CAAA,EAAA,IAAA,CAAA,EAMvC,KANuC,EAAA,OAAA,CAAA,EAOpC,IAPoC,CAO/B,WAP+B,EAAA,QAAA,GAAA,MAAA,CAAA,EAAA,GAOA,OAPA,CAOA,SAPA,GAOA,CAAA,CAAA,mBAPA,CAOA,SAPA,CAAA,GAOA,CAAA,CAAA,iBAPA,CAOA,SAPA,CAAA,CAAA;EAK5B,MAAA,EAAA,CAAA,SAAA,CAAA,CAAA,QAAA,EAAA,MAAA,EAAA,MAAA,EAOV,CAAA,CAAE,OAPQ,CAOA,SAPA,CAAA,EAAA,OAAA,CAAA,EAQR,IARQ,CAQH,WARG,EAAA,QAAA,GAAA,MAAA,CAAA,EAAA,GAQ4B,OAR5B,CAQ4B,SAR5B,GAQ4B,CAAA,CAAA,mBAR5B,CAQ4B,SAR5B,CAAA,GAQ4B,CAAA,CAAA,iBAR5B,CAQ4B,SAR5B,CAAA,CAAA;CAAV"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { t as FetchError } from "./errors-BCMOymYz.mjs";
|
|
2
|
+
import { n as prepareHeadersAndBody, t as parseResponse } from "./utils-CZ-1Z2-1.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/index.ts
|
|
5
|
+
/**
|
|
6
|
+
* Type-safe fetch wrapper with Zod validation
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { z } from "zod";
|
|
10
|
+
* import { $fetch } from "@zap-studio/fetch";
|
|
11
|
+
*
|
|
12
|
+
* const UserSchema = z.object({
|
|
13
|
+
* id: z.number(),
|
|
14
|
+
* name: z.string(),
|
|
15
|
+
* email: z.string().email(),
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* async function getUser(userId: number) {
|
|
19
|
+
* const user = await $fetch(
|
|
20
|
+
* `https://api.example.com/users/${userId}`,
|
|
21
|
+
* UserSchema,
|
|
22
|
+
* { method: "GET" }
|
|
23
|
+
* );
|
|
24
|
+
* return user; // user is typed as { id: number; name: string; email: string; }
|
|
25
|
+
* }
|
|
26
|
+
*/
|
|
27
|
+
async function $fetch(resource, responseSchema, config) {
|
|
28
|
+
const { body, headers, throwOnValidationError = true, responseType = "json", ...rest } = config || {};
|
|
29
|
+
const { body: preparedBody, headers: preparedHeaders } = prepareHeadersAndBody(body, headers);
|
|
30
|
+
const response = await fetch(resource, {
|
|
31
|
+
...rest,
|
|
32
|
+
body: preparedBody,
|
|
33
|
+
headers: preparedHeaders
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) throw new FetchError(`HTTP ${response.status}: ${response.statusText}`, response.status, response.statusText, response);
|
|
36
|
+
const data = await parseResponse(response, responseType);
|
|
37
|
+
return throwOnValidationError ? responseSchema.parse(data) : responseSchema.safeParse(data);
|
|
38
|
+
}
|
|
39
|
+
const safeFetch = $fetch;
|
|
40
|
+
/**
|
|
41
|
+
* Convenience methods for common HTTP verbs
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* import { z } from "zod";
|
|
45
|
+
* import { api } from "@zap-studio/fetch";
|
|
46
|
+
*
|
|
47
|
+
* const PostSchema = z.object({
|
|
48
|
+
* id: z.number(),
|
|
49
|
+
* title: z.string(),
|
|
50
|
+
* content: z.string(),
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* async function fetchPost(postId: number) {
|
|
54
|
+
* const post = await api.get(`https://api.example.com/posts/${postId}`, PostSchema);
|
|
55
|
+
* return post; // post is typed as { id: number; title: string; content: string; }
|
|
56
|
+
* }
|
|
57
|
+
*/
|
|
58
|
+
const api = {
|
|
59
|
+
get: (resource, schema, options) => $fetch(resource, schema, {
|
|
60
|
+
...options,
|
|
61
|
+
method: "GET"
|
|
62
|
+
}),
|
|
63
|
+
post: (resource, schema, body, options) => $fetch(resource, schema, {
|
|
64
|
+
...options,
|
|
65
|
+
method: "POST",
|
|
66
|
+
body
|
|
67
|
+
}),
|
|
68
|
+
put: (resource, schema, body, options) => $fetch(resource, schema, {
|
|
69
|
+
...options,
|
|
70
|
+
method: "PUT",
|
|
71
|
+
body
|
|
72
|
+
}),
|
|
73
|
+
patch: (resource, schema, body, options) => $fetch(resource, schema, {
|
|
74
|
+
...options,
|
|
75
|
+
method: "PATCH",
|
|
76
|
+
body
|
|
77
|
+
}),
|
|
78
|
+
delete: (resource, schema, options) => $fetch(resource, schema, {
|
|
79
|
+
...options,
|
|
80
|
+
method: "DELETE"
|
|
81
|
+
})
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
export { $fetch, api, safeFetch };
|
|
86
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { z } from \"zod\";\nimport { FetchError } from \"./errors\";\nimport type { FetchConfig, ResponseType } from \"./types\";\nimport { parseResponse, prepareHeadersAndBody } from \"./utils\";\n\n/**\n * Type-safe fetch wrapper with Zod validation\n *\n * @example\n * import { z } from \"zod\";\n * import { $fetch } from \"@zap-studio/fetch\";\n *\n * const UserSchema = z.object({\n * id: z.number(),\n * name: z.string(),\n * email: z.string().email(),\n * });\n *\n * async function getUser(userId: number) {\n * const user = await $fetch(\n * `https://api.example.com/users/${userId}`,\n * UserSchema,\n * { method: \"GET\" }\n * );\n * return user; // user is typed as { id: number; name: string; email: string; }\n * }\n */\nexport async function $fetch<\n TResponse,\n TBody = unknown,\n TResponseType extends ResponseType = \"json\",\n>(\n resource: string,\n responseSchema: z.ZodType<TResponse>,\n config?: FetchConfig<TBody> & {\n throwOnValidationError?: boolean;\n responseType?: TResponseType;\n }\n): Promise<\n TResponse | z.ZodSafeParseSuccess<TResponse> | z.ZodSafeParseError<TResponse>\n> {\n const {\n body,\n headers,\n throwOnValidationError = true,\n responseType = \"json\" as TResponseType,\n ...rest\n } = config || {};\n\n const { body: preparedBody, headers: preparedHeaders } =\n prepareHeadersAndBody(body, headers);\n\n const response = await fetch(resource, {\n ...rest,\n body: preparedBody,\n headers: preparedHeaders,\n });\n\n if (!response.ok) {\n throw new FetchError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n response.statusText,\n response\n );\n }\n\n const data = await parseResponse(response, responseType);\n\n return throwOnValidationError\n ? responseSchema.parse(data)\n : responseSchema.safeParse(data);\n}\n\nexport const safeFetch = $fetch;\n\n/**\n * Convenience methods for common HTTP verbs\n *\n * @example\n * import { z } from \"zod\";\n * import { api } from \"@zap-studio/fetch\";\n *\n * const PostSchema = z.object({\n * id: z.number(),\n * title: z.string(),\n * content: z.string(),\n * });\n *\n * async function fetchPost(postId: number) {\n * const post = await api.get(`https://api.example.com/posts/${postId}`, PostSchema);\n * return post; // post is typed as { id: number; title: string; content: string; }\n * }\n */\nexport const api = {\n get: <TResponse>(\n resource: string,\n schema: z.ZodType<TResponse>,\n options?: Omit<RequestInit, \"method\" | \"body\">\n ) => $fetch(resource, schema, { ...options, method: \"GET\" }),\n\n post: <TResponse, TBody = unknown>(\n resource: string,\n schema: z.ZodType<TResponse>,\n body?: TBody,\n options?: Omit<RequestInit, \"method\" | \"body\">\n ) => $fetch(resource, schema, { ...options, method: \"POST\", body }),\n\n put: <TResponse, TBody = unknown>(\n resource: string,\n schema: z.ZodType<TResponse>,\n body?: TBody,\n options?: Omit<RequestInit, \"method\" | \"body\">\n ) => $fetch(resource, schema, { ...options, method: \"PUT\", body }),\n\n patch: <TResponse, TBody = unknown>(\n resource: string,\n schema: z.ZodType<TResponse>,\n body?: TBody,\n options?: Omit<RequestInit, \"method\" | \"body\">\n ) => $fetch(resource, schema, { ...options, method: \"PATCH\", body }),\n\n delete: <TResponse>(\n resource: string,\n schema: z.ZodType<TResponse>,\n options?: Omit<RequestInit, \"method\" | \"body\">\n ) => $fetch(resource, schema, { ...options, method: \"DELETE\" }),\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,eAAsB,OAKpB,UACA,gBACA,QAMA;CACA,MAAM,EACJ,MACA,SACA,yBAAyB,MACzB,eAAe,QACf,GAAG,SACD,UAAU,EAAE;CAEhB,MAAM,EAAE,MAAM,cAAc,SAAS,oBACnC,sBAAsB,MAAM,QAAQ;CAEtC,MAAM,WAAW,MAAM,MAAM,UAAU;EACrC,GAAG;EACH,MAAM;EACN,SAAS;EACV,CAAC;AAEF,KAAI,CAAC,SAAS,GACZ,OAAM,IAAI,WACR,QAAQ,SAAS,OAAO,IAAI,SAAS,cACrC,SAAS,QACT,SAAS,YACT,SACD;CAGH,MAAM,OAAO,MAAM,cAAc,UAAU,aAAa;AAExD,QAAO,yBACH,eAAe,MAAM,KAAK,GAC1B,eAAe,UAAU,KAAK;;AAGpC,MAAa,YAAY;;;;;;;;;;;;;;;;;;;AAoBzB,MAAa,MAAM;CACjB,MACE,UACA,QACA,YACG,OAAO,UAAU,QAAQ;EAAE,GAAG;EAAS,QAAQ;EAAO,CAAC;CAE5D,OACE,UACA,QACA,MACA,YACG,OAAO,UAAU,QAAQ;EAAE,GAAG;EAAS,QAAQ;EAAQ;EAAM,CAAC;CAEnE,MACE,UACA,QACA,MACA,YACG,OAAO,UAAU,QAAQ;EAAE,GAAG;EAAS,QAAQ;EAAO;EAAM,CAAC;CAElE,QACE,UACA,QACA,MACA,YACG,OAAO,UAAU,QAAQ;EAAE,GAAG;EAAS,QAAQ;EAAS;EAAM,CAAC;CAEpE,SACE,UACA,QACA,YACG,OAAO,UAAU,QAAQ;EAAE,GAAG;EAAS,QAAQ;EAAU,CAAC;CAChE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { n as ResponseType, r as ResponseTypeMap } from "../index-Dz58_HD6.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/index.d.ts
|
|
4
|
+
declare function parseResponse<TResponseType extends ResponseType>(response: Response, responseType: TResponseType): Promise<ResponseTypeMap[TResponseType]>;
|
|
5
|
+
declare function prepareHeadersAndBody<TBody = unknown>(body: TBody, headers: HeadersInit | undefined): {
|
|
6
|
+
body: BodyInit | null;
|
|
7
|
+
headers: HeadersInit | undefined;
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
export { parseResponse, prepareHeadersAndBody };
|
|
11
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/utils/index.ts"],"sourcesContent":[],"mappings":";;;iBAGgB,oCAAoC,wBACxC,wBACI,gBACb,QAAQ,gBAAgB;iBAmEX,6CACR,gBACG;EAxEK,IAAA,EAyEL,QAzEK,GAAa,IAAA;EAAuB,OAAA,EAyEf,WAzEe,GAAA,SAAA;CACxC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { t as FetchError } from "./errors-BCMOymYz.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/index.ts
|
|
4
|
+
function parseResponse(response, responseType) {
|
|
5
|
+
const contentType = response.headers.get("content-type");
|
|
6
|
+
const parser = {
|
|
7
|
+
json: async () => {
|
|
8
|
+
if (!contentType?.includes("application/json")) throw new FetchError(contentType ? `Expected JSON response but received content type: ${contentType}` : "Expected JSON response but received no content type", response.status, response.statusText, response);
|
|
9
|
+
return await response.json();
|
|
10
|
+
},
|
|
11
|
+
arrayBuffer: async () => response.arrayBuffer(),
|
|
12
|
+
blob: async () => response.blob(),
|
|
13
|
+
bytes: async () => {
|
|
14
|
+
const buffer = await response.arrayBuffer();
|
|
15
|
+
return new Uint8Array(buffer);
|
|
16
|
+
},
|
|
17
|
+
clone: async () => response.clone(),
|
|
18
|
+
formData: async () => {
|
|
19
|
+
if (!contentType?.includes("multipart/form-data")) throw new FetchError("Expected FormData response but received different content type", response.status, response.statusText, response);
|
|
20
|
+
return await response.formData();
|
|
21
|
+
},
|
|
22
|
+
text: async () => {
|
|
23
|
+
if (!contentType?.includes("text/")) throw new FetchError("Expected text response but received different content type", response.status, response.statusText, response);
|
|
24
|
+
return await response.text();
|
|
25
|
+
}
|
|
26
|
+
}[responseType];
|
|
27
|
+
if (!parser) return Promise.reject(new FetchError(`Unsupported response type: ${responseType}`, response.status, response.statusText, response));
|
|
28
|
+
return parser();
|
|
29
|
+
}
|
|
30
|
+
function prepareHeadersAndBody(body, headers) {
|
|
31
|
+
let preparedBody = null;
|
|
32
|
+
let preparedHeaders = headers;
|
|
33
|
+
if (body === null || body === void 0) return {
|
|
34
|
+
body: null,
|
|
35
|
+
headers: preparedHeaders
|
|
36
|
+
};
|
|
37
|
+
if (body instanceof FormData || body instanceof URLSearchParams || body instanceof Blob || body instanceof ArrayBuffer || body instanceof ReadableStream || typeof body === "string") preparedBody = body;
|
|
38
|
+
else if (typeof body === "object") {
|
|
39
|
+
preparedBody = JSON.stringify(body);
|
|
40
|
+
preparedHeaders = {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
...headers
|
|
43
|
+
};
|
|
44
|
+
} else preparedBody = String(body);
|
|
45
|
+
return {
|
|
46
|
+
body: preparedBody,
|
|
47
|
+
headers: preparedHeaders
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { prepareHeadersAndBody as n, parseResponse as t };
|
|
53
|
+
//# sourceMappingURL=utils-CZ-1Z2-1.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils-CZ-1Z2-1.mjs","names":["preparedBody: BodyInit | null"],"sources":["../src/utils/index.ts"],"sourcesContent":["import { FetchError } from \"../errors\";\nimport type { ResponseType, ResponseTypeMap } from \"../types\";\n\nexport function parseResponse<TResponseType extends ResponseType>(\n response: Response,\n responseType: TResponseType\n): Promise<ResponseTypeMap[TResponseType]> {\n const contentType = response.headers.get(\"content-type\");\n\n const parsers: Record<\n ResponseType,\n () => Promise<ResponseTypeMap[ResponseType]>\n > = {\n json: async () => {\n if (!contentType?.includes(\"application/json\")) {\n const errorMessage = contentType\n ? `Expected JSON response but received content type: ${contentType}`\n : \"Expected JSON response but received no content type\";\n throw new FetchError(\n errorMessage,\n response.status,\n response.statusText,\n response\n );\n }\n return await response.json();\n },\n arrayBuffer: async () => response.arrayBuffer(),\n blob: async () => response.blob(),\n bytes: async () => {\n const buffer = await response.arrayBuffer();\n return new Uint8Array(buffer);\n },\n clone: async () => response.clone(),\n formData: async () => {\n if (!contentType?.includes(\"multipart/form-data\")) {\n throw new FetchError(\n \"Expected FormData response but received different content type\",\n response.status,\n response.statusText,\n response\n );\n }\n return await response.formData();\n },\n text: async () => {\n if (!contentType?.includes(\"text/\")) {\n throw new FetchError(\n \"Expected text response but received different content type\",\n response.status,\n response.statusText,\n response\n );\n }\n return await response.text();\n },\n };\n\n const parser = parsers[responseType];\n if (!parser) {\n return Promise.reject(\n new FetchError(\n `Unsupported response type: ${responseType}`,\n response.status,\n response.statusText,\n response\n )\n ) as Promise<ResponseTypeMap[TResponseType]>;\n }\n\n return parser() as Promise<ResponseTypeMap[TResponseType]>;\n}\n\nexport function prepareHeadersAndBody<TBody = unknown>(\n body: TBody,\n headers: HeadersInit | undefined\n): { body: BodyInit | null; headers: HeadersInit | undefined } {\n let preparedBody: BodyInit | null = null;\n let preparedHeaders = headers;\n\n // Handle null/undefined body\n if (body === null || body === undefined) {\n return { body: null, headers: preparedHeaders };\n }\n\n // Check if body is already a valid BodyInit type\n if (\n body instanceof FormData ||\n body instanceof URLSearchParams ||\n body instanceof Blob ||\n body instanceof ArrayBuffer ||\n body instanceof ReadableStream ||\n typeof body === \"string\"\n ) {\n preparedBody = body as BodyInit;\n } else if (typeof body === \"object\") {\n // If body is a plain object, stringify it and set Content-Type\n preparedBody = JSON.stringify(body);\n preparedHeaders = {\n \"Content-Type\": \"application/json\",\n ...headers,\n };\n } else {\n // Handle other primitive types by converting to string\n preparedBody = String(body);\n }\n\n return { body: preparedBody, headers: preparedHeaders };\n}\n"],"mappings":";;;AAGA,SAAgB,cACd,UACA,cACyC;CACzC,MAAM,cAAc,SAAS,QAAQ,IAAI,eAAe;CAmDxD,MAAM,SA9CF;EACF,MAAM,YAAY;AAChB,OAAI,CAAC,aAAa,SAAS,mBAAmB,CAI5C,OAAM,IAAI,WAHW,cACjB,qDAAqD,gBACrD,uDAGF,SAAS,QACT,SAAS,YACT,SACD;AAEH,UAAO,MAAM,SAAS,MAAM;;EAE9B,aAAa,YAAY,SAAS,aAAa;EAC/C,MAAM,YAAY,SAAS,MAAM;EACjC,OAAO,YAAY;GACjB,MAAM,SAAS,MAAM,SAAS,aAAa;AAC3C,UAAO,IAAI,WAAW,OAAO;;EAE/B,OAAO,YAAY,SAAS,OAAO;EACnC,UAAU,YAAY;AACpB,OAAI,CAAC,aAAa,SAAS,sBAAsB,CAC/C,OAAM,IAAI,WACR,kEACA,SAAS,QACT,SAAS,YACT,SACD;AAEH,UAAO,MAAM,SAAS,UAAU;;EAElC,MAAM,YAAY;AAChB,OAAI,CAAC,aAAa,SAAS,QAAQ,CACjC,OAAM,IAAI,WACR,8DACA,SAAS,QACT,SAAS,YACT,SACD;AAEH,UAAO,MAAM,SAAS,MAAM;;EAE/B,CAEsB;AACvB,KAAI,CAAC,OACH,QAAO,QAAQ,OACb,IAAI,WACF,8BAA8B,gBAC9B,SAAS,QACT,SAAS,YACT,SACD,CACF;AAGH,QAAO,QAAQ;;AAGjB,SAAgB,sBACd,MACA,SAC6D;CAC7D,IAAIA,eAAgC;CACpC,IAAI,kBAAkB;AAGtB,KAAI,SAAS,QAAQ,SAAS,OAC5B,QAAO;EAAE,MAAM;EAAM,SAAS;EAAiB;AAIjD,KACE,gBAAgB,YAChB,gBAAgB,mBAChB,gBAAgB,QAChB,gBAAgB,eAChB,gBAAgB,kBAChB,OAAO,SAAS,SAEhB,gBAAe;UACN,OAAO,SAAS,UAAU;AAEnC,iBAAe,KAAK,UAAU,KAAK;AACnC,oBAAkB;GAChB,gBAAgB;GAChB,GAAG;GACJ;OAGD,gBAAe,OAAO,KAAK;AAG7B,QAAO;EAAE,MAAM;EAAc,SAAS;EAAiB"}
|
package/package.json
CHANGED
|
@@ -1,33 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zap-studio/fetch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"CHANGELOG.md"
|
|
13
|
+
],
|
|
9
14
|
"dependencies": {
|
|
10
15
|
"zod": "^4.1.12"
|
|
11
16
|
},
|
|
12
17
|
"devDependencies": {
|
|
13
18
|
"@types/node": "latest",
|
|
19
|
+
"@vitest/coverage-v8": "^4.0.13",
|
|
14
20
|
"jsdom": "latest",
|
|
15
21
|
"tsdown": "latest",
|
|
16
22
|
"typescript": "latest",
|
|
17
23
|
"vitest": "latest",
|
|
24
|
+
"@zap-studio/tsdown-config": "0.0.0",
|
|
18
25
|
"@zap-studio/typescript-config": "0.0.0",
|
|
19
|
-
"@zap-studio/vitest-config": "0.0.0"
|
|
20
|
-
"@zap-studio/tsdown-config": "0.0.0"
|
|
26
|
+
"@zap-studio/vitest-config": "0.0.0"
|
|
21
27
|
},
|
|
22
28
|
"exports": {
|
|
23
|
-
".": "./dist/index.
|
|
24
|
-
"./errors": "./dist/errors/index.
|
|
25
|
-
"./types": "./dist/types/index.
|
|
29
|
+
".": "./dist/index.mjs",
|
|
30
|
+
"./errors": "./dist/errors/index.mjs",
|
|
31
|
+
"./types": "./dist/types/index.mjs",
|
|
32
|
+
"./utils": "./dist/utils/index.mjs",
|
|
26
33
|
"./package.json": "./package.json"
|
|
27
34
|
},
|
|
28
|
-
"main": "./dist/index.
|
|
29
|
-
"module": "./dist/index.
|
|
30
|
-
"types": "./dist/index.d.
|
|
35
|
+
"main": "./dist/index.mjs",
|
|
36
|
+
"module": "./dist/index.mjs",
|
|
37
|
+
"types": "./dist/index.d.mts",
|
|
31
38
|
"scripts": {
|
|
32
39
|
"build": "tsdown --config tsdown.config.ts",
|
|
33
40
|
"check-types": "tsc --noEmit",
|