@tahanabavi/typefetch 1.0.0 → 1.0.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/.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
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
import { authMiddleware } from "../middlewares/auth";
|
|
2
|
-
import { cacheMiddleware } from "../middlewares/cache";
|
|
3
|
-
import { loggingMiddleware } from "../middlewares/logging";
|
|
4
|
-
import { retryMiddleware } from "../middlewares/retry";
|
|
5
|
-
|
|
6
|
-
describe("middlewares", () => {
|
|
7
|
-
const mockCtx = (url = "/test", method = "GET") => ({
|
|
8
|
-
url,
|
|
9
|
-
init: { method, headers: {} as Record<string, string> },
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
const mockNext = (response: any = { ok: true, status: 200 }) =>
|
|
13
|
-
jest.fn().mockResolvedValue(new Response(JSON.stringify(response)));
|
|
14
|
-
|
|
15
|
-
// ---------------- AUTH ----------------
|
|
16
|
-
it("authMiddleware should add refreshed token", async () => {
|
|
17
|
-
const ctx = mockCtx();
|
|
18
|
-
const next = mockNext();
|
|
19
|
-
|
|
20
|
-
await authMiddleware(ctx, next, {
|
|
21
|
-
refreshToken: async () => "NEW_TOKEN",
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
expect(ctx.init.headers["Authorization"]).toBe("Bearer NEW_TOKEN");
|
|
25
|
-
expect(next).toHaveBeenCalled();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("authMiddleware should skip if no refreshToken provided", async () => {
|
|
29
|
-
const ctx = mockCtx();
|
|
30
|
-
const next = mockNext();
|
|
31
|
-
|
|
32
|
-
await authMiddleware(ctx, next, {});
|
|
33
|
-
expect(ctx.init.headers["Authorization"]).toBeUndefined();
|
|
34
|
-
expect(next).toHaveBeenCalled();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// ---------------- CACHE ----------------
|
|
38
|
-
it("cacheMiddleware should cache GET responses", async () => {
|
|
39
|
-
const ctx = mockCtx("/users", "GET");
|
|
40
|
-
const next = mockNext({ users: [1, 2, 3] });
|
|
41
|
-
const middleware = cacheMiddleware({ ttl: 1000 });
|
|
42
|
-
|
|
43
|
-
const res1 = await middleware(ctx, next);
|
|
44
|
-
const res2 = await middleware(ctx, next);
|
|
45
|
-
|
|
46
|
-
expect(next).toHaveBeenCalledTimes(1); // only first time
|
|
47
|
-
const data2 = await res2.json();
|
|
48
|
-
expect(data2.users).toEqual([1, 2, 3]);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("cacheMiddleware should bypass cache for non-GET requests", async () => {
|
|
52
|
-
const ctx = mockCtx("/users", "POST");
|
|
53
|
-
const next = mockNext({ ok: true });
|
|
54
|
-
const middleware = cacheMiddleware();
|
|
55
|
-
|
|
56
|
-
await middleware(ctx, next);
|
|
57
|
-
expect(next).toHaveBeenCalledTimes(1);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// ---------------- LOGGING ----------------
|
|
61
|
-
it("loggingMiddleware should log request and response", async () => {
|
|
62
|
-
const ctx = mockCtx();
|
|
63
|
-
const next = mockNext();
|
|
64
|
-
|
|
65
|
-
const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
|
66
|
-
|
|
67
|
-
await loggingMiddleware(ctx, next, {
|
|
68
|
-
logRequest: true,
|
|
69
|
-
logResponse: true,
|
|
70
|
-
debug: true,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
expect(logSpy).toHaveBeenCalledWith("➡️ Request:", ctx.url, ctx.init);
|
|
74
|
-
expect(logSpy).toHaveBeenCalledWith("⬅️ Response:", 200);
|
|
75
|
-
|
|
76
|
-
logSpy.mockRestore();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// ---------------- RETRY ----------------
|
|
80
|
-
it("retryMiddleware should retry failed requests", async () => {
|
|
81
|
-
const ctx = mockCtx();
|
|
82
|
-
let attempt = 0;
|
|
83
|
-
|
|
84
|
-
const next = jest.fn().mockImplementation(() => {
|
|
85
|
-
attempt++;
|
|
86
|
-
if (attempt < 2) throw new Error("fail");
|
|
87
|
-
return Promise.resolve(new Response(JSON.stringify({ ok: true })));
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const middleware = retryMiddleware({ maxRetries: 3, delay: 10 });
|
|
91
|
-
|
|
92
|
-
const res = await middleware(ctx, next);
|
|
93
|
-
const json = await res.json();
|
|
94
|
-
|
|
95
|
-
expect(json.ok).toBe(true);
|
|
96
|
-
expect(next).toHaveBeenCalledTimes(2);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("retryMiddleware should throw after exceeding maxRetries", async () => {
|
|
100
|
-
const ctx = mockCtx();
|
|
101
|
-
const next = jest.fn().mockRejectedValue(new Error("fail always"));
|
|
102
|
-
|
|
103
|
-
const middleware = retryMiddleware({ maxRetries: 2, delay: 10 });
|
|
104
|
-
|
|
105
|
-
await expect(middleware(ctx, next)).rejects.toThrow("fail always");
|
|
106
|
-
expect(next).toHaveBeenCalledTimes(3); // initial + 2 retries
|
|
107
|
-
});
|
|
108
|
-
});
|
|
1
|
+
import { authMiddleware } from "../middlewares/auth";
|
|
2
|
+
import { cacheMiddleware } from "../middlewares/cache";
|
|
3
|
+
import { loggingMiddleware } from "../middlewares/logging";
|
|
4
|
+
import { retryMiddleware } from "../middlewares/retry";
|
|
5
|
+
|
|
6
|
+
describe("middlewares", () => {
|
|
7
|
+
const mockCtx = (url = "/test", method = "GET") => ({
|
|
8
|
+
url,
|
|
9
|
+
init: { method, headers: {} as Record<string, string> },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const mockNext = (response: any = { ok: true, status: 200 }) =>
|
|
13
|
+
jest.fn().mockResolvedValue(new Response(JSON.stringify(response)));
|
|
14
|
+
|
|
15
|
+
// ---------------- AUTH ----------------
|
|
16
|
+
it("authMiddleware should add refreshed token", async () => {
|
|
17
|
+
const ctx = mockCtx();
|
|
18
|
+
const next = mockNext();
|
|
19
|
+
|
|
20
|
+
await authMiddleware(ctx, next, {
|
|
21
|
+
refreshToken: async () => "NEW_TOKEN",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(ctx.init.headers["Authorization"]).toBe("Bearer NEW_TOKEN");
|
|
25
|
+
expect(next).toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("authMiddleware should skip if no refreshToken provided", async () => {
|
|
29
|
+
const ctx = mockCtx();
|
|
30
|
+
const next = mockNext();
|
|
31
|
+
|
|
32
|
+
await authMiddleware(ctx, next, {});
|
|
33
|
+
expect(ctx.init.headers["Authorization"]).toBeUndefined();
|
|
34
|
+
expect(next).toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ---------------- CACHE ----------------
|
|
38
|
+
it("cacheMiddleware should cache GET responses", async () => {
|
|
39
|
+
const ctx = mockCtx("/users", "GET");
|
|
40
|
+
const next = mockNext({ users: [1, 2, 3] });
|
|
41
|
+
const middleware = cacheMiddleware({ ttl: 1000 });
|
|
42
|
+
|
|
43
|
+
const res1 = await middleware(ctx, next);
|
|
44
|
+
const res2 = await middleware(ctx, next);
|
|
45
|
+
|
|
46
|
+
expect(next).toHaveBeenCalledTimes(1); // only first time
|
|
47
|
+
const data2 = await res2.json();
|
|
48
|
+
expect(data2.users).toEqual([1, 2, 3]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("cacheMiddleware should bypass cache for non-GET requests", async () => {
|
|
52
|
+
const ctx = mockCtx("/users", "POST");
|
|
53
|
+
const next = mockNext({ ok: true });
|
|
54
|
+
const middleware = cacheMiddleware();
|
|
55
|
+
|
|
56
|
+
await middleware(ctx, next);
|
|
57
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ---------------- LOGGING ----------------
|
|
61
|
+
it("loggingMiddleware should log request and response", async () => {
|
|
62
|
+
const ctx = mockCtx();
|
|
63
|
+
const next = mockNext();
|
|
64
|
+
|
|
65
|
+
const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
|
66
|
+
|
|
67
|
+
await loggingMiddleware(ctx, next, {
|
|
68
|
+
logRequest: true,
|
|
69
|
+
logResponse: true,
|
|
70
|
+
debug: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(logSpy).toHaveBeenCalledWith("➡️ Request:", ctx.url, ctx.init);
|
|
74
|
+
expect(logSpy).toHaveBeenCalledWith("⬅️ Response:", 200);
|
|
75
|
+
|
|
76
|
+
logSpy.mockRestore();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ---------------- RETRY ----------------
|
|
80
|
+
it("retryMiddleware should retry failed requests", async () => {
|
|
81
|
+
const ctx = mockCtx();
|
|
82
|
+
let attempt = 0;
|
|
83
|
+
|
|
84
|
+
const next = jest.fn().mockImplementation(() => {
|
|
85
|
+
attempt++;
|
|
86
|
+
if (attempt < 2) throw new Error("fail");
|
|
87
|
+
return Promise.resolve(new Response(JSON.stringify({ ok: true })));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const middleware = retryMiddleware({ maxRetries: 3, delay: 10 });
|
|
91
|
+
|
|
92
|
+
const res = await middleware(ctx, next);
|
|
93
|
+
const json = await res.json();
|
|
94
|
+
|
|
95
|
+
expect(json.ok).toBe(true);
|
|
96
|
+
expect(next).toHaveBeenCalledTimes(2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("retryMiddleware should throw after exceeding maxRetries", async () => {
|
|
100
|
+
const ctx = mockCtx();
|
|
101
|
+
const next = jest.fn().mockRejectedValue(new Error("fail always"));
|
|
102
|
+
|
|
103
|
+
const middleware = retryMiddleware({ maxRetries: 2, delay: 10 });
|
|
104
|
+
|
|
105
|
+
await expect(middleware(ctx, next)).rejects.toThrow("fail always");
|
|
106
|
+
expect(next).toHaveBeenCalledTimes(3); // initial + 2 retries
|
|
107
|
+
});
|
|
108
|
+
});
|
package/src/client.ts
CHANGED
|
@@ -1,142 +1,142 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Contracts,
|
|
3
|
-
EndpointDef,
|
|
4
|
-
EndpointDefZ,
|
|
5
|
-
Middleware,
|
|
6
|
-
ErrorLike,
|
|
7
|
-
EndpointMethods,
|
|
8
|
-
} from "./types";
|
|
9
|
-
import { z } from "zod";
|
|
10
|
-
|
|
11
|
-
export class RichError extends Error implements ErrorLike {
|
|
12
|
-
status?: number;
|
|
13
|
-
code?: string;
|
|
14
|
-
title?: string;
|
|
15
|
-
detail?: string;
|
|
16
|
-
errors?: Record<string, string[]>;
|
|
17
|
-
|
|
18
|
-
constructor(error: Partial<ErrorLike> & { message: string }) {
|
|
19
|
-
super(error.message);
|
|
20
|
-
Object.assign(this, error);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export class ApiClient<C extends Contracts, E extends ErrorLike = RichError> {
|
|
25
|
-
private middlewares: Array<{ fn: Middleware; options?: any }> = [];
|
|
26
|
-
private errorHandler?: (error: E) => void;
|
|
27
|
-
private responseTransform: (data: any) => any = (d) => d;
|
|
28
|
-
|
|
29
|
-
private _modules!: {
|
|
30
|
-
[M in keyof C]: EndpointMethods<C[M]>;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
constructor(
|
|
34
|
-
private config: { baseUrl: string; token?: string },
|
|
35
|
-
private contracts: C
|
|
36
|
-
) {}
|
|
37
|
-
|
|
38
|
-
init() {
|
|
39
|
-
const modules = {} as {
|
|
40
|
-
[M in keyof C]: EndpointMethods<C[M]>;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
for (const moduleName in this.contracts) {
|
|
44
|
-
const module = this.contracts[moduleName];
|
|
45
|
-
(modules as any)[moduleName] = {} as EndpointMethods<typeof module>;
|
|
46
|
-
|
|
47
|
-
for (const endpointName in module) {
|
|
48
|
-
const endpoint = module[endpointName] as EndpointDefZ;
|
|
49
|
-
|
|
50
|
-
(modules as any)[moduleName][endpointName] = (
|
|
51
|
-
input: z.infer<(typeof endpoint)["request"]>
|
|
52
|
-
) => this.request(endpoint, input);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
this._modules = modules;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
get modules() {
|
|
60
|
-
return this._modules;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
use<T>(middleware: Middleware<T>, options?: T) {
|
|
64
|
-
this.middlewares.push({ fn: middleware, options });
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
onError(handler: (error: E) => void) {
|
|
68
|
-
this.errorHandler = handler;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
useResponseTransform(fn: (data: any) => any) {
|
|
72
|
-
this.responseTransform = fn;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
private async request<TReq extends z.ZodTypeAny, TRes extends z.ZodTypeAny>(
|
|
76
|
-
endpoint: EndpointDef<TReq, TRes>,
|
|
77
|
-
input: z.infer<TReq>
|
|
78
|
-
): Promise<z.infer<TRes>> {
|
|
79
|
-
endpoint.request.parse(input);
|
|
80
|
-
|
|
81
|
-
if (endpoint.auth && !this.config.token) {
|
|
82
|
-
const error = this.createError({
|
|
83
|
-
message: `Missing token for ${endpoint.path}`,
|
|
84
|
-
status: 401,
|
|
85
|
-
code: "NO_TOKEN",
|
|
86
|
-
});
|
|
87
|
-
this.errorHandler?.(error as unknown as E);
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const headers: HeadersInit = { "Content-Type": "application/json" };
|
|
92
|
-
if (endpoint.auth && this.config.token)
|
|
93
|
-
headers["Authorization"] = `Bearer ${this.config.token}`;
|
|
94
|
-
|
|
95
|
-
const ctx = {
|
|
96
|
-
url: this.config.baseUrl + endpoint.path,
|
|
97
|
-
init: {
|
|
98
|
-
method: endpoint.method,
|
|
99
|
-
headers,
|
|
100
|
-
body: endpoint.method !== "GET" ? JSON.stringify(input) : undefined,
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const runner = this.middlewares.reduceRight(
|
|
105
|
-
(next, mw) => () => mw.fn(ctx, next, mw.options),
|
|
106
|
-
() => fetch(ctx.url, ctx.init)
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const res = await runner();
|
|
111
|
-
if (!res.ok) {
|
|
112
|
-
const errorData = await res.json().catch(() => ({}));
|
|
113
|
-
const error = this.createError({
|
|
114
|
-
message: errorData.message || res.statusText,
|
|
115
|
-
status: res.status,
|
|
116
|
-
code: errorData.code,
|
|
117
|
-
title: errorData.title,
|
|
118
|
-
detail: errorData.detail,
|
|
119
|
-
errors: errorData.errors,
|
|
120
|
-
});
|
|
121
|
-
this.errorHandler?.(error as unknown as E);
|
|
122
|
-
throw error;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const json = await res.json();
|
|
126
|
-
return this.responseTransform(endpoint.response.parse(json));
|
|
127
|
-
} catch (err: any) {
|
|
128
|
-
const error = this.normalizeError(err);
|
|
129
|
-
this.errorHandler?.(error as unknown as E);
|
|
130
|
-
throw error;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
private createError(error: Partial<RichError> & { message: string }) {
|
|
135
|
-
return new RichError(error);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private normalizeError(err: any) {
|
|
139
|
-
if (err instanceof RichError) return err;
|
|
140
|
-
return this.createError({ message: err.message || "Unknown error" });
|
|
141
|
-
}
|
|
142
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
Contracts,
|
|
3
|
+
EndpointDef,
|
|
4
|
+
EndpointDefZ,
|
|
5
|
+
Middleware,
|
|
6
|
+
ErrorLike,
|
|
7
|
+
EndpointMethods,
|
|
8
|
+
} from "./types";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
export class RichError extends Error implements ErrorLike {
|
|
12
|
+
status?: number;
|
|
13
|
+
code?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
detail?: string;
|
|
16
|
+
errors?: Record<string, string[]>;
|
|
17
|
+
|
|
18
|
+
constructor(error: Partial<ErrorLike> & { message: string }) {
|
|
19
|
+
super(error.message);
|
|
20
|
+
Object.assign(this, error);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ApiClient<C extends Contracts, E extends ErrorLike = RichError> {
|
|
25
|
+
private middlewares: Array<{ fn: Middleware; options?: any }> = [];
|
|
26
|
+
private errorHandler?: (error: E) => void;
|
|
27
|
+
private responseTransform: (data: any) => any = (d) => d;
|
|
28
|
+
|
|
29
|
+
private _modules!: {
|
|
30
|
+
[M in keyof C]: EndpointMethods<C[M]>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
private config: { baseUrl: string; token?: string },
|
|
35
|
+
private contracts: C
|
|
36
|
+
) {}
|
|
37
|
+
|
|
38
|
+
init() {
|
|
39
|
+
const modules = {} as {
|
|
40
|
+
[M in keyof C]: EndpointMethods<C[M]>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
for (const moduleName in this.contracts) {
|
|
44
|
+
const module = this.contracts[moduleName];
|
|
45
|
+
(modules as any)[moduleName] = {} as EndpointMethods<typeof module>;
|
|
46
|
+
|
|
47
|
+
for (const endpointName in module) {
|
|
48
|
+
const endpoint = module[endpointName] as EndpointDefZ;
|
|
49
|
+
|
|
50
|
+
(modules as any)[moduleName][endpointName] = (
|
|
51
|
+
input: z.infer<(typeof endpoint)["request"]>
|
|
52
|
+
) => this.request(endpoint, input);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this._modules = modules;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get modules() {
|
|
60
|
+
return this._modules;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
use<T>(middleware: Middleware<T>, options?: T) {
|
|
64
|
+
this.middlewares.push({ fn: middleware, options });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onError(handler: (error: E) => void) {
|
|
68
|
+
this.errorHandler = handler;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
useResponseTransform(fn: (data: any) => any) {
|
|
72
|
+
this.responseTransform = fn;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async request<TReq extends z.ZodTypeAny, TRes extends z.ZodTypeAny>(
|
|
76
|
+
endpoint: EndpointDef<TReq, TRes>,
|
|
77
|
+
input: z.infer<TReq>
|
|
78
|
+
): Promise<z.infer<TRes>> {
|
|
79
|
+
endpoint.request.parse(input);
|
|
80
|
+
|
|
81
|
+
if (endpoint.auth && !this.config.token) {
|
|
82
|
+
const error = this.createError({
|
|
83
|
+
message: `Missing token for ${endpoint.path}`,
|
|
84
|
+
status: 401,
|
|
85
|
+
code: "NO_TOKEN",
|
|
86
|
+
});
|
|
87
|
+
this.errorHandler?.(error as unknown as E);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const headers: HeadersInit = { "Content-Type": "application/json" };
|
|
92
|
+
if (endpoint.auth && this.config.token)
|
|
93
|
+
headers["Authorization"] = `Bearer ${this.config.token}`;
|
|
94
|
+
|
|
95
|
+
const ctx = {
|
|
96
|
+
url: this.config.baseUrl + endpoint.path,
|
|
97
|
+
init: {
|
|
98
|
+
method: endpoint.method,
|
|
99
|
+
headers,
|
|
100
|
+
body: endpoint.method !== "GET" ? JSON.stringify(input) : undefined,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const runner = this.middlewares.reduceRight(
|
|
105
|
+
(next, mw) => () => mw.fn(ctx, next, mw.options),
|
|
106
|
+
() => fetch(ctx.url, ctx.init)
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const res = await runner();
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
const errorData = await res.json().catch(() => ({}));
|
|
113
|
+
const error = this.createError({
|
|
114
|
+
message: errorData.message || res.statusText,
|
|
115
|
+
status: res.status,
|
|
116
|
+
code: errorData.code,
|
|
117
|
+
title: errorData.title,
|
|
118
|
+
detail: errorData.detail,
|
|
119
|
+
errors: errorData.errors,
|
|
120
|
+
});
|
|
121
|
+
this.errorHandler?.(error as unknown as E);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const json = await res.json();
|
|
126
|
+
return this.responseTransform(endpoint.response.parse(json));
|
|
127
|
+
} catch (err: any) {
|
|
128
|
+
const error = this.normalizeError(err);
|
|
129
|
+
this.errorHandler?.(error as unknown as E);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private createError(error: Partial<RichError> & { message: string }) {
|
|
135
|
+
return new RichError(error);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private normalizeError(err: any) {
|
|
139
|
+
if (err instanceof RichError) return err;
|
|
140
|
+
return this.createError({ message: err.message || "Unknown error" });
|
|
141
|
+
}
|
|
142
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export * from "./types";
|
|
2
|
-
export * from "./client";
|
|
3
|
-
export * from "./middlewares/logging";
|
|
4
|
-
export * from "./middlewares/retry";
|
|
5
|
-
export * from "./middlewares/auth";
|
|
6
|
-
export * from "./middlewares/cache";
|
|
1
|
+
export * from "./types";
|
|
2
|
+
export * from "./client";
|
|
3
|
+
export * from "./middlewares/logging";
|
|
4
|
+
export * from "./middlewares/retry";
|
|
5
|
+
export * from "./middlewares/auth";
|
|
6
|
+
export * from "./middlewares/cache";
|
package/src/middlewares/auth.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { Middleware } from "../types";
|
|
2
|
-
|
|
3
|
-
export type AuthOptions = {
|
|
4
|
-
refreshToken?: () => Promise<string>;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export const authMiddleware: Middleware<AuthOptions> = async (ctx, next, options) => {
|
|
8
|
-
if (options?.refreshToken) {
|
|
9
|
-
try {
|
|
10
|
-
const newToken = await options.refreshToken();
|
|
11
|
-
ctx.init.headers = {
|
|
12
|
-
...ctx.init.headers,
|
|
13
|
-
Authorization: `Bearer ${newToken}`
|
|
14
|
-
};
|
|
15
|
-
} catch {}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return next();
|
|
19
|
-
};
|
|
1
|
+
import { Middleware } from "../types";
|
|
2
|
+
|
|
3
|
+
export type AuthOptions = {
|
|
4
|
+
refreshToken?: () => Promise<string>;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const authMiddleware: Middleware<AuthOptions> = async (ctx, next, options) => {
|
|
8
|
+
if (options?.refreshToken) {
|
|
9
|
+
try {
|
|
10
|
+
const newToken = await options.refreshToken();
|
|
11
|
+
ctx.init.headers = {
|
|
12
|
+
...ctx.init.headers,
|
|
13
|
+
Authorization: `Bearer ${newToken}`
|
|
14
|
+
};
|
|
15
|
+
} catch {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return next();
|
|
19
|
+
};
|
package/src/middlewares/cache.ts
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { MiddlewareContext, MiddlewareNext } from "../types";
|
|
2
|
-
|
|
3
|
-
export type CacheOptions = { ttl?: number };
|
|
4
|
-
|
|
5
|
-
export const cacheMiddleware = (options: CacheOptions = {}) => {
|
|
6
|
-
const { ttl = 60000 } = options;
|
|
7
|
-
const cache = new Map<string, { data: any; expires: number }>();
|
|
8
|
-
|
|
9
|
-
return async (ctx: MiddlewareContext, next: MiddlewareNext) => {
|
|
10
|
-
if (ctx.init.method === "GET") {
|
|
11
|
-
const cached = cache.get(ctx.url);
|
|
12
|
-
const now = Date.now();
|
|
13
|
-
if (cached && cached.expires > now)
|
|
14
|
-
return new Response(JSON.stringify(cached.data));
|
|
15
|
-
|
|
16
|
-
const res = await next();
|
|
17
|
-
const data = await res
|
|
18
|
-
.clone()
|
|
19
|
-
.json()
|
|
20
|
-
.catch(() => null);
|
|
21
|
-
if (data) cache.set(ctx.url, { data, expires: now + ttl });
|
|
22
|
-
return res;
|
|
23
|
-
}
|
|
24
|
-
return next();
|
|
25
|
-
};
|
|
26
|
-
};
|
|
1
|
+
import { MiddlewareContext, MiddlewareNext } from "../types";
|
|
2
|
+
|
|
3
|
+
export type CacheOptions = { ttl?: number };
|
|
4
|
+
|
|
5
|
+
export const cacheMiddleware = (options: CacheOptions = {}) => {
|
|
6
|
+
const { ttl = 60000 } = options;
|
|
7
|
+
const cache = new Map<string, { data: any; expires: number }>();
|
|
8
|
+
|
|
9
|
+
return async (ctx: MiddlewareContext, next: MiddlewareNext) => {
|
|
10
|
+
if (ctx.init.method === "GET") {
|
|
11
|
+
const cached = cache.get(ctx.url);
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
if (cached && cached.expires > now)
|
|
14
|
+
return new Response(JSON.stringify(cached.data));
|
|
15
|
+
|
|
16
|
+
const res = await next();
|
|
17
|
+
const data = await res
|
|
18
|
+
.clone()
|
|
19
|
+
.json()
|
|
20
|
+
.catch(() => null);
|
|
21
|
+
if (data) cache.set(ctx.url, { data, expires: now + ttl });
|
|
22
|
+
return res;
|
|
23
|
+
}
|
|
24
|
+
return next();
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { Middleware } from "../types";
|
|
2
|
-
|
|
3
|
-
export type LoggingOptions = {
|
|
4
|
-
logRequest?: boolean;
|
|
5
|
-
logResponse?: boolean;
|
|
6
|
-
debug?: boolean;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export const loggingMiddleware: Middleware<LoggingOptions> = async (ctx, next, options) => {
|
|
10
|
-
const { logRequest = true, logResponse = true, debug = true } = options || {};
|
|
11
|
-
|
|
12
|
-
if (debug && logRequest) console.log("➡️ Request:", ctx.url, ctx.init);
|
|
13
|
-
|
|
14
|
-
const res = await next();
|
|
15
|
-
|
|
16
|
-
if (debug && logResponse) console.log("⬅️ Response:", res.status);
|
|
17
|
-
|
|
18
|
-
return res;
|
|
19
|
-
};
|
|
1
|
+
import { Middleware } from "../types";
|
|
2
|
+
|
|
3
|
+
export type LoggingOptions = {
|
|
4
|
+
logRequest?: boolean;
|
|
5
|
+
logResponse?: boolean;
|
|
6
|
+
debug?: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const loggingMiddleware: Middleware<LoggingOptions> = async (ctx, next, options) => {
|
|
10
|
+
const { logRequest = true, logResponse = true, debug = true } = options || {};
|
|
11
|
+
|
|
12
|
+
if (debug && logRequest) console.log("➡️ Request:", ctx.url, ctx.init);
|
|
13
|
+
|
|
14
|
+
const res = await next();
|
|
15
|
+
|
|
16
|
+
if (debug && logResponse) console.log("⬅️ Response:", res.status);
|
|
17
|
+
|
|
18
|
+
return res;
|
|
19
|
+
};
|