@zap-studio/fetch 0.1.0 → 0.1.1
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 +6 -0
- package/README.md +169 -0
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +3 -0
- package/dist/errors-DrfSty0J.js +14 -0
- package/dist/errors-DrfSty0J.js.map +1 -0
- package/dist/index-BkzLFXkF.d.ts +9 -0
- package/dist/index-BkzLFXkF.d.ts.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +1 -0
- package/package.json +8 -3
- 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
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 `safeFetch` directly
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { safeFetch } from "@zap-studio/fetch";
|
|
91
|
+
|
|
92
|
+
const result = await safeFetch(
|
|
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 safeFetch(
|
|
109
|
+
"/api/data",
|
|
110
|
+
z.string(),
|
|
111
|
+
{ responseType: "text" }
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Get blob response
|
|
115
|
+
const blob = await safeFetch(
|
|
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 safeFetch(url, UserSchema, {
|
|
141
|
+
throwOnValidationError: true,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Return validation result without throwing
|
|
145
|
+
const result = await safeFetch(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.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/errors/index.ts"],"sourcesContent":[],"mappings":";cAAa,UAAA,SAAmB,KAAA;EAAnB,MAAA,EAAA,MAAW;EAKL,UAAA,EAAA,MAAA;EAAA,QAAA,EAAA,QAAA;EALa,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,EAAA,UAAA,EAAA,MAAA,EAAA,QAAA,EAKb,QALa"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/errors/index.ts
|
|
2
|
+
var FetchError = class extends Error {
|
|
3
|
+
constructor(message, status, statusText, response) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.statusText = statusText;
|
|
7
|
+
this.response = response;
|
|
8
|
+
this.name = "FetchError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { FetchError as t };
|
|
14
|
+
//# sourceMappingURL=errors-DrfSty0J.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors-DrfSty0J.js","names":["status: number","statusText: string","response: Response"],"sources":["../src/errors/index.ts"],"sourcesContent":["export class FetchError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic status: number,\n\t\tpublic statusText: string,\n\t\tpublic response: Response,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"FetchError\";\n\t}\n}\n"],"mappings":";AAAA,IAAa,aAAb,cAAgC,MAAM;CACrC,YACC,SACA,AAAOA,QACP,AAAOC,YACP,AAAOC,UACN;AACD,QAAM,QAAQ;EAJP;EACA;EACA;AAGP,OAAK,OAAO"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/types/index.d.ts
|
|
2
|
+
type FetchConfig<TBody> = Omit<RequestInit, "body"> & {
|
|
3
|
+
body?: TBody;
|
|
4
|
+
throwOnValidationError?: boolean;
|
|
5
|
+
};
|
|
6
|
+
type ResponseType = "json" | "text" | "blob" | "arrayBuffer" | "formData";
|
|
7
|
+
//#endregion
|
|
8
|
+
export { ResponseType as n, FetchConfig as t };
|
|
9
|
+
//# sourceMappingURL=index-BkzLFXkF.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-BkzLFXkF.d.ts","names":[],"sources":["../src/types/index.ts"],"sourcesContent":[],"mappings":";KAAY,qBAAqB,KAAK;EAA1B,IAAA,CAAA,EACJ,KADI;EAA0B,sBAAA,CAAA,EAAA,OAAA;CAAL;AACzB,KAII,YAAA,GAJJ,MAAA,GAAA,MAAA,GAAA,MAAA,GAAA,aAAA,GAAA,UAAA"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { n as ResponseType, t as FetchConfig } from "./index-BkzLFXkF.js";
|
|
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 { safeFetch } 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 safeFetch(
|
|
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 safeFetch<TResponse$1, TBody$1 = unknown>(url: string, responseSchema: z.ZodType<TResponse$1>, config?: FetchConfig<TBody$1> & {
|
|
29
|
+
throwOnValidationError?: true;
|
|
30
|
+
responseType?: ResponseType;
|
|
31
|
+
}): Promise<TResponse$1>;
|
|
32
|
+
declare function safeFetch<TResponse$1, TBody$1 = unknown>(url: string, responseSchema: z.ZodType<TResponse$1>, config?: FetchConfig<TBody$1> & {
|
|
33
|
+
throwOnValidationError: false;
|
|
34
|
+
responseType?: ResponseType;
|
|
35
|
+
}): Promise<ReturnType<typeof responseSchema.safeParse>>;
|
|
36
|
+
/**
|
|
37
|
+
* Convenience methods for common HTTP verbs
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* import { z } from "zod";
|
|
41
|
+
* import { api } from "@zap-studio/fetch";
|
|
42
|
+
*
|
|
43
|
+
* const PostSchema = z.object({
|
|
44
|
+
* id: z.number(),
|
|
45
|
+
* title: z.string(),
|
|
46
|
+
* content: z.string(),
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* async function fetchPost(postId: number) {
|
|
50
|
+
* const post = await api.get(`https://api.example.com/posts/${postId}`, PostSchema);
|
|
51
|
+
* return post; // post is typed as { id: number; title: string; content: string; }
|
|
52
|
+
* }
|
|
53
|
+
*/
|
|
54
|
+
declare const api: {
|
|
55
|
+
get: <TResponse>(url: string, schema: z.ZodType<TResponse>, config?: Omit<RequestInit, "method" | "body">) => Promise<TResponse>;
|
|
56
|
+
post: <TResponse, TBody = unknown>(url: string, schema: z.ZodType<TResponse>, body?: TBody, config?: Omit<RequestInit, "method" | "body">) => Promise<TResponse>;
|
|
57
|
+
put: <TResponse, TBody = unknown>(url: string, schema: z.ZodType<TResponse>, body?: TBody, config?: Omit<RequestInit, "method" | "body">) => Promise<TResponse>;
|
|
58
|
+
patch: <TResponse, TBody = unknown>(url: string, schema: z.ZodType<TResponse>, body?: TBody, config?: Omit<RequestInit, "method" | "body">) => Promise<TResponse>;
|
|
59
|
+
delete: <TResponse>(url: string, schema: z.ZodType<TResponse>, config?: Omit<RequestInit, "method" | "body">) => Promise<TResponse>;
|
|
60
|
+
};
|
|
61
|
+
//#endregion
|
|
62
|
+
export { api, safeFetch };
|
|
63
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;AA0BA;;;;;;;;;AASA;;;;;;;;;;AAoHA;AAGoB,iBAhIE,SAgIF,CAAA,WAAA,EAAA,UAAA,OAAA,CAAA,CAAA,GAAA,EAAA,MAAA,EAAA,cAAA,EA9HH,CAAA,CAAE,OA8HC,CA9HO,WA8HP,CAAA,EAAA,MACT,CADS,EA7HV,WA6HU,CA7HE,OA6HF,CAAA,GAAA;EAAR,sBAAA,CAAA,EAAA,IAAA;EACI,YAAA,CAAA,EA5HC,YA4HD;CAAL,CAAA,EA1HR,OA0HQ,CA1HA,WA0HA,CAAA;AAAoC,iBAxHzB,SAwHyB,CAAA,WAAA,EAAA,UAAA,OAAA,CAAA,CAAA,GAAA,EAAA,MAAA,EAAA,cAAA,EAtH9B,CAAA,CAAE,OAsH4B,CAtHpB,WAsHoB,CAAA,EAAA,MAKrC,CALqC,EArHrC,WAqHqC,CArHzB,OAqHyB,CAAA,GAAA;EAAA,sBAAA,EAAA,KAAA;EAK3B,YAAA,CAAA,EAxHH,YAwHG;CAAV,CAAA,EAtHP,OAsHS,CAtHD,UAsHC,CAAA,OAtHiB,cAAA,CAAe,SAsHhC,CAAA,CAAA;;;;;;;;;;;;;;;;;;;AAgBmC,cAzBlC,GAyBkC,EAAA;EAK3B,GAAA,EAAA,CAAA,SAAA,CAAA,CAAA,GAAA,EAAA,MAAA,EAAA,MAAA,EA3BV,CAAA,CAAE,OA2BQ,CA3BA,SA2BA,CAAA,EAAA,MAAA,CAAA,EA1BT,IA0BS,CA1BJ,WA0BI,EAAA,QAAA,GAAA,MAAA,CAAA,EAAA,GA1B2B,OA0B3B,CA1B2B,SA0B3B,CAAA;EAAR,IAAA,EAAA,CAAA,SAAA,EAAA,QAAA,OAAA,CAAA,CAAA,GAAA,EAAA,MAAA,EAAA,MAAA,EArBF,CAAA,CAAE,OAqBA,CArBQ,SAqBR,CAAA,EAAA,IAAA,CAAA,EApBH,KAoBG,EAAA,MAAA,CAAA,EAnBD,IAmBC,CAnBI,WAmBJ,EAAA,QAAA,GAAA,MAAA,CAAA,EAAA,GAnBmC,OAmBnC,CAnBmC,SAmBnC,CAAA;EACI,GAAA,EAAA,CAAA,SAAA,EAAA,QAAA,OAAA,CAAA,CAAA,GAAA,EAAA,MAAA,EAAA,MAAA,EAfN,CAAA,CAAE,OAeI,CAfI,SAeJ,CAAA,EAAA,IAAA,CAAA,EAdP,KAcO,EAAA,MAAA,CAAA,EAbL,IAaK,CAbA,WAaA,EAAA,QAAA,GAAA,MAAA,CAAA,EAAA,GAb+B,OAa/B,CAb+B,SAa/B,CAAA;EAAL,KAAA,EAAA,CAAA,SAAA,EAAA,QAAA,OAAA,CAAA,CAAA,GAAA,EAAA,MAAA,EAAA,MAAA,EARD,CAAA,CAAE,OAQD,CARS,SAQT,CAAA,EAAA,IAAA,CAAA,EAPF,KAOE,EAAA,MAAA,CAAA,EANA,IAMA,CANK,WAML,EAAA,QAAA,GAAA,MAAA,CAAA,EAAA,GANoC,OAMpC,CANoC,SAMpC,CAAA;EAAoC,MAAA,EAAA,CAAA,SAAA,CAAA,CAAA,GAAA,EAAA,MAAA,EAAA,MAAA,EADrC,CAAA,CAAE,OACmC,CAD3B,SAC2B,CAAA,EAAA,MAAA,CAAA,EAApC,IAAoC,CAA/B,WAA+B,EAAA,QAAA,GAAA,MAAA,CAAA,EAAA,GAAA,OAAA,CAAA,SAAA,CAAA;CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { t as FetchError } from "./errors-DrfSty0J.js";
|
|
2
|
+
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
async function safeFetch(url, responseSchema, config) {
|
|
5
|
+
const { body, headers, throwOnValidationError = true, responseType = "json",...rest } = config || {};
|
|
6
|
+
const fetchConfig = {
|
|
7
|
+
...rest,
|
|
8
|
+
headers: { ...headers }
|
|
9
|
+
};
|
|
10
|
+
if (body !== void 0) {
|
|
11
|
+
const shouldSetContentType = !headers || typeof headers === "object" && !Array.isArray(headers) && !(headers instanceof Headers) && !("Content-Type" in headers);
|
|
12
|
+
if (body instanceof FormData) fetchConfig.body = body;
|
|
13
|
+
else if (typeof body === "string") {
|
|
14
|
+
fetchConfig.body = body;
|
|
15
|
+
if (shouldSetContentType) fetchConfig.headers["Content-Type"] = "text/plain";
|
|
16
|
+
} else {
|
|
17
|
+
fetchConfig.body = JSON.stringify(body);
|
|
18
|
+
if (shouldSetContentType) fetchConfig.headers["Content-Type"] = "application/json";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const response = await fetch(url, fetchConfig);
|
|
22
|
+
if (!response.ok) throw new FetchError(`HTTP ${response.status}: ${response.statusText}`, response.status, response.statusText, response);
|
|
23
|
+
let data;
|
|
24
|
+
if (responseType === "json") if (response.headers.get("content-type")?.includes("application/json")) data = await response.json();
|
|
25
|
+
else throw new FetchError("Expected JSON response but received different content type", response.status, response.statusText, response);
|
|
26
|
+
else if (responseType === "text") data = await response.text();
|
|
27
|
+
else if (responseType === "blob") data = await response.blob();
|
|
28
|
+
else if (responseType === "arrayBuffer") data = await response.arrayBuffer();
|
|
29
|
+
else if (responseType === "formData") data = await response.formData();
|
|
30
|
+
if (throwOnValidationError) return responseSchema.parse(data);
|
|
31
|
+
return responseSchema.safeParse(data);
|
|
32
|
+
}
|
|
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
|
+
const api = {
|
|
52
|
+
get: (url, schema, config) => safeFetch(url, schema, {
|
|
53
|
+
...config,
|
|
54
|
+
method: "GET"
|
|
55
|
+
}),
|
|
56
|
+
post: (url, schema, body, config) => safeFetch(url, schema, {
|
|
57
|
+
...config,
|
|
58
|
+
method: "POST",
|
|
59
|
+
body
|
|
60
|
+
}),
|
|
61
|
+
put: (url, schema, body, config) => safeFetch(url, schema, {
|
|
62
|
+
...config,
|
|
63
|
+
method: "PUT",
|
|
64
|
+
body
|
|
65
|
+
}),
|
|
66
|
+
patch: (url, schema, body, config) => safeFetch(url, schema, {
|
|
67
|
+
...config,
|
|
68
|
+
method: "PATCH",
|
|
69
|
+
body
|
|
70
|
+
}),
|
|
71
|
+
delete: (url, schema, config) => safeFetch(url, schema, {
|
|
72
|
+
...config,
|
|
73
|
+
method: "DELETE"
|
|
74
|
+
})
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
export { api, safeFetch };
|
|
79
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["fetchConfig: RequestInit","data: unknown"],"sources":["../src/index.ts"],"sourcesContent":["import type { z } from \"zod\";\nimport { FetchError } from \"./errors\";\nimport type { FetchConfig, ResponseType } from \"./types\";\n\n/**\n * Type-safe fetch wrapper with Zod validation\n *\n * @example\n * import { z } from \"zod\";\n * import { safeFetch } 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 safeFetch(\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 safeFetch<TResponse, TBody = unknown>(\n\turl: string,\n\tresponseSchema: z.ZodType<TResponse>,\n\tconfig?: FetchConfig<TBody> & {\n\t\tthrowOnValidationError?: true;\n\t\tresponseType?: ResponseType;\n\t},\n): Promise<TResponse>;\n\nexport async function safeFetch<TResponse, TBody = unknown>(\n\turl: string,\n\tresponseSchema: z.ZodType<TResponse>,\n\tconfig?: FetchConfig<TBody> & {\n\t\tthrowOnValidationError: false;\n\t\tresponseType?: ResponseType;\n\t},\n): Promise<ReturnType<typeof responseSchema.safeParse>>;\n\nexport async function safeFetch<TResponse, TBody = unknown>(\n\turl: string,\n\tresponseSchema: z.ZodType<TResponse>,\n\tconfig?: FetchConfig<TBody> & { responseType?: ResponseType },\n): Promise<TResponse | ReturnType<typeof responseSchema.safeParse>> {\n\tconst {\n\t\tbody,\n\t\theaders,\n\t\tthrowOnValidationError = true,\n\t\tresponseType = \"json\",\n\t\t...rest\n\t} = config || {};\n\n\tconst fetchConfig: RequestInit = {\n\t\t...rest,\n\t\theaders: {\n\t\t\t...(headers as Record<string, string>),\n\t\t},\n\t};\n\n\t// Only set Content-Type if body exists and it's not FormData\n\tif (body !== undefined) {\n\t\tconst shouldSetContentType =\n\t\t\t!headers ||\n\t\t\t(typeof headers === \"object\" &&\n\t\t\t\t!Array.isArray(headers) &&\n\t\t\t\t!(headers instanceof Headers) &&\n\t\t\t\t!(\"Content-Type\" in headers));\n\n\t\tif (body instanceof FormData) {\n\t\t\tfetchConfig.body = body;\n\t\t} else if (typeof body === \"string\") {\n\t\t\tfetchConfig.body = body;\n\t\t\tif (shouldSetContentType) {\n\t\t\t\t(fetchConfig.headers as Record<string, string>)[\"Content-Type\"] =\n\t\t\t\t\t\"text/plain\";\n\t\t\t}\n\t\t} else {\n\t\t\tfetchConfig.body = JSON.stringify(body);\n\t\t\tif (shouldSetContentType) {\n\t\t\t\t(fetchConfig.headers as Record<string, string>)[\"Content-Type\"] =\n\t\t\t\t\t\"application/json\";\n\t\t\t}\n\t\t}\n\t}\n\n\tconst response = await fetch(url, fetchConfig);\n\n\tif (!response.ok) {\n\t\tthrow new FetchError(\n\t\t\t`HTTP ${response.status}: ${response.statusText}`,\n\t\t\tresponse.status,\n\t\t\tresponse.statusText,\n\t\t\tresponse,\n\t\t);\n\t}\n\n\t// Parse response based on responseType\n\tlet data: unknown;\n\n\tif (responseType === \"json\") {\n\t\tconst contentType = response.headers.get(\"content-type\");\n\t\tif (contentType?.includes(\"application/json\")) {\n\t\t\tdata = await response.json();\n\t\t} else {\n\t\t\tthrow new FetchError(\n\t\t\t\t\"Expected JSON response but received different content type\",\n\t\t\t\tresponse.status,\n\t\t\t\tresponse.statusText,\n\t\t\t\tresponse,\n\t\t\t);\n\t\t}\n\t} else if (responseType === \"text\") {\n\t\tdata = await response.text();\n\t} else if (responseType === \"blob\") {\n\t\tdata = await response.blob();\n\t} else if (responseType === \"arrayBuffer\") {\n\t\tdata = await response.arrayBuffer();\n\t} else if (responseType === \"formData\") {\n\t\tdata = await response.formData();\n\t}\n\n\tif (throwOnValidationError) {\n\t\treturn responseSchema.parse(data);\n\t}\n\n\treturn responseSchema.safeParse(data);\n}\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\tget: <TResponse>(\n\t\turl: string,\n\t\tschema: z.ZodType<TResponse>,\n\t\tconfig?: Omit<RequestInit, \"method\" | \"body\">,\n\t) => safeFetch(url, schema, { ...config, method: \"GET\" }),\n\n\tpost: <TResponse, TBody = unknown>(\n\t\turl: string,\n\t\tschema: z.ZodType<TResponse>,\n\t\tbody?: TBody,\n\t\tconfig?: Omit<RequestInit, \"method\" | \"body\">,\n\t) => safeFetch(url, schema, { ...config, method: \"POST\", body }),\n\n\tput: <TResponse, TBody = unknown>(\n\t\turl: string,\n\t\tschema: z.ZodType<TResponse>,\n\t\tbody?: TBody,\n\t\tconfig?: Omit<RequestInit, \"method\" | \"body\">,\n\t) => safeFetch(url, schema, { ...config, method: \"PUT\", body }),\n\n\tpatch: <TResponse, TBody = unknown>(\n\t\turl: string,\n\t\tschema: z.ZodType<TResponse>,\n\t\tbody?: TBody,\n\t\tconfig?: Omit<RequestInit, \"method\" | \"body\">,\n\t) => safeFetch(url, schema, { ...config, method: \"PATCH\", body }),\n\n\tdelete: <TResponse>(\n\t\turl: string,\n\t\tschema: z.ZodType<TResponse>,\n\t\tconfig?: Omit<RequestInit, \"method\" | \"body\">,\n\t) => safeFetch(url, schema, { ...config, method: \"DELETE\" }),\n};\n"],"mappings":";;;AA4CA,eAAsB,UACrB,KACA,gBACA,QACmE;CACnE,MAAM,EACL,MACA,SACA,yBAAyB,MACzB,eAAe,OACf,GAAG,SACA,UAAU,EAAE;CAEhB,MAAMA,cAA2B;EAChC,GAAG;EACH,SAAS,EACR,GAAI,SACJ;EACD;AAGD,KAAI,SAAS,QAAW;EACvB,MAAM,uBACL,CAAC,WACA,OAAO,YAAY,YACnB,CAAC,MAAM,QAAQ,QAAQ,IACvB,EAAE,mBAAmB,YACrB,EAAE,kBAAkB;AAEtB,MAAI,gBAAgB,SACnB,aAAY,OAAO;WACT,OAAO,SAAS,UAAU;AACpC,eAAY,OAAO;AACnB,OAAI,qBACH,CAAC,YAAY,QAAmC,kBAC/C;SAEI;AACN,eAAY,OAAO,KAAK,UAAU,KAAK;AACvC,OAAI,qBACH,CAAC,YAAY,QAAmC,kBAC/C;;;CAKJ,MAAM,WAAW,MAAM,MAAM,KAAK,YAAY;AAE9C,KAAI,CAAC,SAAS,GACb,OAAM,IAAI,WACT,QAAQ,SAAS,OAAO,IAAI,SAAS,cACrC,SAAS,QACT,SAAS,YACT,SACA;CAIF,IAAIC;AAEJ,KAAI,iBAAiB,OAEpB,KADoB,SAAS,QAAQ,IAAI,eAAe,EACvC,SAAS,mBAAmB,CAC5C,QAAO,MAAM,SAAS,MAAM;KAE5B,OAAM,IAAI,WACT,8DACA,SAAS,QACT,SAAS,YACT,SACA;UAEQ,iBAAiB,OAC3B,QAAO,MAAM,SAAS,MAAM;UAClB,iBAAiB,OAC3B,QAAO,MAAM,SAAS,MAAM;UAClB,iBAAiB,cAC3B,QAAO,MAAM,SAAS,aAAa;UACzB,iBAAiB,WAC3B,QAAO,MAAM,SAAS,UAAU;AAGjC,KAAI,uBACH,QAAO,eAAe,MAAM,KAAK;AAGlC,QAAO,eAAe,UAAU,KAAK;;;;;;;;;;;;;;;;;;;;AAqBtC,MAAa,MAAM;CAClB,MACC,KACA,QACA,WACI,UAAU,KAAK,QAAQ;EAAE,GAAG;EAAQ,QAAQ;EAAO,CAAC;CAEzD,OACC,KACA,QACA,MACA,WACI,UAAU,KAAK,QAAQ;EAAE,GAAG;EAAQ,QAAQ;EAAQ;EAAM,CAAC;CAEhE,MACC,KACA,QACA,MACA,WACI,UAAU,KAAK,QAAQ;EAAE,GAAG;EAAQ,QAAQ;EAAO;EAAM,CAAC;CAE/D,QACC,KACA,QACA,MACA,WACI,UAAU,KAAK,QAAQ;EAAE,GAAG;EAAQ,QAAQ;EAAS;EAAM,CAAC;CAEjE,SACC,KACA,QACA,WACI,UAAU,KAAK,QAAQ;EAAE,GAAG;EAAQ,QAAQ;EAAU,CAAC;CAC5D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zap-studio/fetch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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
|
},
|
|
@@ -15,9 +20,9 @@
|
|
|
15
20
|
"tsdown": "latest",
|
|
16
21
|
"typescript": "latest",
|
|
17
22
|
"vitest": "latest",
|
|
18
|
-
"@zap-studio/
|
|
23
|
+
"@zap-studio/tsdown-config": "0.0.0",
|
|
19
24
|
"@zap-studio/vitest-config": "0.0.0",
|
|
20
|
-
"@zap-studio/
|
|
25
|
+
"@zap-studio/typescript-config": "0.0.0"
|
|
21
26
|
},
|
|
22
27
|
"exports": {
|
|
23
28
|
".": "./dist/index.js",
|
package/src/errors/index.ts
DELETED
package/src/index.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import type { z } from "zod";
|
|
2
|
-
import { FetchError } from "./errors";
|
|
3
|
-
import type { FetchConfig, ResponseType } from "./types";
|
|
4
|
-
import { parseResponse, prepareHeadersAndBody } from "./utils";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Type-safe fetch wrapper with Zod validation
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* import { z } from "zod";
|
|
11
|
-
* import { safeFetch } 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 safeFetch(
|
|
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
|
-
export async function safeFetch<
|
|
29
|
-
TResponse,
|
|
30
|
-
TBody = unknown,
|
|
31
|
-
TResponseType extends ResponseType = "json",
|
|
32
|
-
>(
|
|
33
|
-
resource: string,
|
|
34
|
-
responseSchema: z.ZodType<TResponse>,
|
|
35
|
-
config?: FetchConfig<TBody> & {
|
|
36
|
-
throwOnValidationError?: boolean;
|
|
37
|
-
responseType?: TResponseType;
|
|
38
|
-
},
|
|
39
|
-
): Promise<
|
|
40
|
-
TResponse | z.ZodSafeParseSuccess<TResponse> | z.ZodSafeParseError<TResponse>
|
|
41
|
-
> {
|
|
42
|
-
const {
|
|
43
|
-
body,
|
|
44
|
-
headers,
|
|
45
|
-
throwOnValidationError = true,
|
|
46
|
-
responseType = "json" as TResponseType,
|
|
47
|
-
...rest
|
|
48
|
-
} = config || {};
|
|
49
|
-
|
|
50
|
-
const { body: preparedBody, headers: preparedHeaders } =
|
|
51
|
-
prepareHeadersAndBody(body, headers);
|
|
52
|
-
|
|
53
|
-
const response = await fetch(resource, {
|
|
54
|
-
...rest,
|
|
55
|
-
body: preparedBody,
|
|
56
|
-
headers: preparedHeaders,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
if (!response.ok) {
|
|
60
|
-
throw new FetchError(
|
|
61
|
-
`HTTP ${response.status}: ${response.statusText}`,
|
|
62
|
-
response.status,
|
|
63
|
-
response.statusText,
|
|
64
|
-
response,
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const data = await parseResponse(response, responseType);
|
|
69
|
-
|
|
70
|
-
return throwOnValidationError
|
|
71
|
-
? responseSchema.parse(data)
|
|
72
|
-
: responseSchema.safeParse(data);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Convenience methods for common HTTP verbs
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* import { z } from "zod";
|
|
80
|
-
* import { api } from "@zap-studio/fetch";
|
|
81
|
-
*
|
|
82
|
-
* const PostSchema = z.object({
|
|
83
|
-
* id: z.number(),
|
|
84
|
-
* title: z.string(),
|
|
85
|
-
* content: z.string(),
|
|
86
|
-
* });
|
|
87
|
-
*
|
|
88
|
-
* async function fetchPost(postId: number) {
|
|
89
|
-
* const post = await api.get(`https://api.example.com/posts/${postId}`, PostSchema);
|
|
90
|
-
* return post; // post is typed as { id: number; title: string; content: string; }
|
|
91
|
-
* }
|
|
92
|
-
*/
|
|
93
|
-
export const api = {
|
|
94
|
-
get: <TResponse>(
|
|
95
|
-
resource: string,
|
|
96
|
-
schema: z.ZodType<TResponse>,
|
|
97
|
-
options?: Omit<RequestInit, "method" | "body">,
|
|
98
|
-
) => safeFetch(resource, schema, { ...options, method: "GET" }),
|
|
99
|
-
|
|
100
|
-
post: <TResponse, TBody = unknown>(
|
|
101
|
-
resource: string,
|
|
102
|
-
schema: z.ZodType<TResponse>,
|
|
103
|
-
body?: TBody,
|
|
104
|
-
options?: Omit<RequestInit, "method" | "body">,
|
|
105
|
-
) => safeFetch(resource, schema, { ...options, method: "POST", body }),
|
|
106
|
-
|
|
107
|
-
put: <TResponse, TBody = unknown>(
|
|
108
|
-
resource: string,
|
|
109
|
-
schema: z.ZodType<TResponse>,
|
|
110
|
-
body?: TBody,
|
|
111
|
-
options?: Omit<RequestInit, "method" | "body">,
|
|
112
|
-
) => safeFetch(resource, schema, { ...options, method: "PUT", body }),
|
|
113
|
-
|
|
114
|
-
patch: <TResponse, TBody = unknown>(
|
|
115
|
-
resource: string,
|
|
116
|
-
schema: z.ZodType<TResponse>,
|
|
117
|
-
body?: TBody,
|
|
118
|
-
options?: Omit<RequestInit, "method" | "body">,
|
|
119
|
-
) => safeFetch(resource, schema, { ...options, method: "PATCH", body }),
|
|
120
|
-
|
|
121
|
-
delete: <TResponse>(
|
|
122
|
-
resource: string,
|
|
123
|
-
schema: z.ZodType<TResponse>,
|
|
124
|
-
options?: Omit<RequestInit, "method" | "body">,
|
|
125
|
-
) => safeFetch(resource, schema, { ...options, method: "DELETE" }),
|
|
126
|
-
};
|
package/src/types/index.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export type FetchConfig<TBody> = Omit<RequestInit, "body"> & {
|
|
2
|
-
body?: TBody;
|
|
3
|
-
throwOnValidationError?: boolean;
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
export type ResponseType =
|
|
7
|
-
| "arrayBuffer"
|
|
8
|
-
| "blob"
|
|
9
|
-
| "bytes"
|
|
10
|
-
| "clone"
|
|
11
|
-
| "formData"
|
|
12
|
-
| "json"
|
|
13
|
-
| "text";
|
|
14
|
-
|
|
15
|
-
export type ResponseTypeMap = {
|
|
16
|
-
arrayBuffer: ArrayBuffer;
|
|
17
|
-
blob: Blob;
|
|
18
|
-
bytes: Uint8Array;
|
|
19
|
-
clone: Response;
|
|
20
|
-
formData: FormData;
|
|
21
|
-
json: unknown;
|
|
22
|
-
text: string;
|
|
23
|
-
};
|