@zap-studio/fetch 0.1.2 → 0.2.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 +47 -0
- package/README.md +127 -63
- package/dist/errors-DKxnbFHZ.mjs +29 -0
- package/dist/errors-DKxnbFHZ.mjs.map +1 -0
- package/dist/errors.d.mts +22 -0
- package/dist/errors.d.mts.map +1 -0
- package/dist/errors.mjs +3 -0
- package/dist/index.d.mts +70 -28
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +155 -55
- package/dist/index.mjs.map +1 -1
- package/dist/types-C0Uhh2KM.d.mts +52 -0
- package/dist/types-C0Uhh2KM.d.mts.map +1 -0
- package/dist/types.d.mts +2 -0
- package/dist/validator-BGjWrM4d.mjs +44 -0
- package/dist/validator-BGjWrM4d.mjs.map +1 -0
- package/dist/validator.d.mts +34 -0
- package/dist/validator.d.mts.map +1 -0
- package/dist/validator.mjs +4 -0
- package/package.json +14 -11
- package/dist/errors/index.d.mts +0 -10
- package/dist/errors/index.d.mts.map +0 -1
- package/dist/errors/index.mjs +0 -3
- package/dist/errors-BCMOymYz.mjs +0 -17
- package/dist/errors-BCMOymYz.mjs.map +0 -1
- package/dist/index-Dz58_HD6.d.mts +0 -18
- package/dist/index-Dz58_HD6.d.mts.map +0 -1
- package/dist/types/index.d.mts +0 -2
- package/dist/utils/index.d.mts +0 -11
- package/dist/utils/index.d.mts.map +0 -1
- package/dist/utils/index.mjs +0 -4
- package/dist/utils-CZ-1Z2-1.mjs +0 -53
- package/dist/utils-CZ-1Z2-1.mjs.map +0 -1
- /package/dist/{types/index.mjs → types.mjs} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# @zap-studio/fetch
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 82bac5c: Replace regex-based slash trimming with more efficient string manipulation functions for URL normalization
|
|
8
|
+
|
|
9
|
+
## 0.2.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 78afb76: ### Standard Schema Support
|
|
14
|
+
|
|
15
|
+
Migrated from Zod-only validation to **Standard Schema v1** specification, enabling support for multiple validation libraries:
|
|
16
|
+
|
|
17
|
+
- Zod
|
|
18
|
+
- Valibot
|
|
19
|
+
- ArkType
|
|
20
|
+
- Any Standard Schema compliant library
|
|
21
|
+
|
|
22
|
+
### New Features
|
|
23
|
+
|
|
24
|
+
- **Factory Pattern**: `createFetch()` for creating pre-configured fetch instances with `baseURL`, default `headers`, and error handling options
|
|
25
|
+
- **Smart URL Handling**: Absolute URLs bypass `baseURL` configuration
|
|
26
|
+
- **Auto JSON Body**: Automatic `JSON.stringify()` and `Content-Type` header when using schemas with request bodies
|
|
27
|
+
|
|
28
|
+
### Breaking Changes
|
|
29
|
+
|
|
30
|
+
- Schema validation now requires Standard Schema compliant libraries (Zod 3.23+, Valibot 1.0+, ArkType 2.0+)
|
|
31
|
+
- Internal file structure reorganized (affects deep imports if any were used)
|
|
32
|
+
- `FetchError` constructor signature changed: now requires `(message, response)`
|
|
33
|
+
|
|
34
|
+
### Migration Guide
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// Before (Zod-only)
|
|
38
|
+
import { $fetch } from "@zap-studio/fetch";
|
|
39
|
+
import { z } from "zod";
|
|
40
|
+
|
|
41
|
+
// After (Standard Schema - works the same with Zod!)
|
|
42
|
+
import { $fetch } from "@zap-studio/fetch";
|
|
43
|
+
import { z } from "zod"; // Zod 3.23+ is Standard Schema compliant
|
|
44
|
+
|
|
45
|
+
// Or use other libraries
|
|
46
|
+
import * as v from "valibot";
|
|
47
|
+
import { type } from "arktype";
|
|
48
|
+
```
|
|
49
|
+
|
|
3
50
|
## 0.1.2
|
|
4
51
|
|
|
5
52
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
# @zap-studio/fetch
|
|
2
2
|
|
|
3
|
-
A type-safe fetch wrapper with
|
|
3
|
+
A type-safe fetch wrapper with Standard Schema validation.
|
|
4
|
+
|
|
5
|
+
## Why @zap-studio/fetch?
|
|
6
|
+
|
|
7
|
+
**Before:**
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
const response = await fetch("/api/users/1");
|
|
11
|
+
const data = await response.json();
|
|
12
|
+
const user = data as User; // 😱 Unsafe type assertion
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**After:**
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
const user = await api.get("/api/users/1", UserSchema);
|
|
19
|
+
// ✨ Typed, validated, and safe!
|
|
20
|
+
```
|
|
4
21
|
|
|
5
22
|
## Features
|
|
6
23
|
|
|
7
24
|
- 🎯 **Type-safe requests** with automatic type inference
|
|
8
|
-
- 🛡️ **Runtime validation** using Zod
|
|
9
|
-
- 🔄 **Automatic content-type** detection and handling
|
|
25
|
+
- 🛡️ **Runtime validation** using Standard Schema (Zod, Valibot, ArkType, etc.)
|
|
10
26
|
- ⚡️ **Convenient API methods** (GET, POST, PUT, PATCH, DELETE)
|
|
11
|
-
-
|
|
12
|
-
- 🚨 **Custom error handling** with FetchError
|
|
27
|
+
- 🏭 **Factory pattern** for creating pre-configured instances with base URLs
|
|
28
|
+
- 🚨 **Custom error handling** with FetchError and ValidationError classes
|
|
13
29
|
- 📘 **Full TypeScript support** with zero configuration
|
|
14
30
|
|
|
15
31
|
## Installation
|
|
16
32
|
|
|
17
33
|
```bash
|
|
18
34
|
pnpm add @zap-studio/fetch
|
|
19
|
-
#or
|
|
35
|
+
# or
|
|
20
36
|
npm install @zap-studio/fetch
|
|
21
37
|
```
|
|
22
38
|
|
|
@@ -34,10 +50,7 @@ const UserSchema = z.object({
|
|
|
34
50
|
});
|
|
35
51
|
|
|
36
52
|
// Make a type-safe request
|
|
37
|
-
const user = await api.get(
|
|
38
|
-
"https://api.example.com/users/1",
|
|
39
|
-
UserSchema
|
|
40
|
-
);
|
|
53
|
+
const user = await api.get("https://api.example.com/users/1", UserSchema);
|
|
41
54
|
|
|
42
55
|
// user is fully typed and validated! ✨
|
|
43
56
|
console.log(user.name); // TypeScript knows this is a string
|
|
@@ -51,28 +64,31 @@ console.log(user.name); // TypeScript knows this is a string
|
|
|
51
64
|
const user = await api.get("/api/users/1", UserSchema);
|
|
52
65
|
```
|
|
53
66
|
|
|
54
|
-
### `api.post(url, schema,
|
|
67
|
+
### `api.post(url, schema, options?)`
|
|
55
68
|
|
|
56
69
|
```typescript
|
|
57
70
|
const newUser = await api.post("/api/users", UserSchema, {
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
body: {
|
|
72
|
+
name: "John Doe",
|
|
73
|
+
email: "john@example.com",
|
|
74
|
+
},
|
|
60
75
|
});
|
|
76
|
+
// Automatically stringifies body and sets Content-Type: application/json
|
|
61
77
|
```
|
|
62
78
|
|
|
63
|
-
### `api.put(url, schema,
|
|
79
|
+
### `api.put(url, schema, options?)`
|
|
64
80
|
|
|
65
81
|
```typescript
|
|
66
82
|
const updated = await api.put("/api/users/1", UserSchema, {
|
|
67
|
-
name: "Jane Doe",
|
|
83
|
+
body: { name: "Jane Doe" },
|
|
68
84
|
});
|
|
69
85
|
```
|
|
70
86
|
|
|
71
|
-
### `api.patch(url, schema,
|
|
87
|
+
### `api.patch(url, schema, options?)`
|
|
72
88
|
|
|
73
89
|
```typescript
|
|
74
90
|
const patched = await api.patch("/api/users/1", UserSchema, {
|
|
75
|
-
email: "newemail@example.com",
|
|
91
|
+
body: { email: "newemail@example.com" },
|
|
76
92
|
});
|
|
77
93
|
```
|
|
78
94
|
|
|
@@ -82,58 +98,121 @@ const patched = await api.patch("/api/users/1", UserSchema, {
|
|
|
82
98
|
const deleted = await api.delete("/api/users/1", UserSchema);
|
|
83
99
|
```
|
|
84
100
|
|
|
101
|
+
> **Note:** The `api.*` methods always require a schema for validation. For raw responses without validation, use `$fetch` directly.
|
|
102
|
+
|
|
85
103
|
## Advanced Usage
|
|
86
104
|
|
|
87
105
|
### Using `$fetch` directly
|
|
88
106
|
|
|
107
|
+
For more control or when you don't need schema validation:
|
|
108
|
+
|
|
89
109
|
```typescript
|
|
90
110
|
import { $fetch } from "@zap-studio/fetch";
|
|
91
111
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
112
|
+
// With schema validation
|
|
113
|
+
const user = await $fetch("https://api.example.com/users/1", UserSchema, {
|
|
114
|
+
method: "GET",
|
|
115
|
+
headers: {
|
|
116
|
+
Authorization: "Bearer token",
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Without schema - returns raw Response object
|
|
121
|
+
const response = await $fetch("https://api.example.com/users/1", {
|
|
122
|
+
method: "GET",
|
|
123
|
+
});
|
|
124
|
+
const data = await response.json();
|
|
102
125
|
```
|
|
103
126
|
|
|
104
|
-
###
|
|
127
|
+
### Factory Pattern with `createFetch`
|
|
128
|
+
|
|
129
|
+
Create pre-configured fetch instances with base URLs and default headers. Useful for API clients:
|
|
105
130
|
|
|
106
131
|
```typescript
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
132
|
+
import { z } from "zod";
|
|
133
|
+
import { createFetch } from "@zap-studio/fetch";
|
|
134
|
+
|
|
135
|
+
// Create a configured instance
|
|
136
|
+
const { $fetch, api } = createFetch({
|
|
137
|
+
baseURL: "https://api.example.com",
|
|
138
|
+
headers: {
|
|
139
|
+
Authorization: "Bearer your-token",
|
|
140
|
+
"X-API-Key": "your-api-key",
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const UserSchema = z.object({
|
|
145
|
+
id: z.number(),
|
|
146
|
+
name: z.string(),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Now use relative paths - baseURL is prepended automatically
|
|
150
|
+
const user = await api.get("/users/1", UserSchema);
|
|
151
|
+
|
|
152
|
+
// POST with auto-stringified body
|
|
153
|
+
const newUser = await api.post("/users", UserSchema, {
|
|
154
|
+
body: { name: "John Doe" },
|
|
155
|
+
});
|
|
120
156
|
```
|
|
121
157
|
|
|
122
|
-
|
|
158
|
+
#### Factory Options
|
|
159
|
+
|
|
160
|
+
| Option | Type | Default | Description |
|
|
161
|
+
| ------------------------ | ------------- | ------- | ------------------------------------------------ |
|
|
162
|
+
| `baseURL` | `string` | `""` | Base URL prepended to relative paths only |
|
|
163
|
+
| `headers` | `HeadersInit` | - | Default headers included in all requests |
|
|
164
|
+
| `throwOnFetchError` | `boolean` | `true` | Throw `FetchError` on non-2xx responses |
|
|
165
|
+
| `throwOnValidationError` | `boolean` | `true` | Throw `ValidationError` on schema validation failures |
|
|
166
|
+
|
|
167
|
+
> **Note:** Absolute URLs (starting with `http://`, `https://`, or `//`) are used as-is and ignore the `baseURL`.
|
|
168
|
+
|
|
169
|
+
#### Multiple API Clients
|
|
170
|
+
|
|
171
|
+
You can create separate fetch instances for different APIs:
|
|
123
172
|
|
|
124
173
|
```typescript
|
|
125
|
-
import {
|
|
174
|
+
import { createFetch } from "@zap-studio/fetch";
|
|
175
|
+
|
|
176
|
+
// GitHub API client
|
|
177
|
+
const github = createFetch({
|
|
178
|
+
baseURL: "https://api.github.com",
|
|
179
|
+
headers: { Authorization: "Bearer github-token" },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Your internal API client
|
|
183
|
+
const internal = createFetch({
|
|
184
|
+
baseURL: "https://internal.example.com/api",
|
|
185
|
+
headers: { "X-Internal-Key": "secret" },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Use them independently
|
|
189
|
+
const repo = await github.api.get("/repos/owner/repo", RepoSchema);
|
|
190
|
+
const data = await internal.api.get("/data", DataSchema);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Error Handling
|
|
194
|
+
|
|
195
|
+
The package exports specialized error classes for granular error handling:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { $fetch } from "@zap-studio/fetch";
|
|
199
|
+
import { FetchError, ValidationError } from "@zap-studio/fetch/errors";
|
|
126
200
|
|
|
127
201
|
try {
|
|
128
202
|
const user = await api.get("/api/users/1", UserSchema);
|
|
129
203
|
} catch (error) {
|
|
130
204
|
if (error instanceof FetchError) {
|
|
131
|
-
console.error(`HTTP ${error.status}: ${error.statusText}`);
|
|
205
|
+
console.error(`HTTP ${error.status}: ${error.response.statusText}`);
|
|
206
|
+
}
|
|
207
|
+
if (error instanceof ValidationError) {
|
|
208
|
+
console.error("Validation failed:", error.issues);
|
|
132
209
|
}
|
|
133
210
|
}
|
|
134
211
|
```
|
|
135
212
|
|
|
136
|
-
### Flexible
|
|
213
|
+
### Flexible Validation
|
|
214
|
+
|
|
215
|
+
You can choose whether validation errors should throw exceptions:
|
|
137
216
|
|
|
138
217
|
```typescript
|
|
139
218
|
// Throw on validation error (default)
|
|
@@ -146,24 +225,9 @@ const result = await $fetch(url, UserSchema, {
|
|
|
146
225
|
throwOnValidationError: false,
|
|
147
226
|
});
|
|
148
227
|
|
|
149
|
-
if (result.
|
|
150
|
-
console.
|
|
228
|
+
if (result.issues) {
|
|
229
|
+
console.error("Validation failed:", result.issues);
|
|
151
230
|
} else {
|
|
152
|
-
console.
|
|
231
|
+
console.log("Success:", result.value);
|
|
153
232
|
}
|
|
154
233
|
```
|
|
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,29 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Error thrown for HTTP errors (non-2xx responses)
|
|
4
|
+
*/
|
|
5
|
+
var FetchError = class extends Error {
|
|
6
|
+
status;
|
|
7
|
+
response;
|
|
8
|
+
constructor(message, response) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "FetchError";
|
|
11
|
+
this.status = response.status;
|
|
12
|
+
this.response = response;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Error thrown for validation errors
|
|
17
|
+
*/
|
|
18
|
+
var ValidationError = class extends Error {
|
|
19
|
+
issues;
|
|
20
|
+
constructor(issues) {
|
|
21
|
+
super(JSON.stringify(issues, null, 2));
|
|
22
|
+
this.name = "ValidationError";
|
|
23
|
+
this.issues = issues;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
export { ValidationError as n, FetchError as t };
|
|
29
|
+
//# sourceMappingURL=errors-DKxnbFHZ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors-DKxnbFHZ.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\n/**\n * Error thrown for HTTP errors (non-2xx responses)\n */\nexport class FetchError extends Error {\n status: Response[\"status\"];\n response: Response;\n\n constructor(message: string, response: Response) {\n super(message);\n this.name = \"FetchError\";\n this.status = response.status;\n this.response = response;\n }\n}\n\n/**\n * Error thrown for validation errors\n */\nexport class ValidationError extends Error {\n issues: StandardSchemaV1.Issue[];\n\n constructor(issues: StandardSchemaV1.Issue[]) {\n super(JSON.stringify(issues, null, 2));\n this.name = \"ValidationError\";\n this.issues = issues;\n }\n}\n"],"mappings":";;;;AAKA,IAAa,aAAb,cAAgC,MAAM;CACpC;CACA;CAEA,YAAY,SAAiB,UAAoB;AAC/C,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,SAAS,SAAS;AACvB,OAAK,WAAW;;;;;;AAOpB,IAAa,kBAAb,cAAqC,MAAM;CACzC;CAEA,YAAY,QAAkC;AAC5C,QAAM,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC;AACtC,OAAK,OAAO;AACZ,OAAK,SAAS"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
|
|
3
|
+
//#region src/errors.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Error thrown for HTTP errors (non-2xx responses)
|
|
7
|
+
*/
|
|
8
|
+
declare class FetchError extends Error {
|
|
9
|
+
status: Response["status"];
|
|
10
|
+
response: Response;
|
|
11
|
+
constructor(message: string, response: Response);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Error thrown for validation errors
|
|
15
|
+
*/
|
|
16
|
+
declare class ValidationError extends Error {
|
|
17
|
+
issues: StandardSchemaV1.Issue[];
|
|
18
|
+
constructor(issues: StandardSchemaV1.Issue[]);
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { FetchError, ValidationError };
|
|
22
|
+
//# sourceMappingURL=errors.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.mts","names":[],"sources":["../src/errors.ts"],"sourcesContent":[],"mappings":";;;;;;AAKA;AACU,cADG,UAAA,SAAmB,KAAA,CACtB;EACE,MAAA,EADF,QACE,CAAA,QAAA,CAAA;EAE6B,QAAA,EAF7B,QAE6B;EAJT,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,QAAA,EAIS,QAJT;;AAehC;;;AAAqC,cAAxB,eAAA,SAAwB,KAAA,CAAA;EAAK,MAAA,EAChC,gBAAA,CAAiB,KADe,EAAA;sBAGpB,gBAAA,CAAiB"}
|
package/dist/errors.mjs
ADDED
package/dist/index.d.mts
CHANGED
|
@@ -1,37 +1,51 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import {
|
|
1
|
+
import { n as ExtendedRequestInit, t as CreateFetchOptions } from "./types-C0Uhh2KM.mjs";
|
|
2
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
3
3
|
|
|
4
4
|
//#region src/index.d.ts
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Type-safe fetch wrapper with
|
|
7
|
+
* Type-safe fetch wrapper with Standard Schema validation.
|
|
8
|
+
*
|
|
9
|
+
* - When `throwOnValidationError: true`: validated data of type `TSchema`
|
|
10
|
+
* - When `throwOnValidationError: false`: Standard Schema Result object `{ value?, issues? }`
|
|
11
|
+
* - When `throwOnFetchError: true`: throws `FetchError` on non-ok responses
|
|
12
|
+
*
|
|
13
|
+
* If no schema is provided, returns the raw `Response` object.
|
|
14
|
+
*
|
|
15
|
+
* @throws {FetchError} When `throwOnFetchError: true` and response is not ok
|
|
16
|
+
* @throws {ValidationError} When `throwOnValidationError: true` and validation fails
|
|
8
17
|
*
|
|
9
18
|
* @example
|
|
10
19
|
* import { z } from "zod";
|
|
11
20
|
* import { $fetch } from "@zap-studio/fetch";
|
|
12
21
|
*
|
|
13
|
-
* const UserSchema = z.object({
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
22
|
+
* const UserSchema = z.object({ id: z.number(), name: z.string() });
|
|
23
|
+
*
|
|
24
|
+
* // Basic usage (schema validation)
|
|
25
|
+
* const user = await $fetch("/api/users/1", UserSchema, { headers: { "Authorization": "Bearer token" } });
|
|
26
|
+
* console.log("Validated user:", user);
|
|
18
27
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
28
|
+
* // Raw usage (no schema validation and typed Response object)
|
|
29
|
+
* const result = await $fetch("/api/data", { method: "POST", body: JSON.stringify({ key: "value" }) });
|
|
30
|
+
* const json = await result.json() as ResultType;
|
|
31
|
+
* console.log("Raw response data:", json);
|
|
32
|
+
*
|
|
33
|
+
* // Usage with validation errors returned instead of thrown
|
|
34
|
+
* const result = await $fetch("/api/users/1", UserSchema, { throwOnValidationError: false });
|
|
35
|
+
*
|
|
36
|
+
* if (result.issues) {
|
|
37
|
+
* console.error("Validation errors:", result.issues);
|
|
38
|
+
* } else {
|
|
39
|
+
* console.log("Validated user:", result.value);
|
|
26
40
|
* }
|
|
27
41
|
*/
|
|
28
|
-
declare function $fetch<
|
|
29
|
-
|
|
30
|
-
responseType?: TResponseType;
|
|
31
|
-
}): Promise<TResponse | z.ZodSafeParseSuccess<TResponse> | z.ZodSafeParseError<TResponse>>;
|
|
32
|
-
declare const safeFetch: typeof $fetch;
|
|
42
|
+
declare function $fetch<TSchema extends StandardSchemaV1>(resource: string, schema: TSchema, options?: ExtendedRequestInit): Promise<StandardSchemaV1.InferOutput<TSchema> | StandardSchemaV1.Result<StandardSchemaV1.InferOutput<TSchema>>>;
|
|
43
|
+
declare function $fetch(resource: string, options?: ExtendedRequestInit): Promise<Response>;
|
|
33
44
|
/**
|
|
34
|
-
* Convenience methods for common HTTP verbs
|
|
45
|
+
* Convenience methods for common HTTP verbs.
|
|
46
|
+
*
|
|
47
|
+
* These methods always require a schema for validation.
|
|
48
|
+
* For raw responses without validation, use `$fetch` directly.
|
|
35
49
|
*
|
|
36
50
|
* @example
|
|
37
51
|
* import { z } from "zod";
|
|
@@ -44,17 +58,45 @@ declare const safeFetch: typeof $fetch;
|
|
|
44
58
|
* });
|
|
45
59
|
*
|
|
46
60
|
* async function fetchPost(postId: number) {
|
|
47
|
-
*
|
|
61
|
+
* const post = await api.get(`https://api.example.com/posts/${postId}`, PostSchema);
|
|
48
62
|
* return post; // post is typed as { id: number; title: string; content: string; }
|
|
49
63
|
* }
|
|
50
64
|
*/
|
|
51
65
|
declare const api: {
|
|
52
|
-
get: <
|
|
53
|
-
post: <
|
|
54
|
-
put: <
|
|
55
|
-
patch: <
|
|
56
|
-
delete: <
|
|
66
|
+
get: <TSchema extends StandardSchemaV1>(resource: string, schema: TSchema, options?: Omit<ExtendedRequestInit, "method">) => Promise<StandardSchemaV1.InferOutput<TSchema> | StandardSchemaV1.Result<StandardSchemaV1.InferOutput<TSchema>>>;
|
|
67
|
+
post: <TSchema extends StandardSchemaV1>(resource: string, schema: TSchema, options?: Omit<ExtendedRequestInit, "method">) => Promise<StandardSchemaV1.InferOutput<TSchema> | StandardSchemaV1.Result<StandardSchemaV1.InferOutput<TSchema>>>;
|
|
68
|
+
put: <TSchema extends StandardSchemaV1>(resource: string, schema: TSchema, options?: Omit<ExtendedRequestInit, "method">) => Promise<StandardSchemaV1.InferOutput<TSchema> | StandardSchemaV1.Result<StandardSchemaV1.InferOutput<TSchema>>>;
|
|
69
|
+
patch: <TSchema extends StandardSchemaV1>(resource: string, schema: TSchema, options?: Omit<ExtendedRequestInit, "method">) => Promise<StandardSchemaV1.InferOutput<TSchema> | StandardSchemaV1.Result<StandardSchemaV1.InferOutput<TSchema>>>;
|
|
70
|
+
delete: <TSchema extends StandardSchemaV1>(resource: string, schema: TSchema, options?: Omit<ExtendedRequestInit, "method">) => Promise<StandardSchemaV1.InferOutput<TSchema> | StandardSchemaV1.Result<StandardSchemaV1.InferOutput<TSchema>>>;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Creates a custom fetch instance with pre-configured defaults.
|
|
74
|
+
*
|
|
75
|
+
* Use this factory to create API clients with a base URL, default headers,
|
|
76
|
+
* and other shared configuration. Each instance is independent.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* import { z } from "zod";
|
|
80
|
+
* import { createFetch } from "@zap-studio/fetch";
|
|
81
|
+
*
|
|
82
|
+
* // Create a configured instance
|
|
83
|
+
* const { $fetch, api } = createFetch({
|
|
84
|
+
* baseURL: "https://api.example.com",
|
|
85
|
+
* headers: { "Authorization": "Bearer token" },
|
|
86
|
+
* });
|
|
87
|
+
*
|
|
88
|
+
* const UserSchema = z.object({ id: z.number(), name: z.string() });
|
|
89
|
+
*
|
|
90
|
+
* // Now use relative paths - baseURL is prepended automatically
|
|
91
|
+
* const user = await api.get("/users/1", UserSchema);
|
|
92
|
+
*
|
|
93
|
+
* // Or use $fetch directly
|
|
94
|
+
* const response = await $fetch("/users", UserSchema, { method: "POST", body: { name: "John" } });
|
|
95
|
+
*/
|
|
96
|
+
declare function createFetch(factoryOptions?: CreateFetchOptions): {
|
|
97
|
+
$fetch: typeof $fetch;
|
|
98
|
+
api: typeof api;
|
|
57
99
|
};
|
|
58
100
|
//#endregion
|
|
59
|
-
export { $fetch, api,
|
|
101
|
+
export { $fetch, api, createFetch };
|
|
60
102
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;AA8CA;;;;;;;;;;;AASA;;;;;AAsCA;;;;;;;;;;;;;;;;;;iBA/CsB,uBAAuB,4CAEnC,mBACE,sBACT,QACC,gBAAA,CAAiB,YAAY,WAC7B,gBAAA,CAAiB,OAAO,gBAAA,CAAiB,YAAY;iBAGnC,MAAA,6BAEV,sBACT,QAAQ;;;;;;;;;;;;;;;;;;;;;;cAmCE;;;;;;;;;;;AAgCb;;;;;;;;;;;;;;;;;;;;iBAAgB,WAAA,kBAA4B;iBAC3B;cACH"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,44 +1,116 @@
|
|
|
1
|
-
import { t as FetchError } from "./errors-
|
|
2
|
-
import { n as
|
|
1
|
+
import { t as FetchError } from "./errors-DKxnbFHZ.mjs";
|
|
2
|
+
import { n as standardValidate, t as isStandardSchema } from "./validator-BGjWrM4d.mjs";
|
|
3
3
|
|
|
4
|
-
//#region src/
|
|
4
|
+
//#region src/internal/constants.ts
|
|
5
|
+
/**
|
|
6
|
+
* Default options for the global $fetch
|
|
7
|
+
*/
|
|
8
|
+
const GLOBAL_DEFAULTS = {
|
|
9
|
+
baseURL: "",
|
|
10
|
+
headers: void 0,
|
|
11
|
+
throwOnFetchError: true,
|
|
12
|
+
throwOnValidationError: true
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/internal/utils.ts
|
|
5
17
|
/**
|
|
6
|
-
*
|
|
18
|
+
* Merges two HeadersInit objects, with the second one taking precedence
|
|
7
19
|
*
|
|
8
20
|
* @example
|
|
9
|
-
*
|
|
10
|
-
*
|
|
21
|
+
* const baseHeaders = { "Authorization": "Bearer token", "Content-Type": "application/json" };
|
|
22
|
+
* const overrideHeaders = { "Content-Type": "application/xml", "X-Custom-Header": "value" };
|
|
11
23
|
*
|
|
12
|
-
* const
|
|
13
|
-
* id: z.number(),
|
|
14
|
-
* name: z.string(),
|
|
15
|
-
* email: z.string().email(),
|
|
16
|
-
* });
|
|
24
|
+
* const merged = mergeHeaders(baseHeaders, overrideHeaders);
|
|
17
25
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
* // Resulting headers:
|
|
27
|
+
* // {
|
|
28
|
+
* // "Authorization": "Bearer token",
|
|
29
|
+
* // "Content-Type": "application/xml",
|
|
30
|
+
* // "X-Custom-Header": "value"
|
|
31
|
+
* // }
|
|
32
|
+
*/
|
|
33
|
+
function mergeHeaders(base, override) {
|
|
34
|
+
if (!(base || override)) return;
|
|
35
|
+
const merged = new Headers(base);
|
|
36
|
+
if (override) {
|
|
37
|
+
const overrideHeaders = new Headers(override);
|
|
38
|
+
for (const [key, value] of overrideHeaders.entries()) merged.set(key, value);
|
|
39
|
+
}
|
|
40
|
+
return merged;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Removes trailing slashes from a string
|
|
44
|
+
*/
|
|
45
|
+
function trimTrailingSlashes(str) {
|
|
46
|
+
let end = str.length;
|
|
47
|
+
while (end > 0 && str[end - 1] === "/") end -= 1;
|
|
48
|
+
return str.slice(0, end);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Removes leading slashes from a string
|
|
52
|
+
*/
|
|
53
|
+
function trimLeadingSlashes(str) {
|
|
54
|
+
let start = 0;
|
|
55
|
+
while (start < str.length && str[start] === "/") start += 1;
|
|
56
|
+
return str.slice(start);
|
|
57
|
+
}
|
|
58
|
+
const ABSOLUTE_URL_PATTERN = /^(?:https?:)?\/\//i;
|
|
59
|
+
/**
|
|
60
|
+
* Checks if a URL is absolute (starts with http://, https://, or //)
|
|
61
|
+
*/
|
|
62
|
+
function isAbsoluteURL(url) {
|
|
63
|
+
return ABSOLUTE_URL_PATTERN.test(url);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Internal fetch implementation used by both $fetch and createFetch
|
|
26
67
|
*/
|
|
27
|
-
async function
|
|
28
|
-
const {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
68
|
+
async function fetchInternal(resource, schema, options, defaults) {
|
|
69
|
+
const { throwOnValidationError = defaults.throwOnValidationError, throwOnFetchError = defaults.throwOnFetchError, headers: requestHeaders, ...rest } = options || {};
|
|
70
|
+
const mergedHeaders = mergeHeaders(defaults.headers, requestHeaders);
|
|
71
|
+
const init = {
|
|
31
72
|
...rest,
|
|
32
|
-
|
|
33
|
-
|
|
73
|
+
headers: mergedHeaders
|
|
74
|
+
};
|
|
75
|
+
if (schema && init.body) {
|
|
76
|
+
init.body = JSON.stringify(init.body);
|
|
77
|
+
const existingHeaders = new Headers(init.headers);
|
|
78
|
+
if (!existingHeaders.has("Content-Type")) existingHeaders.set("Content-Type", "application/json");
|
|
79
|
+
init.headers = existingHeaders;
|
|
80
|
+
}
|
|
81
|
+
let url;
|
|
82
|
+
if (isAbsoluteURL(resource)) url = resource;
|
|
83
|
+
else {
|
|
84
|
+
const base = trimTrailingSlashes(defaults.baseURL);
|
|
85
|
+
const path = trimLeadingSlashes(resource);
|
|
86
|
+
url = base ? `${base}/${path}` : resource;
|
|
87
|
+
}
|
|
88
|
+
const response = await fetch(url, init);
|
|
89
|
+
if (throwOnFetchError && !response.ok) throw new FetchError(`HTTP ${response.status}: ${response.statusText}`, response);
|
|
90
|
+
if (schema) return standardValidate(schema, await response.json(), throwOnValidationError);
|
|
91
|
+
return response;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Creates an HTTP method helper bound to a fetch function
|
|
95
|
+
*/
|
|
96
|
+
function createMethod(fetchFn, method) {
|
|
97
|
+
return (resource, schema, options) => fetchFn(resource, schema, {
|
|
98
|
+
...options,
|
|
99
|
+
method
|
|
34
100
|
});
|
|
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
101
|
}
|
|
39
|
-
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/index.ts
|
|
105
|
+
async function $fetch(resource, schemaOrOptions, optionsOrUndefined) {
|
|
106
|
+
const [schema, options] = isStandardSchema(schemaOrOptions) ? [schemaOrOptions, optionsOrUndefined] : [void 0, schemaOrOptions];
|
|
107
|
+
return await fetchInternal(resource, schema, options, GLOBAL_DEFAULTS);
|
|
108
|
+
}
|
|
40
109
|
/**
|
|
41
|
-
* Convenience methods for common HTTP verbs
|
|
110
|
+
* Convenience methods for common HTTP verbs.
|
|
111
|
+
*
|
|
112
|
+
* These methods always require a schema for validation.
|
|
113
|
+
* For raw responses without validation, use `$fetch` directly.
|
|
42
114
|
*
|
|
43
115
|
* @example
|
|
44
116
|
* import { z } from "zod";
|
|
@@ -51,36 +123,64 @@ const safeFetch = $fetch;
|
|
|
51
123
|
* });
|
|
52
124
|
*
|
|
53
125
|
* async function fetchPost(postId: number) {
|
|
54
|
-
*
|
|
126
|
+
* const post = await api.get(`https://api.example.com/posts/${postId}`, PostSchema);
|
|
55
127
|
* return post; // post is typed as { id: number; title: string; content: string; }
|
|
56
128
|
* }
|
|
57
129
|
*/
|
|
58
130
|
const api = {
|
|
59
|
-
get: (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
})
|
|
131
|
+
get: createMethod($fetch, "GET"),
|
|
132
|
+
post: createMethod($fetch, "POST"),
|
|
133
|
+
put: createMethod($fetch, "PUT"),
|
|
134
|
+
patch: createMethod($fetch, "PATCH"),
|
|
135
|
+
delete: createMethod($fetch, "DELETE")
|
|
82
136
|
};
|
|
137
|
+
/**
|
|
138
|
+
* Creates a custom fetch instance with pre-configured defaults.
|
|
139
|
+
*
|
|
140
|
+
* Use this factory to create API clients with a base URL, default headers,
|
|
141
|
+
* and other shared configuration. Each instance is independent.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* import { z } from "zod";
|
|
145
|
+
* import { createFetch } from "@zap-studio/fetch";
|
|
146
|
+
*
|
|
147
|
+
* // Create a configured instance
|
|
148
|
+
* const { $fetch, api } = createFetch({
|
|
149
|
+
* baseURL: "https://api.example.com",
|
|
150
|
+
* headers: { "Authorization": "Bearer token" },
|
|
151
|
+
* });
|
|
152
|
+
*
|
|
153
|
+
* const UserSchema = z.object({ id: z.number(), name: z.string() });
|
|
154
|
+
*
|
|
155
|
+
* // Now use relative paths - baseURL is prepended automatically
|
|
156
|
+
* const user = await api.get("/users/1", UserSchema);
|
|
157
|
+
*
|
|
158
|
+
* // Or use $fetch directly
|
|
159
|
+
* const response = await $fetch("/users", UserSchema, { method: "POST", body: { name: "John" } });
|
|
160
|
+
*/
|
|
161
|
+
function createFetch(factoryOptions = {}) {
|
|
162
|
+
const defaults = {
|
|
163
|
+
baseURL: factoryOptions.baseURL ?? "",
|
|
164
|
+
headers: factoryOptions.headers,
|
|
165
|
+
throwOnFetchError: factoryOptions.throwOnFetchError ?? true,
|
|
166
|
+
throwOnValidationError: factoryOptions.throwOnValidationError ?? true
|
|
167
|
+
};
|
|
168
|
+
async function customFetch(resource, schemaOrOptions, optionsOrUndefined) {
|
|
169
|
+
const [schema, options] = isStandardSchema(schemaOrOptions) ? [schemaOrOptions, optionsOrUndefined] : [void 0, schemaOrOptions];
|
|
170
|
+
return await fetchInternal(resource, schema, options, defaults);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
$fetch: customFetch,
|
|
174
|
+
api: {
|
|
175
|
+
get: createMethod(customFetch, "GET"),
|
|
176
|
+
post: createMethod(customFetch, "POST"),
|
|
177
|
+
put: createMethod(customFetch, "PUT"),
|
|
178
|
+
patch: createMethod(customFetch, "PATCH"),
|
|
179
|
+
delete: createMethod(customFetch, "DELETE")
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
83
183
|
|
|
84
184
|
//#endregion
|
|
85
|
-
export { $fetch, api,
|
|
185
|
+
export { $fetch, api, createFetch };
|
|
86
186
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["url: string","defaults: FetchDefaults"],"sources":["../src/internal/constants.ts","../src/internal/utils.ts","../src/index.ts"],"sourcesContent":["import type { FetchDefaults } from \"../types\";\n\n/**\n * Default options for the global $fetch\n */\nexport const GLOBAL_DEFAULTS = {\n baseURL: \"\",\n headers: undefined,\n throwOnFetchError: true,\n throwOnValidationError: true,\n} as const satisfies FetchDefaults;\n","import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { $fetch } from \"..\";\nimport { FetchError } from \"../errors\";\nimport type { ExtendedRequestInit, FetchDefaults } from \"../types\";\nimport { standardValidate } from \"../validator\";\n\n/**\n * Merges two HeadersInit objects, with the second one taking precedence\n *\n * @example\n * const baseHeaders = { \"Authorization\": \"Bearer token\", \"Content-Type\": \"application/json\" };\n * const overrideHeaders = { \"Content-Type\": \"application/xml\", \"X-Custom-Header\": \"value\" };\n *\n * const merged = mergeHeaders(baseHeaders, overrideHeaders);\n *\n * // Resulting headers:\n * // {\n * // \"Authorization\": \"Bearer token\",\n * // \"Content-Type\": \"application/xml\",\n * // \"X-Custom-Header\": \"value\"\n * // }\n */\nexport function mergeHeaders(\n base: HeadersInit | undefined,\n override: HeadersInit | undefined\n): Headers | undefined {\n if (!(base || override)) {\n return;\n }\n\n const merged = new Headers(base);\n if (override) {\n const overrideHeaders = new Headers(override);\n for (const [key, value] of overrideHeaders.entries()) {\n merged.set(key, value);\n }\n }\n return merged;\n}\n\n/**\n * Removes trailing slashes from a string\n */\nfunction trimTrailingSlashes(str: string): string {\n let end = str.length;\n while (end > 0 && str[end - 1] === \"/\") {\n end -= 1;\n }\n return str.slice(0, end);\n}\n\n/**\n * Removes leading slashes from a string\n */\nfunction trimLeadingSlashes(str: string): string {\n let start = 0;\n while (start < str.length && str[start] === \"/\") {\n start += 1;\n }\n return str.slice(start);\n}\n\nconst ABSOLUTE_URL_PATTERN = /^(?:https?:)?\\/\\//i;\n\n/**\n * Checks if a URL is absolute (starts with http://, https://, or //)\n */\nfunction isAbsoluteURL(url: string): boolean {\n return ABSOLUTE_URL_PATTERN.test(url);\n}\n\n/**\n * Internal fetch implementation used by both $fetch and createFetch\n */\nexport async function fetchInternal(\n resource: string,\n schema: StandardSchemaV1 | undefined,\n options: ExtendedRequestInit | undefined,\n defaults: FetchDefaults\n): Promise<unknown> {\n const {\n throwOnValidationError = defaults.throwOnValidationError,\n throwOnFetchError = defaults.throwOnFetchError,\n headers: requestHeaders,\n ...rest\n } = options || {};\n\n const mergedHeaders = mergeHeaders(defaults.headers, requestHeaders);\n const init = { ...rest, headers: mergedHeaders } as RequestInit;\n\n // Auto-stringify body and set default Content-Type if we have a schema\n if (schema && init.body) {\n init.body = JSON.stringify(init.body);\n\n const existingHeaders = new Headers(init.headers);\n if (!existingHeaders.has(\"Content-Type\")) {\n existingHeaders.set(\"Content-Type\", \"application/json\");\n }\n\n init.headers = existingHeaders;\n }\n\n // For absolute URLs, ignore baseURL entirely\n let url: string;\n if (isAbsoluteURL(resource)) {\n url = resource;\n } else {\n // Normalize URL by avoiding double slashes between baseURL and resource\n const base = trimTrailingSlashes(defaults.baseURL);\n const path = trimLeadingSlashes(resource);\n url = base ? `${base}/${path}` : resource;\n }\n\n const response = await fetch(url, init);\n\n if (throwOnFetchError && !response.ok) {\n throw new FetchError(\n `HTTP ${response.status}: ${response.statusText}`,\n response\n );\n }\n\n // For json with schema, validate\n if (schema) {\n const raw = await response.json();\n return standardValidate(schema, raw, throwOnValidationError);\n }\n\n // No validation, return raw response data\n return response;\n}\n\n/**\n * Creates an HTTP method helper bound to a fetch function\n */\nexport function createMethod<TFetch extends typeof $fetch>(\n fetchFn: TFetch,\n method: string\n) {\n return <TSchema extends StandardSchemaV1>(\n resource: string,\n schema: TSchema,\n options?: Omit<ExtendedRequestInit, \"method\">\n ) => fetchFn(resource, schema, { ...options, method });\n}\n","import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { GLOBAL_DEFAULTS } from \"./internal/constants\";\nimport { createMethod, fetchInternal } from \"./internal/utils\";\nimport type {\n CreateFetchOptions,\n ExtendedRequestInit,\n FetchDefaults,\n} from \"./types\";\nimport { isStandardSchema } from \"./validator\";\n\n/**\n * Type-safe fetch wrapper with Standard Schema validation.\n *\n * - When `throwOnValidationError: true`: validated data of type `TSchema`\n * - When `throwOnValidationError: false`: Standard Schema Result object `{ value?, issues? }`\n * - When `throwOnFetchError: true`: throws `FetchError` on non-ok responses\n *\n * If no schema is provided, returns the raw `Response` object.\n *\n * @throws {FetchError} When `throwOnFetchError: true` and response is not ok\n * @throws {ValidationError} When `throwOnValidationError: true` and validation fails\n *\n * @example\n * import { z } from \"zod\";\n * import { $fetch } from \"@zap-studio/fetch\";\n *\n * const UserSchema = z.object({ id: z.number(), name: z.string() });\n *\n * // Basic usage (schema validation)\n * const user = await $fetch(\"/api/users/1\", UserSchema, { headers: { \"Authorization\": \"Bearer token\" } });\n * console.log(\"Validated user:\", user);\n *\n * // Raw usage (no schema validation and typed Response object)\n * const result = await $fetch(\"/api/data\", { method: \"POST\", body: JSON.stringify({ key: \"value\" }) });\n * const json = await result.json() as ResultType;\n * console.log(\"Raw response data:\", json);\n *\n * // Usage with validation errors returned instead of thrown\n * const result = await $fetch(\"/api/users/1\", UserSchema, { throwOnValidationError: false });\n *\n * if (result.issues) {\n * console.error(\"Validation errors:\", result.issues);\n * } else {\n * console.log(\"Validated user:\", result.value);\n * }\n */\nexport async function $fetch<TSchema extends StandardSchemaV1>(\n resource: string,\n schema: TSchema,\n options?: ExtendedRequestInit\n): Promise<\n | StandardSchemaV1.InferOutput<TSchema>\n | StandardSchemaV1.Result<StandardSchemaV1.InferOutput<TSchema>>\n>;\n\nexport async function $fetch(\n resource: string,\n options?: ExtendedRequestInit\n): Promise<Response>;\n\nexport async function $fetch(\n resource: string,\n schemaOrOptions?: StandardSchemaV1 | ExtendedRequestInit,\n optionsOrUndefined?: ExtendedRequestInit\n): Promise<unknown> {\n const [schema, options] = isStandardSchema(schemaOrOptions)\n ? [schemaOrOptions, optionsOrUndefined]\n : [undefined, schemaOrOptions];\n\n return await fetchInternal(resource, schema, options, GLOBAL_DEFAULTS);\n}\n\n/**\n * Convenience methods for common HTTP verbs.\n *\n * These methods always require a schema for validation.\n * For raw responses without validation, use `$fetch` directly.\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: createMethod($fetch, \"GET\"),\n post: createMethod($fetch, \"POST\"),\n put: createMethod($fetch, \"PUT\"),\n patch: createMethod($fetch, \"PATCH\"),\n delete: createMethod($fetch, \"DELETE\"),\n};\n\n/**\n * Creates a custom fetch instance with pre-configured defaults.\n *\n * Use this factory to create API clients with a base URL, default headers,\n * and other shared configuration. Each instance is independent.\n *\n * @example\n * import { z } from \"zod\";\n * import { createFetch } from \"@zap-studio/fetch\";\n *\n * // Create a configured instance\n * const { $fetch, api } = createFetch({\n * baseURL: \"https://api.example.com\",\n * headers: { \"Authorization\": \"Bearer token\" },\n * });\n *\n * const UserSchema = z.object({ id: z.number(), name: z.string() });\n *\n * // Now use relative paths - baseURL is prepended automatically\n * const user = await api.get(\"/users/1\", UserSchema);\n *\n * // Or use $fetch directly\n * const response = await $fetch(\"/users\", UserSchema, { method: \"POST\", body: { name: \"John\" } });\n */\nexport function createFetch(factoryOptions: CreateFetchOptions = {}): {\n $fetch: typeof $fetch;\n api: typeof api;\n} {\n const defaults: FetchDefaults = {\n baseURL: factoryOptions.baseURL ?? \"\",\n headers: factoryOptions.headers,\n throwOnFetchError: factoryOptions.throwOnFetchError ?? true,\n throwOnValidationError: factoryOptions.throwOnValidationError ?? true,\n };\n\n async function customFetch<TSchema extends StandardSchemaV1>(\n resource: string,\n schema: TSchema,\n options?: ExtendedRequestInit\n ): Promise<\n | StandardSchemaV1.InferOutput<TSchema>\n | StandardSchemaV1.Result<StandardSchemaV1.InferOutput<TSchema>>\n >;\n\n async function customFetch(\n resource: string,\n options?: ExtendedRequestInit\n ): Promise<Response>;\n\n async function customFetch(\n resource: string,\n schemaOrOptions?: StandardSchemaV1 | ExtendedRequestInit,\n optionsOrUndefined?: ExtendedRequestInit\n ): Promise<unknown> {\n const [schema, options] = isStandardSchema(schemaOrOptions)\n ? [schemaOrOptions, optionsOrUndefined]\n : [undefined, schemaOrOptions];\n\n return await fetchInternal(resource, schema, options, defaults);\n }\n\n const customApi = {\n get: createMethod(customFetch, \"GET\"),\n post: createMethod(customFetch, \"POST\"),\n put: createMethod(customFetch, \"PUT\"),\n patch: createMethod(customFetch, \"PATCH\"),\n delete: createMethod(customFetch, \"DELETE\"),\n };\n\n return {\n $fetch: customFetch,\n api: customApi,\n };\n}\n"],"mappings":";;;;;;;AAKA,MAAa,kBAAkB;CAC7B,SAAS;CACT,SAAS;CACT,mBAAmB;CACnB,wBAAwB;CACzB;;;;;;;;;;;;;;;;;;;;ACYD,SAAgB,aACd,MACA,UACqB;AACrB,KAAI,EAAE,QAAQ,UACZ;CAGF,MAAM,SAAS,IAAI,QAAQ,KAAK;AAChC,KAAI,UAAU;EACZ,MAAM,kBAAkB,IAAI,QAAQ,SAAS;AAC7C,OAAK,MAAM,CAAC,KAAK,UAAU,gBAAgB,SAAS,CAClD,QAAO,IAAI,KAAK,MAAM;;AAG1B,QAAO;;;;;AAMT,SAAS,oBAAoB,KAAqB;CAChD,IAAI,MAAM,IAAI;AACd,QAAO,MAAM,KAAK,IAAI,MAAM,OAAO,IACjC,QAAO;AAET,QAAO,IAAI,MAAM,GAAG,IAAI;;;;;AAM1B,SAAS,mBAAmB,KAAqB;CAC/C,IAAI,QAAQ;AACZ,QAAO,QAAQ,IAAI,UAAU,IAAI,WAAW,IAC1C,UAAS;AAEX,QAAO,IAAI,MAAM,MAAM;;AAGzB,MAAM,uBAAuB;;;;AAK7B,SAAS,cAAc,KAAsB;AAC3C,QAAO,qBAAqB,KAAK,IAAI;;;;;AAMvC,eAAsB,cACpB,UACA,QACA,SACA,UACkB;CAClB,MAAM,EACJ,yBAAyB,SAAS,wBAClC,oBAAoB,SAAS,mBAC7B,SAAS,gBACT,GAAG,SACD,WAAW,EAAE;CAEjB,MAAM,gBAAgB,aAAa,SAAS,SAAS,eAAe;CACpE,MAAM,OAAO;EAAE,GAAG;EAAM,SAAS;EAAe;AAGhD,KAAI,UAAU,KAAK,MAAM;AACvB,OAAK,OAAO,KAAK,UAAU,KAAK,KAAK;EAErC,MAAM,kBAAkB,IAAI,QAAQ,KAAK,QAAQ;AACjD,MAAI,CAAC,gBAAgB,IAAI,eAAe,CACtC,iBAAgB,IAAI,gBAAgB,mBAAmB;AAGzD,OAAK,UAAU;;CAIjB,IAAIA;AACJ,KAAI,cAAc,SAAS,CACzB,OAAM;MACD;EAEL,MAAM,OAAO,oBAAoB,SAAS,QAAQ;EAClD,MAAM,OAAO,mBAAmB,SAAS;AACzC,QAAM,OAAO,GAAG,KAAK,GAAG,SAAS;;CAGnC,MAAM,WAAW,MAAM,MAAM,KAAK,KAAK;AAEvC,KAAI,qBAAqB,CAAC,SAAS,GACjC,OAAM,IAAI,WACR,QAAQ,SAAS,OAAO,IAAI,SAAS,cACrC,SACD;AAIH,KAAI,OAEF,QAAO,iBAAiB,QADZ,MAAM,SAAS,MAAM,EACI,uBAAuB;AAI9D,QAAO;;;;;AAMT,SAAgB,aACd,SACA,QACA;AACA,SACE,UACA,QACA,YACG,QAAQ,UAAU,QAAQ;EAAE,GAAG;EAAS;EAAQ,CAAC;;;;;ACnFxD,eAAsB,OACpB,UACA,iBACA,oBACkB;CAClB,MAAM,CAAC,QAAQ,WAAW,iBAAiB,gBAAgB,GACvD,CAAC,iBAAiB,mBAAmB,GACrC,CAAC,QAAW,gBAAgB;AAEhC,QAAO,MAAM,cAAc,UAAU,QAAQ,SAAS,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;AAwBxE,MAAa,MAAM;CACjB,KAAK,aAAa,QAAQ,MAAM;CAChC,MAAM,aAAa,QAAQ,OAAO;CAClC,KAAK,aAAa,QAAQ,MAAM;CAChC,OAAO,aAAa,QAAQ,QAAQ;CACpC,QAAQ,aAAa,QAAQ,SAAS;CACvC;;;;;;;;;;;;;;;;;;;;;;;;;AA0BD,SAAgB,YAAY,iBAAqC,EAAE,EAGjE;CACA,MAAMC,WAA0B;EAC9B,SAAS,eAAe,WAAW;EACnC,SAAS,eAAe;EACxB,mBAAmB,eAAe,qBAAqB;EACvD,wBAAwB,eAAe,0BAA0B;EAClE;CAgBD,eAAe,YACb,UACA,iBACA,oBACkB;EAClB,MAAM,CAAC,QAAQ,WAAW,iBAAiB,gBAAgB,GACvD,CAAC,iBAAiB,mBAAmB,GACrC,CAAC,QAAW,gBAAgB;AAEhC,SAAO,MAAM,cAAc,UAAU,QAAQ,SAAS,SAAS;;AAWjE,QAAO;EACL,QAAQ;EACR,KAVgB;GAChB,KAAK,aAAa,aAAa,MAAM;GACrC,MAAM,aAAa,aAAa,OAAO;GACvC,KAAK,aAAa,aAAa,MAAM;GACrC,OAAO,aAAa,aAAa,QAAQ;GACzC,QAAQ,aAAa,aAAa,SAAS;GAC5C;EAKA"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Extended RequestInit type to include custom fetch options
|
|
4
|
+
*/
|
|
5
|
+
type ExtendedRequestInit = Omit<RequestInit, "body"> & {
|
|
6
|
+
/**
|
|
7
|
+
* Request body - can be a BodyInit value or an object that will be JSON-stringified
|
|
8
|
+
*/
|
|
9
|
+
body?: BodyInit | Record<string, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* Whether to throw a FetchError on HTTP errors (non-2xx responses)
|
|
12
|
+
* @default true
|
|
13
|
+
*/
|
|
14
|
+
throwOnFetchError?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Whether to throw a ValidationError on validation errors
|
|
17
|
+
* @default true
|
|
18
|
+
*/
|
|
19
|
+
throwOnValidationError?: boolean;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Internal defaults used by fetchInternal
|
|
23
|
+
*/
|
|
24
|
+
type FetchDefaults = {
|
|
25
|
+
/**
|
|
26
|
+
* Base URL to prepend to all requests
|
|
27
|
+
* @default ""
|
|
28
|
+
*/
|
|
29
|
+
baseURL: string;
|
|
30
|
+
/**
|
|
31
|
+
* Default headers to include in all requests
|
|
32
|
+
* @default undefined
|
|
33
|
+
*/
|
|
34
|
+
headers: HeadersInit | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Whether to throw a `FetchError` on HTTP errors (non-2xx responses)
|
|
37
|
+
* @default true
|
|
38
|
+
*/
|
|
39
|
+
throwOnFetchError: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Whether to throw a `ValidationError` on validation errors
|
|
42
|
+
* @default true
|
|
43
|
+
*/
|
|
44
|
+
throwOnValidationError: boolean;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Options for creating a custom fetch instance with `createFetch`
|
|
48
|
+
*/
|
|
49
|
+
type CreateFetchOptions = Partial<FetchDefaults>;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { ExtendedRequestInit as n, FetchDefaults as r, CreateFetchOptions as t };
|
|
52
|
+
//# sourceMappingURL=types-C0Uhh2KM.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-C0Uhh2KM.d.mts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;AAGA;;AAAkC,KAAtB,mBAAA,GAAsB,IAAA,CAAK,WAAL,EAAA,MAAA,CAAA,GAAA;EAIzB;;;EAgBG,IAAA,CAAA,EAhBH,QAgBG,GAhBQ,MAgBK,CAAA,MAUd,EAAA,OAAW,CAAA;EAgBV;;;;;;;;;;;;;;KA1BA,aAAA;;;;;;;;;;WAUD;;;;;;;;;;;;;;;KAgBC,kBAAA,GAAqB,QAAQ"}
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { n as ValidationError } from "./errors-DKxnbFHZ.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/validator.ts
|
|
4
|
+
/**
|
|
5
|
+
* Type guard to check if a value is a Standard Schema schema
|
|
6
|
+
*/
|
|
7
|
+
function isStandardSchema(value) {
|
|
8
|
+
return !!value && (typeof value === "object" || typeof value === "function") && "~standard" in value;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Helper function to validate data using Standard Schema
|
|
12
|
+
*
|
|
13
|
+
* @throws {ValidationError} When `throwOnError` is true and validation fails
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* import { standardValidate } from "@zap-studio/fetch/validator";
|
|
17
|
+
* import { z } from "zod";
|
|
18
|
+
*
|
|
19
|
+
* const UserSchema = z.object({ id: z.number(), name: z.string() });
|
|
20
|
+
*
|
|
21
|
+
* // Basic usage
|
|
22
|
+
* const user = await standardValidate(UserSchema, data);
|
|
23
|
+
*
|
|
24
|
+
* // Non-throwing usage
|
|
25
|
+
* const result = await standardValidate(UserSchema, data, false);
|
|
26
|
+
* if (result.issues) {
|
|
27
|
+
* console.error("Validation failed:", result.issues);
|
|
28
|
+
* } else {
|
|
29
|
+
* console.log("Success:", result.value);
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
async function standardValidate(schema, input, throwOnError) {
|
|
33
|
+
let result = schema["~standard"].validate(input);
|
|
34
|
+
if (result instanceof Promise) result = await result;
|
|
35
|
+
if (result.issues) {
|
|
36
|
+
if (throwOnError) throw new ValidationError([...result.issues]);
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
return throwOnError ? result.value : result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
export { standardValidate as n, isStandardSchema as t };
|
|
44
|
+
//# sourceMappingURL=validator-BGjWrM4d.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator-BGjWrM4d.mjs","names":[],"sources":["../src/validator.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { ValidationError } from \"./errors\";\n\n/**\n * Type guard to check if a value is a Standard Schema schema\n */\nexport function isStandardSchema(value: unknown): value is StandardSchemaV1 {\n return (\n !!value &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n \"~standard\" in value\n );\n}\n\n/**\n * Helper function to validate data using Standard Schema\n *\n * @throws {ValidationError} When `throwOnError` is true and validation fails\n *\n * @example\n * import { standardValidate } from \"@zap-studio/fetch/validator\";\n * import { z } from \"zod\";\n *\n * const UserSchema = z.object({ id: z.number(), name: z.string() });\n *\n * // Basic usage\n * const user = await standardValidate(UserSchema, data);\n *\n * // Non-throwing usage\n * const result = await standardValidate(UserSchema, data, false);\n * if (result.issues) {\n * console.error(\"Validation failed:\", result.issues);\n * } else {\n * console.log(\"Success:\", result.value);\n * }\n */\nexport async function standardValidate<TSchema extends StandardSchemaV1>(\n schema: TSchema,\n input: unknown,\n throwOnError: boolean\n): Promise<\n | StandardSchemaV1.InferOutput<TSchema>\n | StandardSchemaV1.Result<StandardSchemaV1.InferOutput<TSchema>>\n> {\n let result = schema[\"~standard\"].validate(input);\n if (result instanceof Promise) {\n result = await result;\n }\n\n if (result.issues) {\n if (throwOnError) {\n throw new ValidationError([...result.issues]);\n }\n return result;\n }\n\n return throwOnError ? result.value : result;\n}\n"],"mappings":";;;;;;AAMA,SAAgB,iBAAiB,OAA2C;AAC1E,QACE,CAAC,CAAC,UACD,OAAO,UAAU,YAAY,OAAO,UAAU,eAC/C,eAAe;;;;;;;;;;;;;;;;;;;;;;;;AA0BnB,eAAsB,iBACpB,QACA,OACA,cAIA;CACA,IAAI,SAAS,OAAO,aAAa,SAAS,MAAM;AAChD,KAAI,kBAAkB,QACpB,UAAS,MAAM;AAGjB,KAAI,OAAO,QAAQ;AACjB,MAAI,aACF,OAAM,IAAI,gBAAgB,CAAC,GAAG,OAAO,OAAO,CAAC;AAE/C,SAAO;;AAGT,QAAO,eAAe,OAAO,QAAQ"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
|
|
3
|
+
//#region src/validator.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Type guard to check if a value is a Standard Schema schema
|
|
7
|
+
*/
|
|
8
|
+
declare function isStandardSchema(value: unknown): value is StandardSchemaV1;
|
|
9
|
+
/**
|
|
10
|
+
* Helper function to validate data using Standard Schema
|
|
11
|
+
*
|
|
12
|
+
* @throws {ValidationError} When `throwOnError` is true and validation fails
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* import { standardValidate } from "@zap-studio/fetch/validator";
|
|
16
|
+
* import { z } from "zod";
|
|
17
|
+
*
|
|
18
|
+
* const UserSchema = z.object({ id: z.number(), name: z.string() });
|
|
19
|
+
*
|
|
20
|
+
* // Basic usage
|
|
21
|
+
* const user = await standardValidate(UserSchema, data);
|
|
22
|
+
*
|
|
23
|
+
* // Non-throwing usage
|
|
24
|
+
* const result = await standardValidate(UserSchema, data, false);
|
|
25
|
+
* if (result.issues) {
|
|
26
|
+
* console.error("Validation failed:", result.issues);
|
|
27
|
+
* } else {
|
|
28
|
+
* console.log("Success:", result.value);
|
|
29
|
+
* }
|
|
30
|
+
*/
|
|
31
|
+
declare function standardValidate<TSchema extends StandardSchemaV1>(schema: TSchema, input: unknown, throwOnError: boolean): Promise<StandardSchemaV1.InferOutput<TSchema> | StandardSchemaV1.Result<StandardSchemaV1.InferOutput<TSchema>>>;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { isStandardSchema, standardValidate };
|
|
34
|
+
//# sourceMappingURL=validator.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.d.mts","names":[],"sources":["../src/validator.ts"],"sourcesContent":[],"mappings":";;;;;;AAMA;AA8BsB,iBA9BN,gBAAA,CA8BsB,KAAA,EAAA,OAAA,CAAA,EAAA,KAAA,IA9BqB,gBA8BrB;;;;;;;;;;;;;;;;;;;;;;;iBAAhB,iCAAiC,0BAC7C,iDAGP,QACC,gBAAA,CAAiB,YAAY,WAC7B,gBAAA,CAAiB,OAAO,gBAAA,CAAiB,YAAY"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zap-studio/fetch",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -12,24 +12,27 @@
|
|
|
12
12
|
"CHANGELOG.md"
|
|
13
13
|
],
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"
|
|
15
|
+
"@standard-schema/spec": "^1.0.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@types/node": "
|
|
19
|
-
"@vitest/coverage-v8": "^4.0.
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
18
|
+
"@types/node": "^24.10.1",
|
|
19
|
+
"@vitest/coverage-v8": "^4.0.14",
|
|
20
|
+
"arktype": "^2.1.27",
|
|
21
|
+
"jsdom": "^27.2.0",
|
|
22
|
+
"tsdown": "^0.17.0-beta.4",
|
|
23
|
+
"typescript": "^5.9.3",
|
|
24
|
+
"valibot": "^1.2.0",
|
|
25
|
+
"vitest": "^4.0.14",
|
|
26
|
+
"zod": "^4.1.13",
|
|
24
27
|
"@zap-studio/tsdown-config": "0.0.0",
|
|
25
28
|
"@zap-studio/typescript-config": "0.0.0",
|
|
26
29
|
"@zap-studio/vitest-config": "0.0.0"
|
|
27
30
|
},
|
|
28
31
|
"exports": {
|
|
29
32
|
".": "./dist/index.mjs",
|
|
30
|
-
"./errors": "./dist/errors
|
|
31
|
-
"./types": "./dist/types
|
|
32
|
-
"./
|
|
33
|
+
"./errors": "./dist/errors.mjs",
|
|
34
|
+
"./types": "./dist/types.mjs",
|
|
35
|
+
"./validator": "./dist/validator.mjs",
|
|
33
36
|
"./package.json": "./package.json"
|
|
34
37
|
},
|
|
35
38
|
"main": "./dist/index.mjs",
|
package/dist/errors/index.d.mts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/errors/index.mjs
DELETED
package/dist/errors-BCMOymYz.mjs
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,18 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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/types/index.d.mts
DELETED
package/dist/utils/index.d.mts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/utils/index.mjs
DELETED
package/dist/utils-CZ-1Z2-1.mjs
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
File without changes
|