@tahanabavi/typefetch 1.0.2 → 1.0.3

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/dist/index.mjs ADDED
@@ -0,0 +1,208 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defProps = Object.defineProperties;
3
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
+ var __pow = Math.pow;
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __spreadValues = (a, b) => {
10
+ for (var prop in b || (b = {}))
11
+ if (__hasOwnProp.call(b, prop))
12
+ __defNormalProp(a, prop, b[prop]);
13
+ if (__getOwnPropSymbols)
14
+ for (var prop of __getOwnPropSymbols(b)) {
15
+ if (__propIsEnum.call(b, prop))
16
+ __defNormalProp(a, prop, b[prop]);
17
+ }
18
+ return a;
19
+ };
20
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
21
+ var __async = (__this, __arguments, generator) => {
22
+ return new Promise((resolve, reject) => {
23
+ var fulfilled = (value) => {
24
+ try {
25
+ step(generator.next(value));
26
+ } catch (e) {
27
+ reject(e);
28
+ }
29
+ };
30
+ var rejected = (value) => {
31
+ try {
32
+ step(generator.throw(value));
33
+ } catch (e) {
34
+ reject(e);
35
+ }
36
+ };
37
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
38
+ step((generator = generator.apply(__this, __arguments)).next());
39
+ });
40
+ };
41
+
42
+ // src/client.ts
43
+ var RichError = class extends Error {
44
+ constructor(error) {
45
+ super(error.message);
46
+ Object.assign(this, error);
47
+ }
48
+ };
49
+ var ApiClient = class {
50
+ constructor(config, contracts) {
51
+ this.config = config;
52
+ this.contracts = contracts;
53
+ this.middlewares = [];
54
+ this.responseTransform = (d) => d;
55
+ }
56
+ init() {
57
+ const modules = {};
58
+ for (const moduleName in this.contracts) {
59
+ const module = this.contracts[moduleName];
60
+ modules[moduleName] = {};
61
+ for (const endpointName in module) {
62
+ const endpoint = module[endpointName];
63
+ modules[moduleName][endpointName] = (input) => this.request(endpoint, input);
64
+ }
65
+ }
66
+ this._modules = modules;
67
+ }
68
+ get modules() {
69
+ return this._modules;
70
+ }
71
+ use(middleware, options) {
72
+ this.middlewares.push({ fn: middleware, options });
73
+ }
74
+ onError(handler) {
75
+ this.errorHandler = handler;
76
+ }
77
+ useResponseTransform(fn) {
78
+ this.responseTransform = fn;
79
+ }
80
+ request(endpoint, input) {
81
+ return __async(this, null, function* () {
82
+ var _a, _b, _c;
83
+ endpoint.request.parse(input);
84
+ if (endpoint.auth && !this.config.token) {
85
+ const error = this.createError({
86
+ message: `Missing token for ${endpoint.path}`,
87
+ status: 401,
88
+ code: "NO_TOKEN"
89
+ });
90
+ (_a = this.errorHandler) == null ? void 0 : _a.call(this, error);
91
+ throw error;
92
+ }
93
+ const headers = { "Content-Type": "application/json" };
94
+ if (endpoint.auth && this.config.token)
95
+ headers["Authorization"] = `Bearer ${this.config.token}`;
96
+ const ctx = {
97
+ url: this.config.baseUrl + endpoint.path,
98
+ init: {
99
+ method: endpoint.method,
100
+ headers,
101
+ body: endpoint.method !== "GET" ? JSON.stringify(input) : void 0
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
+ try {
109
+ const res = yield runner();
110
+ if (!res.ok) {
111
+ const errorData = yield res.json().catch(() => ({}));
112
+ const error = this.createError({
113
+ message: errorData.message || res.statusText,
114
+ status: res.status,
115
+ code: errorData.code,
116
+ title: errorData.title,
117
+ detail: errorData.detail,
118
+ errors: errorData.errors
119
+ });
120
+ (_b = this.errorHandler) == null ? void 0 : _b.call(this, error);
121
+ throw error;
122
+ }
123
+ const json = yield res.json();
124
+ return this.responseTransform(endpoint.response.parse(json));
125
+ } catch (err) {
126
+ const error = this.normalizeError(err);
127
+ (_c = this.errorHandler) == null ? void 0 : _c.call(this, error);
128
+ throw error;
129
+ }
130
+ });
131
+ }
132
+ createError(error) {
133
+ return new RichError(error);
134
+ }
135
+ normalizeError(err) {
136
+ if (err instanceof RichError) return err;
137
+ return this.createError({ message: err.message || "Unknown error" });
138
+ }
139
+ };
140
+
141
+ // src/middlewares/logging.ts
142
+ var loggingMiddleware = (ctx, next, options) => __async(null, null, function* () {
143
+ const { logRequest = true, logResponse = true, debug = true } = options || {};
144
+ if (debug && logRequest) console.log("\u27A1\uFE0F Request:", ctx.url, ctx.init);
145
+ const res = yield next();
146
+ if (debug && logResponse) console.log("\u2B05\uFE0F Response:", res.status);
147
+ return res;
148
+ });
149
+
150
+ // src/middlewares/retry.ts
151
+ var retryMiddleware = (options) => {
152
+ const { maxRetries = 3, delay = 500 } = options || {};
153
+ const middleware = (ctx, next) => __async(null, null, function* () {
154
+ let attempt = 0;
155
+ while (true) {
156
+ try {
157
+ return yield next();
158
+ } catch (err) {
159
+ if (attempt >= maxRetries) throw err;
160
+ attempt++;
161
+ yield new Promise((r) => setTimeout(r, delay * __pow(2, attempt)));
162
+ }
163
+ }
164
+ });
165
+ return middleware;
166
+ };
167
+
168
+ // src/middlewares/auth.ts
169
+ var authMiddleware = (ctx, next, options) => __async(null, null, function* () {
170
+ if (options == null ? void 0 : options.refreshToken) {
171
+ try {
172
+ const newToken = yield options.refreshToken();
173
+ ctx.init.headers = __spreadProps(__spreadValues({}, ctx.init.headers), {
174
+ Authorization: `Bearer ${newToken}`
175
+ });
176
+ } catch (e) {
177
+ }
178
+ }
179
+ return next();
180
+ });
181
+
182
+ // src/middlewares/cache.ts
183
+ var cacheMiddleware = (options = {}) => {
184
+ const { ttl = 6e4 } = options;
185
+ const cache = /* @__PURE__ */ new Map();
186
+ return (ctx, next) => __async(null, null, function* () {
187
+ if (ctx.init.method === "GET") {
188
+ const cached = cache.get(ctx.url);
189
+ const now = Date.now();
190
+ if (cached && cached.expires > now)
191
+ return new Response(JSON.stringify(cached.data));
192
+ const res = yield next();
193
+ const data = yield res.clone().json().catch(() => null);
194
+ if (data) cache.set(ctx.url, { data, expires: now + ttl });
195
+ return res;
196
+ }
197
+ return next();
198
+ });
199
+ };
200
+ export {
201
+ ApiClient,
202
+ RichError,
203
+ authMiddleware,
204
+ cacheMiddleware,
205
+ loggingMiddleware,
206
+ retryMiddleware
207
+ };
208
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/middlewares/logging.ts","../src/middlewares/retry.ts","../src/middlewares/auth.ts","../src/middlewares/cache.ts"],"sourcesContent":["import {\n Contracts,\n EndpointDef,\n EndpointDefZ,\n Middleware,\n ErrorLike,\n EndpointMethods,\n} from \"@/types\";\nimport { z } from \"zod\";\n\nexport class RichError extends Error implements ErrorLike {\n status?: number;\n code?: string;\n title?: string;\n detail?: string;\n errors?: Record<string, string[]>;\n\n constructor(error: Partial<ErrorLike> & { message: string }) {\n super(error.message);\n Object.assign(this, error);\n }\n}\n\nexport class ApiClient<C extends Contracts, E extends ErrorLike = RichError> {\n private middlewares: Array<{ fn: Middleware; options?: any }> = [];\n private errorHandler?: (error: E) => void;\n private responseTransform: (data: any) => any = (d) => d;\n\n private _modules!: {\n [M in keyof C]: EndpointMethods<C[M]>;\n };\n\n constructor(\n private config: { baseUrl: string; token?: string },\n private contracts: C\n ) {}\n\n init() {\n const modules = {} as {\n [M in keyof C]: EndpointMethods<C[M]>;\n };\n\n for (const moduleName in this.contracts) {\n const module = this.contracts[moduleName];\n (modules as any)[moduleName] = {} as EndpointMethods<typeof module>;\n\n for (const endpointName in module) {\n const endpoint = module[endpointName] as EndpointDefZ;\n\n (modules as any)[moduleName][endpointName] = (\n input: z.infer<(typeof endpoint)[\"request\"]>\n ) => this.request(endpoint, input);\n }\n }\n\n this._modules = modules;\n }\n\n get modules() {\n return this._modules;\n }\n\n use<T>(middleware: Middleware<T>, options?: T) {\n this.middlewares.push({ fn: middleware, options });\n }\n\n onError(handler: (error: E) => void) {\n this.errorHandler = handler;\n }\n\n useResponseTransform(fn: (data: any) => any) {\n this.responseTransform = fn;\n }\n\n private async request<TReq extends z.ZodTypeAny, TRes extends z.ZodTypeAny>(\n endpoint: EndpointDef<TReq, TRes>,\n input: z.infer<TReq>\n ): Promise<z.infer<TRes>> {\n endpoint.request.parse(input);\n\n if (endpoint.auth && !this.config.token) {\n const error = this.createError({\n message: `Missing token for ${endpoint.path}`,\n status: 401,\n code: \"NO_TOKEN\",\n });\n this.errorHandler?.(error as unknown as E);\n throw error;\n }\n\n const headers: HeadersInit = { \"Content-Type\": \"application/json\" };\n if (endpoint.auth && this.config.token)\n headers[\"Authorization\"] = `Bearer ${this.config.token}`;\n\n const ctx = {\n url: this.config.baseUrl + endpoint.path,\n init: {\n method: endpoint.method,\n headers,\n body: endpoint.method !== \"GET\" ? JSON.stringify(input) : undefined,\n },\n };\n\n const runner = this.middlewares.reduceRight(\n (next, mw) => () => mw.fn(ctx, next, mw.options),\n () => fetch(ctx.url, ctx.init)\n );\n\n try {\n const res = await runner();\n if (!res.ok) {\n const errorData = await res.json().catch(() => ({}));\n const error = this.createError({\n message: errorData.message || res.statusText,\n status: res.status,\n code: errorData.code,\n title: errorData.title,\n detail: errorData.detail,\n errors: errorData.errors,\n });\n this.errorHandler?.(error as unknown as E);\n throw error;\n }\n\n const json = await res.json();\n return this.responseTransform(endpoint.response.parse(json));\n } catch (err: any) {\n const error = this.normalizeError(err);\n this.errorHandler?.(error as unknown as E);\n throw error;\n }\n }\n\n private createError(error: Partial<RichError> & { message: string }) {\n return new RichError(error);\n }\n\n private normalizeError(err: any) {\n if (err instanceof RichError) return err;\n return this.createError({ message: err.message || \"Unknown error\" });\n }\n}\n","import { Middleware } from \"@/types\";\n\nexport type LoggingOptions = {\n logRequest?: boolean;\n logResponse?: boolean;\n debug?: boolean;\n};\n\nexport const loggingMiddleware: Middleware<LoggingOptions> = async (\n ctx,\n next,\n options\n) => {\n const { logRequest = true, logResponse = true, debug = true } = options || {};\n\n if (debug && logRequest) console.log(\"➡️ Request:\", ctx.url, ctx.init);\n\n const res = await next();\n\n if (debug && logResponse) console.log(\"⬅️ Response:\", res.status);\n\n return res;\n};\n","import { Middleware } from \"@/types\";\n\n\nexport type RetryOptions = {\n maxRetries?: number;\n delay?: number; // ms\n};\n\nexport const retryMiddleware = (options?: RetryOptions): Middleware => {\n const { maxRetries = 3, delay = 500 } = options || {};\n\n const middleware: Middleware = async (ctx, next) => {\n let attempt = 0;\n while (true) {\n try {\n return await next();\n } catch (err) {\n if (attempt >= maxRetries) throw err;\n attempt++;\n await new Promise((r) => setTimeout(r, delay * 2 ** attempt));\n }\n }\n };\n\n return middleware;\n};\n","import { Middleware } from \"@/types\";\n\nexport type AuthOptions = {\n refreshToken?: () => Promise<string>;\n};\n\nexport const authMiddleware: Middleware<AuthOptions> = async (\n ctx,\n next,\n options\n) => {\n if (options?.refreshToken) {\n try {\n const newToken = await options.refreshToken();\n ctx.init.headers = {\n ...ctx.init.headers,\n Authorization: `Bearer ${newToken}`,\n };\n } catch {}\n }\n\n return next();\n};\n","import { MiddlewareContext, MiddlewareNext } from \"@/types\";\n\nexport type CacheOptions = { ttl?: number };\n\nexport const cacheMiddleware = (options: CacheOptions = {}) => {\n const { ttl = 60000 } = options;\n const cache = new Map<string, { data: any; expires: number }>();\n\n return async (ctx: MiddlewareContext, next: MiddlewareNext) => {\n if (ctx.init.method === \"GET\") {\n const cached = cache.get(ctx.url);\n const now = Date.now();\n if (cached && cached.expires > now)\n return new Response(JSON.stringify(cached.data));\n\n const res = await next();\n const data = await res\n .clone()\n .json()\n .catch(() => null);\n if (data) cache.set(ctx.url, { data, expires: now + ttl });\n return res;\n }\n return next();\n };\n};\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@tahanabavi/typefetch",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "/dist"
10
+ ],
7
11
  "scripts": {
8
- "build": "tsc",
12
+ "build": "tsup",
9
13
  "test": "jest",
10
14
  "test:watch": "jest --watch",
11
15
  "test:coverage": "jest --coverage"
@@ -15,6 +19,7 @@
15
19
  "jest": "^30.1.3",
16
20
  "jest-fetch-mock": "^3.0.3",
17
21
  "ts-jest": "^29.4.1",
22
+ "tsup": "^8.5.0",
18
23
  "typescript": "^5.9.2",
19
24
  "zod": "^3.25.76"
20
25
  },
@@ -1,36 +0,0 @@
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 }}
@@ -1 +0,0 @@
1
- export {};
@@ -1,108 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,85 +0,0 @@
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 DELETED
@@ -1,31 +0,0 @@
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
- }
package/dist/client.js DELETED
@@ -1,98 +0,0 @@
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;
@@ -1,5 +0,0 @@
1
- import { Middleware } from "../types";
2
- export type AuthOptions = {
3
- refreshToken?: () => Promise<string>;
4
- };
5
- export declare const authMiddleware: Middleware<AuthOptions>;
@@ -1,17 +0,0 @@
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;
@@ -1,5 +0,0 @@
1
- import { MiddlewareContext, MiddlewareNext } from "../types";
2
- export type CacheOptions = {
3
- ttl?: number;
4
- };
5
- export declare const cacheMiddleware: (options?: CacheOptions) => (ctx: MiddlewareContext, next: MiddlewareNext) => Promise<Response>;