@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
package/dist/client.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiClient = exports.RichError = void 0;
|
|
4
|
+
class RichError extends Error {
|
|
5
|
+
constructor(error) {
|
|
6
|
+
super(error.message);
|
|
7
|
+
Object.assign(this, error);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.RichError = RichError;
|
|
11
|
+
class ApiClient {
|
|
12
|
+
constructor(config, contracts) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.contracts = contracts;
|
|
15
|
+
this.middlewares = [];
|
|
16
|
+
this.responseTransform = (d) => d;
|
|
17
|
+
}
|
|
18
|
+
init() {
|
|
19
|
+
const modules = {};
|
|
20
|
+
for (const moduleName in this.contracts) {
|
|
21
|
+
const module = this.contracts[moduleName];
|
|
22
|
+
modules[moduleName] = {};
|
|
23
|
+
for (const endpointName in module) {
|
|
24
|
+
const endpoint = module[endpointName];
|
|
25
|
+
modules[moduleName][endpointName] = (input) => this.request(endpoint, input);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
this._modules = modules;
|
|
29
|
+
}
|
|
30
|
+
get modules() {
|
|
31
|
+
return this._modules;
|
|
32
|
+
}
|
|
33
|
+
use(middleware, options) {
|
|
34
|
+
this.middlewares.push({ fn: middleware, options });
|
|
35
|
+
}
|
|
36
|
+
onError(handler) {
|
|
37
|
+
this.errorHandler = handler;
|
|
38
|
+
}
|
|
39
|
+
useResponseTransform(fn) {
|
|
40
|
+
this.responseTransform = fn;
|
|
41
|
+
}
|
|
42
|
+
async request(endpoint, input) {
|
|
43
|
+
endpoint.request.parse(input);
|
|
44
|
+
if (endpoint.auth && !this.config.token) {
|
|
45
|
+
const error = this.createError({
|
|
46
|
+
message: `Missing token for ${endpoint.path}`,
|
|
47
|
+
status: 401,
|
|
48
|
+
code: "NO_TOKEN",
|
|
49
|
+
});
|
|
50
|
+
this.errorHandler?.(error);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
const headers = { "Content-Type": "application/json" };
|
|
54
|
+
if (endpoint.auth && this.config.token)
|
|
55
|
+
headers["Authorization"] = `Bearer ${this.config.token}`;
|
|
56
|
+
const ctx = {
|
|
57
|
+
url: this.config.baseUrl + endpoint.path,
|
|
58
|
+
init: {
|
|
59
|
+
method: endpoint.method,
|
|
60
|
+
headers,
|
|
61
|
+
body: endpoint.method !== "GET" ? JSON.stringify(input) : undefined,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
const runner = this.middlewares.reduceRight((next, mw) => () => mw.fn(ctx, next, mw.options), () => fetch(ctx.url, ctx.init));
|
|
65
|
+
try {
|
|
66
|
+
const res = await runner();
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const errorData = await res.json().catch(() => ({}));
|
|
69
|
+
const error = this.createError({
|
|
70
|
+
message: errorData.message || res.statusText,
|
|
71
|
+
status: res.status,
|
|
72
|
+
code: errorData.code,
|
|
73
|
+
title: errorData.title,
|
|
74
|
+
detail: errorData.detail,
|
|
75
|
+
errors: errorData.errors,
|
|
76
|
+
});
|
|
77
|
+
this.errorHandler?.(error);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
const json = await res.json();
|
|
81
|
+
return this.responseTransform(endpoint.response.parse(json));
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const error = this.normalizeError(err);
|
|
85
|
+
this.errorHandler?.(error);
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
createError(error) {
|
|
90
|
+
return new RichError(error);
|
|
91
|
+
}
|
|
92
|
+
normalizeError(err) {
|
|
93
|
+
if (err instanceof RichError)
|
|
94
|
+
return err;
|
|
95
|
+
return this.createError({ message: err.message || "Unknown error" });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.ApiClient = ApiClient;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./client"), exports);
|
|
19
|
+
__exportStar(require("./middlewares/logging"), exports);
|
|
20
|
+
__exportStar(require("./middlewares/retry"), exports);
|
|
21
|
+
__exportStar(require("./middlewares/auth"), exports);
|
|
22
|
+
__exportStar(require("./middlewares/cache"), exports);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authMiddleware = void 0;
|
|
4
|
+
const authMiddleware = async (ctx, next, options) => {
|
|
5
|
+
if (options?.refreshToken) {
|
|
6
|
+
try {
|
|
7
|
+
const newToken = await options.refreshToken();
|
|
8
|
+
ctx.init.headers = {
|
|
9
|
+
...ctx.init.headers,
|
|
10
|
+
Authorization: `Bearer ${newToken}`
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
catch { }
|
|
14
|
+
}
|
|
15
|
+
return next();
|
|
16
|
+
};
|
|
17
|
+
exports.authMiddleware = authMiddleware;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cacheMiddleware = void 0;
|
|
4
|
+
const cacheMiddleware = (options = {}) => {
|
|
5
|
+
const { ttl = 60000 } = options;
|
|
6
|
+
const cache = new Map();
|
|
7
|
+
return async (ctx, next) => {
|
|
8
|
+
if (ctx.init.method === "GET") {
|
|
9
|
+
const cached = cache.get(ctx.url);
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
if (cached && cached.expires > now)
|
|
12
|
+
return new Response(JSON.stringify(cached.data));
|
|
13
|
+
const res = await next();
|
|
14
|
+
const data = await res
|
|
15
|
+
.clone()
|
|
16
|
+
.json()
|
|
17
|
+
.catch(() => null);
|
|
18
|
+
if (data)
|
|
19
|
+
cache.set(ctx.url, { data, expires: now + ttl });
|
|
20
|
+
return res;
|
|
21
|
+
}
|
|
22
|
+
return next();
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
exports.cacheMiddleware = cacheMiddleware;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loggingMiddleware = void 0;
|
|
4
|
+
const loggingMiddleware = async (ctx, next, options) => {
|
|
5
|
+
const { logRequest = true, logResponse = true, debug = true } = options || {};
|
|
6
|
+
if (debug && logRequest)
|
|
7
|
+
console.log("➡️ Request:", ctx.url, ctx.init);
|
|
8
|
+
const res = await next();
|
|
9
|
+
if (debug && logResponse)
|
|
10
|
+
console.log("⬅️ Response:", res.status);
|
|
11
|
+
return res;
|
|
12
|
+
};
|
|
13
|
+
exports.loggingMiddleware = loggingMiddleware;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.retryMiddleware = void 0;
|
|
4
|
+
const retryMiddleware = (options) => {
|
|
5
|
+
const { maxRetries = 3, delay = 500 } = options || {};
|
|
6
|
+
const middleware = async (ctx, next) => {
|
|
7
|
+
let attempt = 0;
|
|
8
|
+
while (true) {
|
|
9
|
+
try {
|
|
10
|
+
return await next();
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
if (attempt >= maxRetries)
|
|
14
|
+
throw err;
|
|
15
|
+
attempt++;
|
|
16
|
+
await new Promise(r => setTimeout(r, delay * 2 ** attempt));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
return middleware;
|
|
21
|
+
};
|
|
22
|
+
exports.retryMiddleware = retryMiddleware;
|
package/jest.config.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import type { Config } from "jest";
|
|
2
|
-
|
|
3
|
-
const config: Config = {
|
|
4
|
-
preset: "ts-jest",
|
|
5
|
-
testEnvironment: "node",
|
|
6
|
-
roots: ["<rootDir>/src"],
|
|
7
|
-
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
|
8
|
-
transform: {
|
|
9
|
-
"^.+\\.(ts|tsx)$": "ts-jest",
|
|
10
|
-
},
|
|
11
|
-
testMatch: ["**/*.test.ts", "**/*.test.tsx"],
|
|
12
|
-
collectCoverage: true,
|
|
13
|
-
collectCoverageFrom: ["src/**/*.{ts,tsx}", "!src/**/*.d.ts"],
|
|
14
|
-
coverageDirectory: "coverage",
|
|
15
|
-
clearMocks: true,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export default config;
|
|
1
|
+
import type { Config } from "jest";
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
preset: "ts-jest",
|
|
5
|
+
testEnvironment: "node",
|
|
6
|
+
roots: ["<rootDir>/src"],
|
|
7
|
+
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
|
8
|
+
transform: {
|
|
9
|
+
"^.+\\.(ts|tsx)$": "ts-jest",
|
|
10
|
+
},
|
|
11
|
+
testMatch: ["**/*.test.ts", "**/*.test.tsx"],
|
|
12
|
+
collectCoverage: true,
|
|
13
|
+
collectCoverageFrom: ["src/**/*.{ts,tsx}", "!src/**/*.d.ts"],
|
|
14
|
+
coverageDirectory: "coverage",
|
|
15
|
+
clearMocks: true,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default config;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tahanabavi/typefetch",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "A fully type-safe, extensible API client for TypeScript projects, featuring global error handling, configurable middleware, automatic retries, auth refresh, response transforms, and seamless contract integration. Designed for large-scale applications and developer-friendly API interactions.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
import { z, ZodError } from "zod";
|
|
2
|
-
import { ApiClient, RichError } from "../client";
|
|
3
|
-
import { Contracts } from "../types";
|
|
4
|
-
|
|
5
|
-
// Mock fetch globally
|
|
6
|
-
global.fetch = jest.fn();
|
|
7
|
-
|
|
8
|
-
const contracts: Contracts = {
|
|
9
|
-
user: {
|
|
10
|
-
getUser: {
|
|
11
|
-
method: "GET",
|
|
12
|
-
path: "/user",
|
|
13
|
-
request: z.object({ id: z.string() }),
|
|
14
|
-
response: z.object({ id: z.string(), name: z.string() }),
|
|
15
|
-
},
|
|
16
|
-
createUser: {
|
|
17
|
-
method: "POST",
|
|
18
|
-
path: "/user",
|
|
19
|
-
auth: true,
|
|
20
|
-
request: z.object({ name: z.string() }),
|
|
21
|
-
response: z.object({ id: z.string(), name: z.string() }),
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
describe("ApiClient", () => {
|
|
27
|
-
let client: ApiClient<typeof contracts>;
|
|
28
|
-
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
jest.clearAllMocks();
|
|
31
|
-
client = new ApiClient({ baseUrl: "https://api.test.com" }, contracts);
|
|
32
|
-
client.init();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("should initialize modules correctly", () => {
|
|
36
|
-
expect(client.modules.user).toBeDefined();
|
|
37
|
-
expect(typeof client.modules.user.getUser).toBe("function");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("should call fetch with correct URL and headers", async () => {
|
|
41
|
-
(fetch as jest.Mock).mockResolvedValueOnce({
|
|
42
|
-
ok: true,
|
|
43
|
-
json: async () => ({ id: "1", name: "John" }),
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const res = await client.modules.user.getUser({ id: "1" });
|
|
47
|
-
expect(fetch).toHaveBeenCalledWith("https://api.test.com/user", {
|
|
48
|
-
method: "GET",
|
|
49
|
-
headers: { "Content-Type": "application/json" },
|
|
50
|
-
body: undefined,
|
|
51
|
-
});
|
|
52
|
-
expect(res).toEqual({ id: "1", name: "John" });
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("should throw validation error if input is invalid", async () => {
|
|
56
|
-
await expect(client.modules.user.getUser({} as any))
|
|
57
|
-
.rejects.toBeInstanceOf(ZodError);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("should handle auth header when token is provided", async () => {
|
|
61
|
-
const authedClient = new ApiClient(
|
|
62
|
-
{ baseUrl: "https://api.test.com", token: "mytoken" },
|
|
63
|
-
contracts
|
|
64
|
-
);
|
|
65
|
-
authedClient.init();
|
|
66
|
-
|
|
67
|
-
(fetch as jest.Mock).mockResolvedValueOnce({
|
|
68
|
-
ok: true,
|
|
69
|
-
json: async () => ({ id: "2", name: "Alice" }),
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
await authedClient.modules.user.createUser({ name: "Alice" });
|
|
73
|
-
|
|
74
|
-
expect(fetch).toHaveBeenCalledWith("https://api.test.com/user", {
|
|
75
|
-
method: "POST",
|
|
76
|
-
headers: {
|
|
77
|
-
"Content-Type": "application/json",
|
|
78
|
-
Authorization: "Bearer mytoken",
|
|
79
|
-
},
|
|
80
|
-
body: JSON.stringify({ name: "Alice" }),
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("should throw error if auth required and no token provided", async () => {
|
|
85
|
-
await expect(
|
|
86
|
-
client.modules.user.createUser({ name: "Alice" })
|
|
87
|
-
).rejects.toThrow(RichError);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("should call errorHandler when error occurs", async () => {
|
|
91
|
-
const handler = jest.fn();
|
|
92
|
-
client.onError(handler);
|
|
93
|
-
|
|
94
|
-
(fetch as jest.Mock).mockResolvedValueOnce({
|
|
95
|
-
ok: false,
|
|
96
|
-
status: 400,
|
|
97
|
-
statusText: "Bad Request",
|
|
98
|
-
json: async () => ({ message: "Invalid input" }),
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
await expect(client.modules.user.getUser({ id: "bad" })).rejects.toThrow();
|
|
102
|
-
|
|
103
|
-
expect(handler).toHaveBeenCalled();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("should apply responseTransform", async () => {
|
|
107
|
-
client.useResponseTransform((data) => ({ ...data, transformed: true }));
|
|
108
|
-
|
|
109
|
-
(fetch as jest.Mock).mockResolvedValueOnce({
|
|
110
|
-
ok: true,
|
|
111
|
-
json: async () => ({ id: "1", name: "John" }),
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const res = await client.modules.user.getUser({ id: "1" });
|
|
115
|
-
expect(res).toEqual({ id: "1", name: "John", transformed: true });
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("should execute middleware in order", async () => {
|
|
119
|
-
const logs: string[] = [];
|
|
120
|
-
|
|
121
|
-
client.use(async (ctx, next) => {
|
|
122
|
-
logs.push("before");
|
|
123
|
-
const res = await next();
|
|
124
|
-
logs.push("after");
|
|
125
|
-
return res;
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
(fetch as jest.Mock).mockResolvedValueOnce({
|
|
129
|
-
ok: true,
|
|
130
|
-
json: async () => ({ id: "1", name: "John" }),
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
await client.modules.user.getUser({ id: "1" });
|
|
134
|
-
|
|
135
|
-
expect(logs).toEqual(["before", "after"]);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
1
|
+
import { z, ZodError } from "zod";
|
|
2
|
+
import { ApiClient, RichError } from "../client";
|
|
3
|
+
import { Contracts } from "../types";
|
|
4
|
+
|
|
5
|
+
// Mock fetch globally
|
|
6
|
+
global.fetch = jest.fn();
|
|
7
|
+
|
|
8
|
+
const contracts: Contracts = {
|
|
9
|
+
user: {
|
|
10
|
+
getUser: {
|
|
11
|
+
method: "GET",
|
|
12
|
+
path: "/user",
|
|
13
|
+
request: z.object({ id: z.string() }),
|
|
14
|
+
response: z.object({ id: z.string(), name: z.string() }),
|
|
15
|
+
},
|
|
16
|
+
createUser: {
|
|
17
|
+
method: "POST",
|
|
18
|
+
path: "/user",
|
|
19
|
+
auth: true,
|
|
20
|
+
request: z.object({ name: z.string() }),
|
|
21
|
+
response: z.object({ id: z.string(), name: z.string() }),
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe("ApiClient", () => {
|
|
27
|
+
let client: ApiClient<typeof contracts>;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
jest.clearAllMocks();
|
|
31
|
+
client = new ApiClient({ baseUrl: "https://api.test.com" }, contracts);
|
|
32
|
+
client.init();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should initialize modules correctly", () => {
|
|
36
|
+
expect(client.modules.user).toBeDefined();
|
|
37
|
+
expect(typeof client.modules.user.getUser).toBe("function");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should call fetch with correct URL and headers", async () => {
|
|
41
|
+
(fetch as jest.Mock).mockResolvedValueOnce({
|
|
42
|
+
ok: true,
|
|
43
|
+
json: async () => ({ id: "1", name: "John" }),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const res = await client.modules.user.getUser({ id: "1" });
|
|
47
|
+
expect(fetch).toHaveBeenCalledWith("https://api.test.com/user", {
|
|
48
|
+
method: "GET",
|
|
49
|
+
headers: { "Content-Type": "application/json" },
|
|
50
|
+
body: undefined,
|
|
51
|
+
});
|
|
52
|
+
expect(res).toEqual({ id: "1", name: "John" });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should throw validation error if input is invalid", async () => {
|
|
56
|
+
await expect(client.modules.user.getUser({} as any))
|
|
57
|
+
.rejects.toBeInstanceOf(ZodError);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should handle auth header when token is provided", async () => {
|
|
61
|
+
const authedClient = new ApiClient(
|
|
62
|
+
{ baseUrl: "https://api.test.com", token: "mytoken" },
|
|
63
|
+
contracts
|
|
64
|
+
);
|
|
65
|
+
authedClient.init();
|
|
66
|
+
|
|
67
|
+
(fetch as jest.Mock).mockResolvedValueOnce({
|
|
68
|
+
ok: true,
|
|
69
|
+
json: async () => ({ id: "2", name: "Alice" }),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await authedClient.modules.user.createUser({ name: "Alice" });
|
|
73
|
+
|
|
74
|
+
expect(fetch).toHaveBeenCalledWith("https://api.test.com/user", {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: {
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
Authorization: "Bearer mytoken",
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify({ name: "Alice" }),
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should throw error if auth required and no token provided", async () => {
|
|
85
|
+
await expect(
|
|
86
|
+
client.modules.user.createUser({ name: "Alice" })
|
|
87
|
+
).rejects.toThrow(RichError);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should call errorHandler when error occurs", async () => {
|
|
91
|
+
const handler = jest.fn();
|
|
92
|
+
client.onError(handler);
|
|
93
|
+
|
|
94
|
+
(fetch as jest.Mock).mockResolvedValueOnce({
|
|
95
|
+
ok: false,
|
|
96
|
+
status: 400,
|
|
97
|
+
statusText: "Bad Request",
|
|
98
|
+
json: async () => ({ message: "Invalid input" }),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await expect(client.modules.user.getUser({ id: "bad" })).rejects.toThrow();
|
|
102
|
+
|
|
103
|
+
expect(handler).toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should apply responseTransform", async () => {
|
|
107
|
+
client.useResponseTransform((data) => ({ ...data, transformed: true }));
|
|
108
|
+
|
|
109
|
+
(fetch as jest.Mock).mockResolvedValueOnce({
|
|
110
|
+
ok: true,
|
|
111
|
+
json: async () => ({ id: "1", name: "John" }),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const res = await client.modules.user.getUser({ id: "1" });
|
|
115
|
+
expect(res).toEqual({ id: "1", name: "John", transformed: true });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should execute middleware in order", async () => {
|
|
119
|
+
const logs: string[] = [];
|
|
120
|
+
|
|
121
|
+
client.use(async (ctx, next) => {
|
|
122
|
+
logs.push("before");
|
|
123
|
+
const res = await next();
|
|
124
|
+
logs.push("after");
|
|
125
|
+
return res;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
(fetch as jest.Mock).mockResolvedValueOnce({
|
|
129
|
+
ok: true,
|
|
130
|
+
json: async () => ({ id: "1", name: "John" }),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await client.modules.user.getUser({ id: "1" });
|
|
134
|
+
|
|
135
|
+
expect(logs).toEqual(["before", "after"]);
|
|
136
|
+
});
|
|
137
|
+
});
|