@tahanabavi/typefetch 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +36 -0
- package/README.md +182 -182
- package/dist/__tests__/client.test.d.ts +1 -0
- package/dist/__tests__/client.test.js +108 -0
- package/dist/__tests__/middlewares.test.d.ts +1 -0
- package/dist/__tests__/middlewares.test.js +85 -0
- package/dist/client.d.ts +31 -0
- package/dist/client.js +98 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +22 -0
- package/dist/middlewares/auth.d.ts +5 -0
- package/dist/middlewares/auth.js +17 -0
- package/dist/middlewares/cache.d.ts +5 -0
- package/dist/middlewares/cache.js +25 -0
- package/dist/middlewares/logging.d.ts +7 -0
- package/dist/middlewares/logging.js +13 -0
- package/dist/middlewares/retry.d.ts +6 -0
- package/dist/middlewares/retry.js +22 -0
- package/jest.config.ts +18 -18
- package/package.json +1 -1
- package/src/__tests__/client.test.ts +137 -137
- package/src/__tests__/middlewares.test.ts +108 -108
- package/src/client.ts +142 -142
- package/src/index.ts +6 -6
- package/src/middlewares/auth.ts +19 -19
- package/src/middlewares/cache.ts +26 -26
- package/src/middlewares/logging.ts +19 -19
- package/src/middlewares/retry.ts +25 -25
- package/src/types.d.ts +46 -46
- package/tsconfig.json +13 -13
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Publish Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
paths:
|
|
8
|
+
- 'package.json'
|
|
9
|
+
- 'src/**'
|
|
10
|
+
- 'README.md'
|
|
11
|
+
- '.github/workflows/publish.yml'
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build-and-publish:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout repository
|
|
19
|
+
uses: actions/checkout@v3
|
|
20
|
+
|
|
21
|
+
- name: Setup Node.js
|
|
22
|
+
uses: actions/setup-node@v3
|
|
23
|
+
with:
|
|
24
|
+
node-version: 20
|
|
25
|
+
registry-url: https://registry.npmjs.org/
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: npm install
|
|
29
|
+
|
|
30
|
+
- name: Build package
|
|
31
|
+
run: npm run build
|
|
32
|
+
|
|
33
|
+
- name: Publish to npm
|
|
34
|
+
run: npm publish --access public
|
|
35
|
+
env:
|
|
36
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/README.md
CHANGED
|
@@ -1,183 +1,183 @@
|
|
|
1
|
-
# TypeFetch
|
|
2
|
-
|
|
3
|
-
TypeFetch is a type-safe client for working with APIs, built with TypeScript and Zod. This project allows you to define API contracts and safely use types, while also supporting middlewares, error handling, and response transformation.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Features
|
|
8
|
-
|
|
9
|
-
* Fully type-safe using TypeScript and Zod
|
|
10
|
-
* Define contracts for modules and endpoints
|
|
11
|
-
* Support for middlewares to add custom behavior before or after requests
|
|
12
|
-
* Error handling with the `RichError` class
|
|
13
|
-
* Ability to transform responses using a response transformer
|
|
14
|
-
* Authentication support via token
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
## Installation
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
npm install typefetch
|
|
22
|
-
# or
|
|
23
|
-
yarn add typefetch
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Defining Contracts
|
|
29
|
-
|
|
30
|
-
Contracts are defined using the `Contracts` and `EndpointDef` types
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
import { z } from "zod";
|
|
34
|
-
|
|
35
|
-
const contracts = {
|
|
36
|
-
user: {
|
|
37
|
-
getUser: {
|
|
38
|
-
method: "GET",
|
|
39
|
-
path: "/user/:id",
|
|
40
|
-
auth: true,
|
|
41
|
-
request: z.object({ id: z.string() }),
|
|
42
|
-
response: z.object({ id: z.string(), name: z.string() }),
|
|
43
|
-
},
|
|
44
|
-
createUser: {
|
|
45
|
-
method: "POST",
|
|
46
|
-
path: "/user",
|
|
47
|
-
request: z.object({ name: z.string() }),
|
|
48
|
-
response: z.object({ id: z.string(), name: z.string() }),
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
} as const;
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## Using `ApiClient`
|
|
57
|
-
|
|
58
|
-
```ts
|
|
59
|
-
import { ApiClient, RichError } from "typefetch";
|
|
60
|
-
|
|
61
|
-
const client = new ApiClient(
|
|
62
|
-
{
|
|
63
|
-
baseUrl: "https://api.example.com",
|
|
64
|
-
token: "your-auth-token",
|
|
65
|
-
},
|
|
66
|
-
contracts
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
client.init();
|
|
70
|
-
|
|
71
|
-
const user = await client.user.getUser({ id: "123" });
|
|
72
|
-
const newUser = await client.user.createUser({ name: "Taha" });
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## Error Handling
|
|
78
|
-
|
|
79
|
-
All errors are provided via the `RichError` class. You can define a custom error handler:
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
client.onError((error: RichError) => {
|
|
83
|
-
console.error("API Error:", error.message, error.status);
|
|
84
|
-
});
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
---
|
|
88
|
-
|
|
89
|
-
## Middlewares
|
|
90
|
-
|
|
91
|
-
You can add custom behavior before or after requests. Middlewares work similarly to Express:
|
|
92
|
-
|
|
93
|
-
```ts
|
|
94
|
-
client.use(async (ctx, next, options) => {
|
|
95
|
-
console.log("Request URL:", ctx.url);
|
|
96
|
-
const response = await next();
|
|
97
|
-
console.log("Response status:", response.status);
|
|
98
|
-
return response;
|
|
99
|
-
});
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Built-in Middlewares
|
|
103
|
-
|
|
104
|
-
This project provides some built-in middlewares:
|
|
105
|
-
|
|
106
|
-
```ts
|
|
107
|
-
import {
|
|
108
|
-
LoggingMiddleware,
|
|
109
|
-
RetryMiddleware,
|
|
110
|
-
AuthMiddleware,
|
|
111
|
-
CacheMiddleware,
|
|
112
|
-
} from "typefetch/middlewares";
|
|
113
|
-
|
|
114
|
-
client.use(LoggingMiddleware);
|
|
115
|
-
client.use(RetryMiddleware, { maxRetries: 3, delay: 100 });
|
|
116
|
-
client.use(AuthMiddleware, { refreshToken: () => "your-auth-token" });
|
|
117
|
-
client.use(CacheMiddleware, { ttl: 60 * 1000 });
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
* `LoggingMiddleware` – Logs requests and responses
|
|
121
|
-
* `RetryMiddleware` – Retries failed requests
|
|
122
|
-
* `AuthMiddleware` – Automatically adds Authorization headers
|
|
123
|
-
* `CacheMiddleware` – Caches responses to reduce repeated requests
|
|
124
|
-
|
|
125
|
-
---
|
|
126
|
-
|
|
127
|
-
## Response Transformation
|
|
128
|
-
|
|
129
|
-
You can transform the response format before returning it:
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
client.useResponseTransform((data) => {
|
|
133
|
-
return { ...data, fetchedAt: new Date() };
|
|
134
|
-
});
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
---
|
|
138
|
-
|
|
139
|
-
## Important Notes
|
|
140
|
-
|
|
141
|
-
* Always call `client.init()` before using endpoints.
|
|
142
|
-
* Types are automatically inferred from Zod, making inputs and outputs type-safe.
|
|
143
|
-
* Middleware execution order: first added middleware runs last, last added middleware runs first.
|
|
144
|
-
|
|
145
|
-
---
|
|
146
|
-
|
|
147
|
-
## Full Example
|
|
148
|
-
|
|
149
|
-
```ts
|
|
150
|
-
import { z } from "zod";
|
|
151
|
-
import { ApiClient, RichError } from "typefetch";
|
|
152
|
-
import { LoggingMiddleware, RetryMiddleware } from "typefetch/middlewares";
|
|
153
|
-
|
|
154
|
-
const contracts = {
|
|
155
|
-
post: {
|
|
156
|
-
getPost: {
|
|
157
|
-
method: "GET",
|
|
158
|
-
path: "/posts/:id",
|
|
159
|
-
auth: true,
|
|
160
|
-
request: z.object({ id: z.string() }),
|
|
161
|
-
response: z.object({ id: z.string(), title: z.string() }),
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
} as const;
|
|
165
|
-
|
|
166
|
-
const client = new ApiClient(
|
|
167
|
-
{ baseUrl: "https://api.example.com", token: "abc123" },
|
|
168
|
-
contracts
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
client.init();
|
|
172
|
-
client.use(LoggingMiddleware);
|
|
173
|
-
client.use(RetryMiddleware, { maxRetries: 2 });
|
|
174
|
-
|
|
175
|
-
client.onError((err: RichError) => {
|
|
176
|
-
console.error("Error:", err.message);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
(async () => {
|
|
180
|
-
const post = await client.post.getPost({ id: "1" });
|
|
181
|
-
console.log(post);
|
|
182
|
-
})();
|
|
1
|
+
# TypeFetch
|
|
2
|
+
|
|
3
|
+
TypeFetch is a type-safe client for working with APIs, built with TypeScript and Zod. This project allows you to define API contracts and safely use types, while also supporting middlewares, error handling, and response transformation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
* Fully type-safe using TypeScript and Zod
|
|
10
|
+
* Define contracts for modules and endpoints
|
|
11
|
+
* Support for middlewares to add custom behavior before or after requests
|
|
12
|
+
* Error handling with the `RichError` class
|
|
13
|
+
* Ability to transform responses using a response transformer
|
|
14
|
+
* Authentication support via token
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @tahanabavi/typefetch
|
|
22
|
+
# or
|
|
23
|
+
yarn add @tahanabavi/typefetch
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Defining Contracts
|
|
29
|
+
|
|
30
|
+
Contracts are defined using the `Contracts` and `EndpointDef` types
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
|
|
35
|
+
const contracts = {
|
|
36
|
+
user: {
|
|
37
|
+
getUser: {
|
|
38
|
+
method: "GET",
|
|
39
|
+
path: "/user/:id",
|
|
40
|
+
auth: true,
|
|
41
|
+
request: z.object({ id: z.string() }),
|
|
42
|
+
response: z.object({ id: z.string(), name: z.string() }),
|
|
43
|
+
},
|
|
44
|
+
createUser: {
|
|
45
|
+
method: "POST",
|
|
46
|
+
path: "/user",
|
|
47
|
+
request: z.object({ name: z.string() }),
|
|
48
|
+
response: z.object({ id: z.string(), name: z.string() }),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
} as const;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Using `ApiClient`
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { ApiClient, RichError } from "typefetch";
|
|
60
|
+
|
|
61
|
+
const client = new ApiClient(
|
|
62
|
+
{
|
|
63
|
+
baseUrl: "https://api.example.com",
|
|
64
|
+
token: "your-auth-token",
|
|
65
|
+
},
|
|
66
|
+
contracts
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
client.init();
|
|
70
|
+
|
|
71
|
+
const user = await client.user.getUser({ id: "123" });
|
|
72
|
+
const newUser = await client.user.createUser({ name: "Taha" });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Error Handling
|
|
78
|
+
|
|
79
|
+
All errors are provided via the `RichError` class. You can define a custom error handler:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
client.onError((error: RichError) => {
|
|
83
|
+
console.error("API Error:", error.message, error.status);
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Middlewares
|
|
90
|
+
|
|
91
|
+
You can add custom behavior before or after requests. Middlewares work similarly to Express:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
client.use(async (ctx, next, options) => {
|
|
95
|
+
console.log("Request URL:", ctx.url);
|
|
96
|
+
const response = await next();
|
|
97
|
+
console.log("Response status:", response.status);
|
|
98
|
+
return response;
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Built-in Middlewares
|
|
103
|
+
|
|
104
|
+
This project provides some built-in middlewares:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import {
|
|
108
|
+
LoggingMiddleware,
|
|
109
|
+
RetryMiddleware,
|
|
110
|
+
AuthMiddleware,
|
|
111
|
+
CacheMiddleware,
|
|
112
|
+
} from "typefetch/middlewares";
|
|
113
|
+
|
|
114
|
+
client.use(LoggingMiddleware);
|
|
115
|
+
client.use(RetryMiddleware, { maxRetries: 3, delay: 100 });
|
|
116
|
+
client.use(AuthMiddleware, { refreshToken: () => "your-auth-token" });
|
|
117
|
+
client.use(CacheMiddleware, { ttl: 60 * 1000 });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
* `LoggingMiddleware` – Logs requests and responses
|
|
121
|
+
* `RetryMiddleware` – Retries failed requests
|
|
122
|
+
* `AuthMiddleware` – Automatically adds Authorization headers
|
|
123
|
+
* `CacheMiddleware` – Caches responses to reduce repeated requests
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Response Transformation
|
|
128
|
+
|
|
129
|
+
You can transform the response format before returning it:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
client.useResponseTransform((data) => {
|
|
133
|
+
return { ...data, fetchedAt: new Date() };
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Important Notes
|
|
140
|
+
|
|
141
|
+
* Always call `client.init()` before using endpoints.
|
|
142
|
+
* Types are automatically inferred from Zod, making inputs and outputs type-safe.
|
|
143
|
+
* Middleware execution order: first added middleware runs last, last added middleware runs first.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Full Example
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { z } from "zod";
|
|
151
|
+
import { ApiClient, RichError } from "typefetch";
|
|
152
|
+
import { LoggingMiddleware, RetryMiddleware } from "typefetch/middlewares";
|
|
153
|
+
|
|
154
|
+
const contracts = {
|
|
155
|
+
post: {
|
|
156
|
+
getPost: {
|
|
157
|
+
method: "GET",
|
|
158
|
+
path: "/posts/:id",
|
|
159
|
+
auth: true,
|
|
160
|
+
request: z.object({ id: z.string() }),
|
|
161
|
+
response: z.object({ id: z.string(), title: z.string() }),
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
} as const;
|
|
165
|
+
|
|
166
|
+
const client = new ApiClient(
|
|
167
|
+
{ baseUrl: "https://api.example.com", token: "abc123" },
|
|
168
|
+
contracts
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
client.init();
|
|
172
|
+
client.use(LoggingMiddleware);
|
|
173
|
+
client.use(RetryMiddleware, { maxRetries: 2 });
|
|
174
|
+
|
|
175
|
+
client.onError((err: RichError) => {
|
|
176
|
+
console.error("Error:", err.message);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
(async () => {
|
|
180
|
+
const post = await client.post.getPost({ id: "1" });
|
|
181
|
+
console.log(post);
|
|
182
|
+
})();
|
|
183
183
|
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const zod_1 = require("zod");
|
|
4
|
+
const client_1 = require("../client");
|
|
5
|
+
// Mock fetch globally
|
|
6
|
+
global.fetch = jest.fn();
|
|
7
|
+
const contracts = {
|
|
8
|
+
user: {
|
|
9
|
+
getUser: {
|
|
10
|
+
method: "GET",
|
|
11
|
+
path: "/user",
|
|
12
|
+
request: zod_1.z.object({ id: zod_1.z.string() }),
|
|
13
|
+
response: zod_1.z.object({ id: zod_1.z.string(), name: zod_1.z.string() }),
|
|
14
|
+
},
|
|
15
|
+
createUser: {
|
|
16
|
+
method: "POST",
|
|
17
|
+
path: "/user",
|
|
18
|
+
auth: true,
|
|
19
|
+
request: zod_1.z.object({ name: zod_1.z.string() }),
|
|
20
|
+
response: zod_1.z.object({ id: zod_1.z.string(), name: zod_1.z.string() }),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
describe("ApiClient", () => {
|
|
25
|
+
let client;
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.clearAllMocks();
|
|
28
|
+
client = new client_1.ApiClient({ baseUrl: "https://api.test.com" }, contracts);
|
|
29
|
+
client.init();
|
|
30
|
+
});
|
|
31
|
+
it("should initialize modules correctly", () => {
|
|
32
|
+
expect(client.modules.user).toBeDefined();
|
|
33
|
+
expect(typeof client.modules.user.getUser).toBe("function");
|
|
34
|
+
});
|
|
35
|
+
it("should call fetch with correct URL and headers", async () => {
|
|
36
|
+
fetch.mockResolvedValueOnce({
|
|
37
|
+
ok: true,
|
|
38
|
+
json: async () => ({ id: "1", name: "John" }),
|
|
39
|
+
});
|
|
40
|
+
const res = await client.modules.user.getUser({ id: "1" });
|
|
41
|
+
expect(fetch).toHaveBeenCalledWith("https://api.test.com/user", {
|
|
42
|
+
method: "GET",
|
|
43
|
+
headers: { "Content-Type": "application/json" },
|
|
44
|
+
body: undefined,
|
|
45
|
+
});
|
|
46
|
+
expect(res).toEqual({ id: "1", name: "John" });
|
|
47
|
+
});
|
|
48
|
+
it("should throw validation error if input is invalid", async () => {
|
|
49
|
+
await expect(client.modules.user.getUser({}))
|
|
50
|
+
.rejects.toBeInstanceOf(zod_1.ZodError);
|
|
51
|
+
});
|
|
52
|
+
it("should handle auth header when token is provided", async () => {
|
|
53
|
+
const authedClient = new client_1.ApiClient({ baseUrl: "https://api.test.com", token: "mytoken" }, contracts);
|
|
54
|
+
authedClient.init();
|
|
55
|
+
fetch.mockResolvedValueOnce({
|
|
56
|
+
ok: true,
|
|
57
|
+
json: async () => ({ id: "2", name: "Alice" }),
|
|
58
|
+
});
|
|
59
|
+
await authedClient.modules.user.createUser({ name: "Alice" });
|
|
60
|
+
expect(fetch).toHaveBeenCalledWith("https://api.test.com/user", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: {
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
Authorization: "Bearer mytoken",
|
|
65
|
+
},
|
|
66
|
+
body: JSON.stringify({ name: "Alice" }),
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
it("should throw error if auth required and no token provided", async () => {
|
|
70
|
+
await expect(client.modules.user.createUser({ name: "Alice" })).rejects.toThrow(client_1.RichError);
|
|
71
|
+
});
|
|
72
|
+
it("should call errorHandler when error occurs", async () => {
|
|
73
|
+
const handler = jest.fn();
|
|
74
|
+
client.onError(handler);
|
|
75
|
+
fetch.mockResolvedValueOnce({
|
|
76
|
+
ok: false,
|
|
77
|
+
status: 400,
|
|
78
|
+
statusText: "Bad Request",
|
|
79
|
+
json: async () => ({ message: "Invalid input" }),
|
|
80
|
+
});
|
|
81
|
+
await expect(client.modules.user.getUser({ id: "bad" })).rejects.toThrow();
|
|
82
|
+
expect(handler).toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
it("should apply responseTransform", async () => {
|
|
85
|
+
client.useResponseTransform((data) => ({ ...data, transformed: true }));
|
|
86
|
+
fetch.mockResolvedValueOnce({
|
|
87
|
+
ok: true,
|
|
88
|
+
json: async () => ({ id: "1", name: "John" }),
|
|
89
|
+
});
|
|
90
|
+
const res = await client.modules.user.getUser({ id: "1" });
|
|
91
|
+
expect(res).toEqual({ id: "1", name: "John", transformed: true });
|
|
92
|
+
});
|
|
93
|
+
it("should execute middleware in order", async () => {
|
|
94
|
+
const logs = [];
|
|
95
|
+
client.use(async (ctx, next) => {
|
|
96
|
+
logs.push("before");
|
|
97
|
+
const res = await next();
|
|
98
|
+
logs.push("after");
|
|
99
|
+
return res;
|
|
100
|
+
});
|
|
101
|
+
fetch.mockResolvedValueOnce({
|
|
102
|
+
ok: true,
|
|
103
|
+
json: async () => ({ id: "1", name: "John" }),
|
|
104
|
+
});
|
|
105
|
+
await client.modules.user.getUser({ id: "1" });
|
|
106
|
+
expect(logs).toEqual(["before", "after"]);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const auth_1 = require("../middlewares/auth");
|
|
4
|
+
const cache_1 = require("../middlewares/cache");
|
|
5
|
+
const logging_1 = require("../middlewares/logging");
|
|
6
|
+
const retry_1 = require("../middlewares/retry");
|
|
7
|
+
describe("middlewares", () => {
|
|
8
|
+
const mockCtx = (url = "/test", method = "GET") => ({
|
|
9
|
+
url,
|
|
10
|
+
init: { method, headers: {} },
|
|
11
|
+
});
|
|
12
|
+
const mockNext = (response = { ok: true, status: 200 }) => jest.fn().mockResolvedValue(new Response(JSON.stringify(response)));
|
|
13
|
+
// ---------------- AUTH ----------------
|
|
14
|
+
it("authMiddleware should add refreshed token", async () => {
|
|
15
|
+
const ctx = mockCtx();
|
|
16
|
+
const next = mockNext();
|
|
17
|
+
await (0, auth_1.authMiddleware)(ctx, next, {
|
|
18
|
+
refreshToken: async () => "NEW_TOKEN",
|
|
19
|
+
});
|
|
20
|
+
expect(ctx.init.headers["Authorization"]).toBe("Bearer NEW_TOKEN");
|
|
21
|
+
expect(next).toHaveBeenCalled();
|
|
22
|
+
});
|
|
23
|
+
it("authMiddleware should skip if no refreshToken provided", async () => {
|
|
24
|
+
const ctx = mockCtx();
|
|
25
|
+
const next = mockNext();
|
|
26
|
+
await (0, auth_1.authMiddleware)(ctx, next, {});
|
|
27
|
+
expect(ctx.init.headers["Authorization"]).toBeUndefined();
|
|
28
|
+
expect(next).toHaveBeenCalled();
|
|
29
|
+
});
|
|
30
|
+
// ---------------- CACHE ----------------
|
|
31
|
+
it("cacheMiddleware should cache GET responses", async () => {
|
|
32
|
+
const ctx = mockCtx("/users", "GET");
|
|
33
|
+
const next = mockNext({ users: [1, 2, 3] });
|
|
34
|
+
const middleware = (0, cache_1.cacheMiddleware)({ ttl: 1000 });
|
|
35
|
+
const res1 = await middleware(ctx, next);
|
|
36
|
+
const res2 = await middleware(ctx, next);
|
|
37
|
+
expect(next).toHaveBeenCalledTimes(1); // only first time
|
|
38
|
+
const data2 = await res2.json();
|
|
39
|
+
expect(data2.users).toEqual([1, 2, 3]);
|
|
40
|
+
});
|
|
41
|
+
it("cacheMiddleware should bypass cache for non-GET requests", async () => {
|
|
42
|
+
const ctx = mockCtx("/users", "POST");
|
|
43
|
+
const next = mockNext({ ok: true });
|
|
44
|
+
const middleware = (0, cache_1.cacheMiddleware)();
|
|
45
|
+
await middleware(ctx, next);
|
|
46
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
47
|
+
});
|
|
48
|
+
// ---------------- LOGGING ----------------
|
|
49
|
+
it("loggingMiddleware should log request and response", async () => {
|
|
50
|
+
const ctx = mockCtx();
|
|
51
|
+
const next = mockNext();
|
|
52
|
+
const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
|
|
53
|
+
await (0, logging_1.loggingMiddleware)(ctx, next, {
|
|
54
|
+
logRequest: true,
|
|
55
|
+
logResponse: true,
|
|
56
|
+
debug: true,
|
|
57
|
+
});
|
|
58
|
+
expect(logSpy).toHaveBeenCalledWith("➡️ Request:", ctx.url, ctx.init);
|
|
59
|
+
expect(logSpy).toHaveBeenCalledWith("⬅️ Response:", 200);
|
|
60
|
+
logSpy.mockRestore();
|
|
61
|
+
});
|
|
62
|
+
// ---------------- RETRY ----------------
|
|
63
|
+
it("retryMiddleware should retry failed requests", async () => {
|
|
64
|
+
const ctx = mockCtx();
|
|
65
|
+
let attempt = 0;
|
|
66
|
+
const next = jest.fn().mockImplementation(() => {
|
|
67
|
+
attempt++;
|
|
68
|
+
if (attempt < 2)
|
|
69
|
+
throw new Error("fail");
|
|
70
|
+
return Promise.resolve(new Response(JSON.stringify({ ok: true })));
|
|
71
|
+
});
|
|
72
|
+
const middleware = (0, retry_1.retryMiddleware)({ maxRetries: 3, delay: 10 });
|
|
73
|
+
const res = await middleware(ctx, next);
|
|
74
|
+
const json = await res.json();
|
|
75
|
+
expect(json.ok).toBe(true);
|
|
76
|
+
expect(next).toHaveBeenCalledTimes(2);
|
|
77
|
+
});
|
|
78
|
+
it("retryMiddleware should throw after exceeding maxRetries", async () => {
|
|
79
|
+
const ctx = mockCtx();
|
|
80
|
+
const next = jest.fn().mockRejectedValue(new Error("fail always"));
|
|
81
|
+
const middleware = (0, retry_1.retryMiddleware)({ maxRetries: 2, delay: 10 });
|
|
82
|
+
await expect(middleware(ctx, next)).rejects.toThrow("fail always");
|
|
83
|
+
expect(next).toHaveBeenCalledTimes(3); // initial + 2 retries
|
|
84
|
+
});
|
|
85
|
+
});
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Contracts, Middleware, ErrorLike, EndpointMethods } from "./types";
|
|
2
|
+
export declare class RichError extends Error implements ErrorLike {
|
|
3
|
+
status?: number;
|
|
4
|
+
code?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
detail?: string;
|
|
7
|
+
errors?: Record<string, string[]>;
|
|
8
|
+
constructor(error: Partial<ErrorLike> & {
|
|
9
|
+
message: string;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export declare class ApiClient<C extends Contracts, E extends ErrorLike = RichError> {
|
|
13
|
+
private config;
|
|
14
|
+
private contracts;
|
|
15
|
+
private middlewares;
|
|
16
|
+
private errorHandler?;
|
|
17
|
+
private responseTransform;
|
|
18
|
+
private _modules;
|
|
19
|
+
constructor(config: {
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
token?: string;
|
|
22
|
+
}, contracts: C);
|
|
23
|
+
init(): void;
|
|
24
|
+
get modules(): { [M in keyof C]: EndpointMethods<C[M]>; };
|
|
25
|
+
use<T>(middleware: Middleware<T>, options?: T): void;
|
|
26
|
+
onError(handler: (error: E) => void): void;
|
|
27
|
+
useResponseTransform(fn: (data: any) => any): void;
|
|
28
|
+
private request;
|
|
29
|
+
private createError;
|
|
30
|
+
private normalizeError;
|
|
31
|
+
}
|