From d2552278ca304998552041903c5a647698d14ab7 Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 5 May 2025 17:47:13 +0500 Subject: [PATCH 01/49] feat: init --- services/http-request/.npmignore | 1 + services/http-request/README.md | 12 +++++ services/http-request/package.json | 34 +++++++++++++ services/http-request/rollup.config.js | 11 ++++ services/http-request/src/httpRequest.ts | 50 +++++++++++++++++++ .../http-request/src/httpRequest.types.ts | 0 services/http-request/src/rest.types.ts | 7 +++ services/http-request/src/restController.ts | 20 ++++++++ .../http-request/src/restController.types.ts | 4 ++ .../src/restControllerTemplates.ts | 34 +++++++++++++ services/http-request/tsconfig.json | 13 +++++ 11 files changed, 186 insertions(+) create mode 100644 services/http-request/.npmignore create mode 100644 services/http-request/README.md create mode 100644 services/http-request/package.json create mode 100644 services/http-request/rollup.config.js create mode 100644 services/http-request/src/httpRequest.ts create mode 100644 services/http-request/src/httpRequest.types.ts create mode 100644 services/http-request/src/rest.types.ts create mode 100644 services/http-request/src/restController.ts create mode 100644 services/http-request/src/restController.types.ts create mode 100644 services/http-request/src/restControllerTemplates.ts create mode 100644 services/http-request/tsconfig.json diff --git a/services/http-request/.npmignore b/services/http-request/.npmignore new file mode 100644 index 00000000..e8310385 --- /dev/null +++ b/services/http-request/.npmignore @@ -0,0 +1 @@ +src \ No newline at end of file diff --git a/services/http-request/README.md b/services/http-request/README.md new file mode 100644 index 00000000..97b21d63 --- /dev/null +++ b/services/http-request/README.md @@ -0,0 +1,12 @@ +# `@byndyusoft-ui/http-request` + +> Http request service +### Installation + +```bash +npm i @byndyusoft-ui/http-request +``` + +## Usage + +TBD \ No newline at end of file diff --git a/services/http-request/package.json b/services/http-request/package.json new file mode 100644 index 00000000..6579eacc --- /dev/null +++ b/services/http-request/package.json @@ -0,0 +1,34 @@ +{ + "name": "@byndyusoft-ui/http-request", + "version": "0.0.1", + "description": "Byndyusoft UI HTTP Service", + "keywords": [ + "byndyusoft", + "byndyusoft-ui", + "http", + "request", + "axios", + "fetch" + ], + "author": "Byndyusoft Frontend Developer ", + "homepage": "https://github.com/Byndyusoft/ui/tree/master/services/http-request#readme", + "license": "Apache-2.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/Byndyusoft/ui.git" + }, + "scripts": { + "build": "tsc --project tsconfig.build.json", + "clean": "rimraf dist", + "lint": "eslint src --config ../../eslint.config.js", + "test": "jest --config ../../jest.config.js --roots services/pub-sub/src" + }, + "bugs": { + "url": "https://github.com/Byndyusoft/ui/issues" + }, + "publishConfig": { + "access": "public" + } + } \ No newline at end of file diff --git a/services/http-request/rollup.config.js b/services/http-request/rollup.config.js new file mode 100644 index 00000000..87cb86d1 --- /dev/null +++ b/services/http-request/rollup.config.js @@ -0,0 +1,11 @@ +import typescript from '@rollup/plugin-typescript'; +import baseConfig from '../../rollup.base.config'; + +export default { + ...baseConfig, + input: ['src/index.ts'], + plugins: [ + ...baseConfig.plugins, + typescript({ tsconfig: './tsconfig.json', exclude: ['src/**/*.stories.tsx', 'src/**/*.tests.tsx', 'node_modules'] }) + ] +}; diff --git a/services/http-request/src/httpRequest.ts b/services/http-request/src/httpRequest.ts new file mode 100644 index 00000000..886b647e --- /dev/null +++ b/services/http-request/src/httpRequest.ts @@ -0,0 +1,50 @@ +import HttpRestController from './restController'; +import { IHTTPRequestRestController } from './restController.types'; +import { fetchRestController, TFetchGetFn, TFetchPostFn } from './restControllerTemplates'; + +interface IHttpRestControllerOptions { + restController?: IHTTPRequestRestController; +} + +class HttpRequest< + GetHandler extends (...args: Parameters) => ReturnType = TFetchGetFn, + PostHandler extends (...args: Parameters) => ReturnType = TFetchPostFn +> { + private restController: + | IHTTPRequestRestController + | IHTTPRequestRestController; + + constructor(options?: IHttpRestControllerOptions) { + if (options?.restController) { + this.restController = new HttpRestController(options?.restController); + } else { + this.restController = new HttpRestController(fetchRestController); + } + } + + get(...args: Parameters) { + return Function.prototype.apply(this.restController.get, args) as GetHandler; + } + + post(...args: Parameters) { + return Function.prototype.apply(this.restController.post, args) as PostHandler; + } +} + +// type TTestGet = () => Promise; +// type TTestPost = (url: string, body: object) => Promise; + +// const TestController = new HttpRestController({ +// get() { +// return Promise.resolve() as Promise; +// }, +// post: (url, body) => { +// return new Promise(() => {}); +// } +// }); + +// const Test = new HttpRequest({ +// restController: TestController +// }); + +export default HttpRequest; diff --git a/services/http-request/src/httpRequest.types.ts b/services/http-request/src/httpRequest.types.ts new file mode 100644 index 00000000..e69de29b diff --git a/services/http-request/src/rest.types.ts b/services/http-request/src/rest.types.ts new file mode 100644 index 00000000..4a4276cb --- /dev/null +++ b/services/http-request/src/rest.types.ts @@ -0,0 +1,7 @@ +type TRequestUrlParam = string; +type TRequestParams = object; +type TRequestOptions = object; +type TRequestBody = object; + +export type TRequestGetArguments = [url: TRequestUrlParam, options?: RequestInit]; +export type TRequestPostArguments = [url: TRequestUrlParam, body?: B]; diff --git a/services/http-request/src/restController.ts b/services/http-request/src/restController.ts new file mode 100644 index 00000000..8a28e486 --- /dev/null +++ b/services/http-request/src/restController.ts @@ -0,0 +1,20 @@ +import { IHTTPRequestRestController } from './restController.types'; + +interface IHttpRestControllerOptions + extends IHTTPRequestRestController {} + +class HttpRestController implements IHTTPRequestRestController { + constructor(public options: IHttpRestControllerOptions) { + this.get = options.get; + this.post = options.post; + } + + get: GetHandler; + post: PostHandler; + + // post(args: PostArguments): Promise { + // return this.options.post(args); + // } +} + +export default HttpRestController; diff --git a/services/http-request/src/restController.types.ts b/services/http-request/src/restController.types.ts new file mode 100644 index 00000000..9330a57c --- /dev/null +++ b/services/http-request/src/restController.types.ts @@ -0,0 +1,4 @@ +export interface IHTTPRequestRestController { + get: GetHandler; + post: PostHandler; +} diff --git a/services/http-request/src/restControllerTemplates.ts b/services/http-request/src/restControllerTemplates.ts new file mode 100644 index 00000000..1e22dd1f --- /dev/null +++ b/services/http-request/src/restControllerTemplates.ts @@ -0,0 +1,34 @@ +import { IHTTPRequestRestController } from './restController.types'; + +export type TFetchGetArguments = [url: string, options?: RequestInit]; +export type TFetchPostArgumentsBody = object | string | number | boolean | undefined; +export type TFetchPostArguments = [url: string, body: TFetchPostArgumentsBody, options?: RequestInit]; + +export type TFetchGetFn = (...args: TFetchGetArguments) => Promise; +export type TFetchPostFn = (...args: TFetchPostArguments) => Promise; + +// Default rest controller with 'fetch' +export const fetchRestController: IHTTPRequestRestController = { + get: async (...args) => { + const [url, options] = args; + + try { + const response = await fetch(url, options); + return response.json(); + } catch (error) { + throw error; + } + }, + post: async (...args) => { + const [url, body] = args; + try { + const response = await fetch(url, { + method: 'POST', + body: body ? JSON.stringify(body) : undefined + }); + return response.json(); + } catch (error) { + throw error; + } + } +}; diff --git a/services/http-request/tsconfig.json b/services/http-request/tsconfig.json new file mode 100644 index 00000000..567aaea9 --- /dev/null +++ b/services/http-request/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationDir": "dist", + "outDir": "dist", + "module": "commonjs" + }, + "include": [ + "../../types.d.ts", + "src" + ] +} From e7da701ac9cc0769de302de2cd02592524e1ca32 Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 5 May 2025 20:27:45 +0500 Subject: [PATCH 02/49] feat: rest methods --- services/http-request/src/httpRequest.ts | 73 +++++++++++++++---- .../http-request/src/httpRequest.types.ts | 0 services/http-request/src/rest.types.ts | 7 -- services/http-request/src/restController.ts | 32 ++++++-- .../http-request/src/restController.types.ts | 5 +- .../src/restControllerTemplates.ts | 44 ++++++++++- 6 files changed, 131 insertions(+), 30 deletions(-) delete mode 100644 services/http-request/src/httpRequest.types.ts delete mode 100644 services/http-request/src/rest.types.ts diff --git a/services/http-request/src/httpRequest.ts b/services/http-request/src/httpRequest.ts index 886b647e..521f60b0 100644 --- a/services/http-request/src/httpRequest.ts +++ b/services/http-request/src/httpRequest.ts @@ -2,37 +2,84 @@ import HttpRestController from './restController'; import { IHTTPRequestRestController } from './restController.types'; import { fetchRestController, TFetchGetFn, TFetchPostFn } from './restControllerTemplates'; -interface IHttpRestControllerOptions { - restController?: IHTTPRequestRestController; +interface IHttpRequestOptions { + restController?: IHTTPRequestRestController; } class HttpRequest< GetHandler extends (...args: Parameters) => ReturnType = TFetchGetFn, - PostHandler extends (...args: Parameters) => ReturnType = TFetchPostFn + PostHandler extends (...args: Parameters) => ReturnType = TFetchPostFn, + PatchHandler extends (...args: Parameters) => ReturnType = TFetchPostFn, + PutHandler extends (...args: Parameters) => ReturnType = TFetchPostFn, + DeleteHandler extends (...args: Parameters) => ReturnType = TFetchPostFn > { - private restController: - | IHTTPRequestRestController - | IHTTPRequestRestController; + private readonly restController: + | IHTTPRequestRestController + | IHTTPRequestRestController; - constructor(options?: IHttpRestControllerOptions) { + constructor(options?: IHttpRequestOptions) { if (options?.restController) { - this.restController = new HttpRestController(options?.restController); + this.restController = new HttpRestController< + GetHandler, + PostHandler, + PatchHandler, + PutHandler, + DeleteHandler + >(options?.restController); } else { - this.restController = new HttpRestController(fetchRestController); + this.restController = new HttpRestController< + TFetchGetFn, + TFetchPostFn, + TFetchPostFn, + TFetchPostFn, + TFetchPostFn + >(fetchRestController); } } get(...args: Parameters) { - return Function.prototype.apply(this.restController.get, args) as GetHandler; + try { + return Function.prototype.apply(this.restController.get, args); + } catch (exception) { + throw exception; + } } post(...args: Parameters) { - return Function.prototype.apply(this.restController.post, args) as PostHandler; + try { + return Function.prototype.apply(this.restController.post, args); + } catch (exception) { + throw exception; + } + } + + patch(...args: Parameters): PatchHandler { + try { + return Function.prototype.apply(this.restController.patch, args); + } catch (exception) { + throw exception; + } + } + + put(...args: Parameters) { + try { + return Function.prototype.apply(this.restController.put, args); + } catch (exception) { + throw exception; + } + } + + delete(...args: Parameters) { + try { + return Function.prototype.apply(this.restController.delete, args); + } catch (exception) { + throw exception; + } } } // type TTestGet = () => Promise; -// type TTestPost = (url: string, body: object) => Promise; +// type TTestPost = (url: string, ebody: object) => Promise; // const TestController = new HttpRestController({ // get() { @@ -43,7 +90,7 @@ class HttpRequest< // } // }); -// const Test = new HttpRequest({ +// const Test = new HttpRequest({ // restController: TestController // }); diff --git a/services/http-request/src/httpRequest.types.ts b/services/http-request/src/httpRequest.types.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/services/http-request/src/rest.types.ts b/services/http-request/src/rest.types.ts deleted file mode 100644 index 4a4276cb..00000000 --- a/services/http-request/src/rest.types.ts +++ /dev/null @@ -1,7 +0,0 @@ -type TRequestUrlParam = string; -type TRequestParams = object; -type TRequestOptions = object; -type TRequestBody = object; - -export type TRequestGetArguments = [url: TRequestUrlParam, options?: RequestInit]; -export type TRequestPostArguments = [url: TRequestUrlParam, body?: B]; diff --git a/services/http-request/src/restController.ts b/services/http-request/src/restController.ts index 8a28e486..db9219b0 100644 --- a/services/http-request/src/restController.ts +++ b/services/http-request/src/restController.ts @@ -1,20 +1,36 @@ import { IHTTPRequestRestController } from './restController.types'; -interface IHttpRestControllerOptions - extends IHTTPRequestRestController {} +interface IHttpRestControllerOptions< + GetHandler, + PostHandler, + PatchHandler = undefined, + PutHandler = undefined, + DeleteHandler = undefined +> extends IHTTPRequestRestController {} -class HttpRestController implements IHTTPRequestRestController { - constructor(public options: IHttpRestControllerOptions) { +class HttpRestController< + GetHandler, + PostHandler, + PatchHandler = undefined, + PutHandler = undefined, + DeleteHandler = undefined +> implements IHTTPRequestRestController +{ + constructor( + public options: IHttpRestControllerOptions + ) { this.get = options.get; this.post = options.post; + this.patch = options.patch; + this.put = options.put; + this.delete = options.delete; } get: GetHandler; post: PostHandler; - - // post(args: PostArguments): Promise { - // return this.options.post(args); - // } + patch: PatchHandler | undefined; + put: PutHandler | undefined; + delete: DeleteHandler | undefined; } export default HttpRestController; diff --git a/services/http-request/src/restController.types.ts b/services/http-request/src/restController.types.ts index 9330a57c..ed8609d2 100644 --- a/services/http-request/src/restController.types.ts +++ b/services/http-request/src/restController.types.ts @@ -1,4 +1,7 @@ -export interface IHTTPRequestRestController { +export interface IHTTPRequestRestController { get: GetHandler; post: PostHandler; + patch?: PatchHandler | undefined; + put?: PutHandler | undefined; + delete?: DeleteHandler | undefined; } diff --git a/services/http-request/src/restControllerTemplates.ts b/services/http-request/src/restControllerTemplates.ts index 1e22dd1f..83b9c34a 100644 --- a/services/http-request/src/restControllerTemplates.ts +++ b/services/http-request/src/restControllerTemplates.ts @@ -8,7 +8,13 @@ export type TFetchGetFn = (...args: TFetchGetArguments) => Promise; export type TFetchPostFn = (...args: TFetchPostArguments) => Promise; // Default rest controller with 'fetch' -export const fetchRestController: IHTTPRequestRestController = { +export const fetchRestController: IHTTPRequestRestController< + TFetchGetFn, + TFetchPostFn, + TFetchPostFn, + TFetchPostFn, + TFetchPostFn +> = { get: async (...args) => { const [url, options] = args; @@ -30,5 +36,41 @@ export const fetchRestController: IHTTPRequestRestController { + const [url, body] = args; + try { + const response = await fetch(url, { + method: 'PATCH', + body: body ? JSON.stringify(body) : undefined + }); + return response.json(); + } catch (error) { + throw error; + } + }, + put: async (...args) => { + const [url, body] = args; + try { + const response = await fetch(url, { + method: 'PUT', + body: body ? JSON.stringify(body) : undefined + }); + return response.json(); + } catch (error) { + throw error; + } + }, + delete: async (...args) => { + const [url, body] = args; + try { + const response = await fetch(url, { + method: 'DELETE', + body: body ? JSON.stringify(body) : undefined + }); + return response.json(); + } catch (error) { + throw error; + } } }; From 6c4752be482e1c88b2cd490f1e9c3435a9153d92 Mon Sep 17 00:00:00 2001 From: ancientbag Date: Tue, 6 May 2025 12:53:22 +0500 Subject: [PATCH 03/49] feat: refactor --- services/http-request/src/httpRequest.ts | 60 +++++++++++++------ .../http-request/src/httpRequest.types.ts | 5 ++ services/http-request/src/restController.ts | 12 +--- .../http-request/src/restController.types.ts | 10 +++- .../src/restControllerTemplates.ts | 6 +- 5 files changed, 62 insertions(+), 31 deletions(-) create mode 100644 services/http-request/src/httpRequest.types.ts diff --git a/services/http-request/src/httpRequest.ts b/services/http-request/src/httpRequest.ts index 521f60b0..2916d821 100644 --- a/services/http-request/src/httpRequest.ts +++ b/services/http-request/src/httpRequest.ts @@ -1,21 +1,28 @@ +import { IHttpRequestOptions } from './httpRequest.types'; import HttpRestController from './restController'; -import { IHTTPRequestRestController } from './restController.types'; +import { IHTTPRestController } from './restController.types'; import { fetchRestController, TFetchGetFn, TFetchPostFn } from './restControllerTemplates'; -interface IHttpRequestOptions { - restController?: IHTTPRequestRestController; -} - class HttpRequest< - GetHandler extends (...args: Parameters) => ReturnType = TFetchGetFn, - PostHandler extends (...args: Parameters) => ReturnType = TFetchPostFn, - PatchHandler extends (...args: Parameters) => ReturnType = TFetchPostFn, - PutHandler extends (...args: Parameters) => ReturnType = TFetchPostFn, - DeleteHandler extends (...args: Parameters) => ReturnType = TFetchPostFn + GetHandler extends (...args: Parameters) => ReturnType = ( + ...args: Parameters + ) => R, + PostHandler extends (...args: Parameters) => ReturnType = ( + ...args: Parameters + ) => R, + PatchHandler extends (...args: Parameters) => ReturnType = ( + ...args: Parameters + ) => R, + PutHandler extends (...args: Parameters) => ReturnType = ( + ...args: Parameters + ) => R, + DeleteHandler extends (...args: Parameters) => ReturnType = ( + ...args: Parameters + ) => R > { private readonly restController: - | IHTTPRequestRestController - | IHTTPRequestRestController; + | IHTTPRestController + | IHTTPRestController; constructor(options?: IHttpRequestOptions) { if (options?.restController) { @@ -37,7 +44,7 @@ class HttpRequest< } } - get(...args: Parameters) { + get(...args: Parameters): Promise { try { return Function.prototype.apply(this.restController.get, args); } catch (exception) { @@ -45,7 +52,7 @@ class HttpRequest< } } - post(...args: Parameters) { + post(...args: Parameters): Promise { try { return Function.prototype.apply(this.restController.post, args); } catch (exception) { @@ -53,7 +60,7 @@ class HttpRequest< } } - patch(...args: Parameters): PatchHandler { + patch(...args: Parameters): Promise { try { return Function.prototype.apply(this.restController.patch, args); } catch (exception) { @@ -61,7 +68,7 @@ class HttpRequest< } } - put(...args: Parameters) { + put(...args: Parameters): Promise { try { return Function.prototype.apply(this.restController.put, args); } catch (exception) { @@ -69,7 +76,7 @@ class HttpRequest< } } - delete(...args: Parameters) { + delete(...args: Parameters): Promise { try { return Function.prototype.apply(this.restController.delete, args); } catch (exception) { @@ -94,4 +101,23 @@ class HttpRequest< // restController: TestController // }); +async () => { + const TestController = new HttpRestController({ + get(url: string) { + console.log(url); + return Promise.resolve(); + }, + post: (url: string, body: object) => { + console.log({ url, body }); + return new Promise(() => {}); + } + }); + + const httpRequestService = new HttpRequest({ + restController: TestController + }); + + const data = await httpRequestService.get<{ data: string }>('http://localhost:1234/api'); +}; + export default HttpRequest; diff --git a/services/http-request/src/httpRequest.types.ts b/services/http-request/src/httpRequest.types.ts new file mode 100644 index 00000000..c4db91b0 --- /dev/null +++ b/services/http-request/src/httpRequest.types.ts @@ -0,0 +1,5 @@ +import { IHTTPRestController } from './restController.types'; + +export interface IHttpRequestOptions { + restController?: IHTTPRestController; +} diff --git a/services/http-request/src/restController.ts b/services/http-request/src/restController.ts index db9219b0..1ebc4d17 100644 --- a/services/http-request/src/restController.ts +++ b/services/http-request/src/restController.ts @@ -1,12 +1,4 @@ -import { IHTTPRequestRestController } from './restController.types'; - -interface IHttpRestControllerOptions< - GetHandler, - PostHandler, - PatchHandler = undefined, - PutHandler = undefined, - DeleteHandler = undefined -> extends IHTTPRequestRestController {} +import { IHTTPRestController, IHttpRestControllerOptions } from './restController.types'; class HttpRestController< GetHandler, @@ -14,7 +6,7 @@ class HttpRestController< PatchHandler = undefined, PutHandler = undefined, DeleteHandler = undefined -> implements IHTTPRequestRestController +> implements IHTTPRestController { constructor( public options: IHttpRestControllerOptions diff --git a/services/http-request/src/restController.types.ts b/services/http-request/src/restController.types.ts index ed8609d2..222d9e6b 100644 --- a/services/http-request/src/restController.types.ts +++ b/services/http-request/src/restController.types.ts @@ -1,7 +1,15 @@ -export interface IHTTPRequestRestController { +export interface IHTTPRestController { get: GetHandler; post: PostHandler; patch?: PatchHandler | undefined; put?: PutHandler | undefined; delete?: DeleteHandler | undefined; } + +export interface IHttpRestControllerOptions< + GetHandler, + PostHandler, + PatchHandler = undefined, + PutHandler = undefined, + DeleteHandler = undefined +> extends IHTTPRestController {} diff --git a/services/http-request/src/restControllerTemplates.ts b/services/http-request/src/restControllerTemplates.ts index 83b9c34a..6c9dc16f 100644 --- a/services/http-request/src/restControllerTemplates.ts +++ b/services/http-request/src/restControllerTemplates.ts @@ -1,14 +1,14 @@ -import { IHTTPRequestRestController } from './restController.types'; +import { IHTTPRestController } from './restController.types'; export type TFetchGetArguments = [url: string, options?: RequestInit]; export type TFetchPostArgumentsBody = object | string | number | boolean | undefined; export type TFetchPostArguments = [url: string, body: TFetchPostArgumentsBody, options?: RequestInit]; -export type TFetchGetFn = (...args: TFetchGetArguments) => Promise; +export type TFetchGetFn> = (...args: TFetchGetArguments) => Promise; export type TFetchPostFn = (...args: TFetchPostArguments) => Promise; // Default rest controller with 'fetch' -export const fetchRestController: IHTTPRequestRestController< +export const fetchRestController: IHTTPRestController< TFetchGetFn, TFetchPostFn, TFetchPostFn, From 77756ca3015d47287f31104e827c3f33a633cd37 Mon Sep 17 00:00:00 2001 From: ancientbag Date: Tue, 6 May 2025 16:37:27 +0500 Subject: [PATCH 04/49] feat: refactor, reexports --- package-lock.json | 19 +++++++++++-- services/http-request/README.md | 27 +++++++++++++++++- services/http-request/package.json | 17 ++++++++---- services/http-request/src/httpRequest.ts | 35 ------------------------ services/http-request/src/index.ts | 4 +++ 5 files changed, 59 insertions(+), 43 deletions(-) create mode 100644 services/http-request/src/index.ts diff --git a/package-lock.json b/package-lock.json index c06cb0c4..a2617d2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -691,8 +691,8 @@ "node": ">=6.9.0" } }, - "node_modules/@byndyusoft-ui/css-tools": { - "resolved": "styles/tools", + "node_modules/@byndyusoft-ui/css-utilities": { + "resolved": "styles/utilities", "link": true }, "node_modules/@byndyusoft-ui/flex": { @@ -707,6 +707,10 @@ "resolved": "components/highlighter", "link": true }, + "node_modules/@byndyusoft-ui/http-request": { + "resolved": "services/http-request", + "link": true + }, "node_modules/@byndyusoft-ui/keyframes-css": { "resolved": "styles/keyframes-css", "link": true @@ -11151,6 +11155,11 @@ "version": "0.3.0", "license": "Apache-2.0" }, + "services/http-request": { + "name": "@byndyusoft-ui/http-request", + "version": "0.0.1", + "license": "Apache-2.0" + }, "styles/keyframes-css": { "name": "@byndyusoft-ui/keyframes-css", "version": "0.0.1", @@ -11164,6 +11173,12 @@ "styles/tools": { "name": "@byndyusoft-ui/css-tools", "version": "0.0.1", + "extraneous": true, + "license": "Apache-2.0" + }, + "styles/utilities": { + "name": "@byndyusoft-ui/css-utilities", + "version": "0.0.1", "license": "Apache-2.0" } } diff --git a/services/http-request/README.md b/services/http-request/README.md index 97b21d63..b39e247b 100644 --- a/services/http-request/README.md +++ b/services/http-request/README.md @@ -9,4 +9,29 @@ npm i @byndyusoft-ui/http-request ## Usage -TBD \ No newline at end of file +### To start using this service you need to create a new class instance of HttpRequest. +By default this service is using 'fetch' for sending requests. + +```ts + const httpRequestService = new HttpRequest(); + + httpRequestService.get("http://localhost:3000/api/"); +``` + +### You can define own HttpRestController and pass it like on example below. +❗GET and POST methods must be described, others (PATCH/PUT/DELETE) are optional. + +```ts + const restController = new HttpRestController({ + get: () => {}, + post: () => {}, + /// ... + }) + + const httpRequestService = new HttpRequest({ + restController + }); + + httpRequestService.get("http://localhost:3000/api/"); + +``` \ No newline at end of file diff --git a/services/http-request/package.json b/services/http-request/package.json index 6579eacc..d2bc8622 100644 --- a/services/http-request/package.json +++ b/services/http-request/package.json @@ -20,11 +20,18 @@ "url": "git+https://github.com/Byndyusoft/ui.git" }, "scripts": { - "build": "tsc --project tsconfig.build.json", - "clean": "rimraf dist", - "lint": "eslint src --config ../../eslint.config.js", - "test": "jest --config ../../jest.config.js --roots services/pub-sub/src" - }, + "build": "rollup --config", + "clean": "rimraf dist", + "test": "jest --config ../../jest.config.js --roots services/http-request/src", + "lint:check": "npm run eslint:check && npm run prettier:check && npm run stylelint:check", + "lint:fix": "npm run eslint:fix && npm run prettier:fix && npm run stylelint:fix", + "eslint:check": "eslint src --config ../../eslint.config.js", + "eslint:fix": "eslint src --config ../../eslint.config.js --fix", + "prettier:check": "prettier --check '**/*.{ts,tsx,css,scss,json}' '!**/dist/**'", + "prettier:fix": "prettier --write '**/*.{ts,tsx,css,scss,json}' '!**/dist/**'", + "stylelint:check": "stylelint '**/*.{css,scss}' --allow-empty-input", + "stylelint:fix": "stylelint '**/*.{css,scss}' --fix --allow-empty-input" + }, "bugs": { "url": "https://github.com/Byndyusoft/ui/issues" }, diff --git a/services/http-request/src/httpRequest.ts b/services/http-request/src/httpRequest.ts index 2916d821..ae0e39a8 100644 --- a/services/http-request/src/httpRequest.ts +++ b/services/http-request/src/httpRequest.ts @@ -85,39 +85,4 @@ class HttpRequest< } } -// type TTestGet = () => Promise; -// type TTestPost = (url: string, ebody: object) => Promise; - -// const TestController = new HttpRestController({ -// get() { -// return Promise.resolve() as Promise; -// }, -// post: (url, body) => { -// return new Promise(() => {}); -// } -// }); - -// const Test = new HttpRequest({ -// restController: TestController -// }); - -async () => { - const TestController = new HttpRestController({ - get(url: string) { - console.log(url); - return Promise.resolve(); - }, - post: (url: string, body: object) => { - console.log({ url, body }); - return new Promise(() => {}); - } - }); - - const httpRequestService = new HttpRequest({ - restController: TestController - }); - - const data = await httpRequestService.get<{ data: string }>('http://localhost:1234/api'); -}; - export default HttpRequest; diff --git a/services/http-request/src/index.ts b/services/http-request/src/index.ts new file mode 100644 index 00000000..d3ae3f61 --- /dev/null +++ b/services/http-request/src/index.ts @@ -0,0 +1,4 @@ +import HttpRequest from './httpRequest'; +import HttpRestController from './restController'; + +export { HttpRequest, HttpRestController }; From 8b6d25391d4dbb2169897892c86b1569d7a35cdf Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 12 May 2025 14:30:36 +0500 Subject: [PATCH 05/49] feat: refactor --- .../src/constants/axiosRestController.ts | 71 ++++++++++++ .../src/constants/fetchRestController.ts | 49 ++++++++ services/http-request/src/httpRequest.ts | 109 +++++------------- .../http-request/src/httpRequest.types.ts | 6 +- services/http-request/src/index.ts | 4 +- services/http-request/src/restController.ts | 31 +---- .../http-request/src/restController.types.ts | 15 --- .../src/restControllerTemplates.ts | 76 ------------ 8 files changed, 162 insertions(+), 199 deletions(-) create mode 100644 services/http-request/src/constants/axiosRestController.ts create mode 100644 services/http-request/src/constants/fetchRestController.ts delete mode 100644 services/http-request/src/restController.types.ts delete mode 100644 services/http-request/src/restControllerTemplates.ts diff --git a/services/http-request/src/constants/axiosRestController.ts b/services/http-request/src/constants/axiosRestController.ts new file mode 100644 index 00000000..375e4491 --- /dev/null +++ b/services/http-request/src/constants/axiosRestController.ts @@ -0,0 +1,71 @@ +import axios, { AxiosInstance, AxiosResponse } from 'axios'; +import HttpRestController from '../restController'; + +const DEFAULT_REQUEST_TIMEOUT = 60000; + +export class HttpRestControllerAxios extends HttpRestController { + public axiosInstance: AxiosInstance; + + constructor() { + super(); + this.axiosInstance = axios.create({ + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Cache-control': 'no-cache', + Pragma: 'no-cache', + 'Access-Control-Expose-Headers': 'Content-Disposition' + }, + timeout: DEFAULT_REQUEST_TIMEOUT + }); + } + + async get>(url: string, params: object = {}, options: object = {}): Promise { + return this.axiosInstance.get(url, { + params, + ...options + }); + } + + async post>( + url: string, + body?: object, + params: object = {}, + options: object = {} + ): Promise { + return this.axiosInstance.post(url, body, { + params, + ...options + }); + } + + async patch>( + url: string, + body?: object, + params: object = {}, + options: object = {} + ): Promise { + return this.axiosInstance.patch(url, body, { + params, + ...options + }); + } + + async put>( + url: string, + body?: object, + params: object = {}, + options: object = {} + ): Promise { + return this.axiosInstance.put(url, body, { + params, + ...options + }); + } + + async delete>(url: string, params: object = {}, options: object = {}): Promise { + return this.axiosInstance.delete(url, { + params, + ...options + }); + } +} diff --git a/services/http-request/src/constants/fetchRestController.ts b/services/http-request/src/constants/fetchRestController.ts new file mode 100644 index 00000000..96ce0189 --- /dev/null +++ b/services/http-request/src/constants/fetchRestController.ts @@ -0,0 +1,49 @@ +import HttpRestController from '../restController'; + +export class HttpRestControllerFetch extends HttpRestController { + constructor() { + super(); + this.headers = new Headers(); + } + + public headers: Headers; + + async get(url: string, headers?: Headers): Promise { + return fetch(url, { method: 'GET', headers: { ...this.headers, ...headers } }) as Promise; + } + + async post(url: string, body: object, headers?: Headers): Promise { + return fetch(url, { + method: 'POST', + body: JSON.stringify(body), + headers: { + ...this.headers, + ...headers + } + }) as Promise; + } + + async put(url: string, body: object, headers?: Headers): Promise { + return fetch(url, { + method: 'PUT', + body: JSON.stringify(body), + headers: { ...this.headers, ...headers } + }) as Promise; + } + + async patch(url: string, body: object, headers?: Headers): Promise { + return fetch(url, { + method: 'PATCH', + body: JSON.stringify(body), + headers: { ...this.headers, ...headers } + }) as Promise; + } + + async delete(url: string, body: object = {}, headers?: Headers): Promise { + return fetch(url, { + method: 'DELETE', + body: JSON.stringify(body), + headers: { ...this.headers, ...headers } + }) as Promise; + } +} diff --git a/services/http-request/src/httpRequest.ts b/services/http-request/src/httpRequest.ts index ae0e39a8..acffc92e 100644 --- a/services/http-request/src/httpRequest.ts +++ b/services/http-request/src/httpRequest.ts @@ -1,88 +1,39 @@ +import { HttpRestControllerAxios } from './constants/axiosRestController'; +import { HttpRestControllerFetch } from './constants/fetchRestController'; import { IHttpRequestOptions } from './httpRequest.types'; import HttpRestController from './restController'; -import { IHTTPRestController } from './restController.types'; -import { fetchRestController, TFetchGetFn, TFetchPostFn } from './restControllerTemplates'; -class HttpRequest< - GetHandler extends (...args: Parameters) => ReturnType = ( - ...args: Parameters - ) => R, - PostHandler extends (...args: Parameters) => ReturnType = ( - ...args: Parameters - ) => R, - PatchHandler extends (...args: Parameters) => ReturnType = ( - ...args: Parameters - ) => R, - PutHandler extends (...args: Parameters) => ReturnType = ( - ...args: Parameters - ) => R, - DeleteHandler extends (...args: Parameters) => ReturnType = ( - ...args: Parameters - ) => R -> { - private readonly restController: - | IHTTPRestController - | IHTTPRestController; - - constructor(options?: IHttpRequestOptions) { - if (options?.restController) { - this.restController = new HttpRestController< - GetHandler, - PostHandler, - PatchHandler, - PutHandler, - DeleteHandler - >(options?.restController); +class HttpRequest { + public restController: RestController | undefined; + + public get: RestController['get']; + public post: RestController['post']; + public patch: RestController['patch']; + public put: RestController['put']; + public delete: RestController['delete']; + + constructor(options: IHttpRequestOptions) { + if (options.restController) { + const restController = options.restController; + this.restController = restController; + + this.get = this.restController.get; + this.post = this.restController.post; + this.patch = this.restController.patch; + this.put = this.restController.put; + this.delete = this.restController.delete; } else { - this.restController = new HttpRestController< - TFetchGetFn, - TFetchPostFn, - TFetchPostFn, - TFetchPostFn, - TFetchPostFn - >(fetchRestController); - } - } - - get(...args: Parameters): Promise { - try { - return Function.prototype.apply(this.restController.get, args); - } catch (exception) { - throw exception; - } - } - - post(...args: Parameters): Promise { - try { - return Function.prototype.apply(this.restController.post, args); - } catch (exception) { - throw exception; - } - } - - patch(...args: Parameters): Promise { - try { - return Function.prototype.apply(this.restController.patch, args); - } catch (exception) { - throw exception; - } - } - - put(...args: Parameters): Promise { - try { - return Function.prototype.apply(this.restController.put, args); - } catch (exception) { - throw exception; - } - } - - delete(...args: Parameters): Promise { - try { - return Function.prototype.apply(this.restController.delete, args); - } catch (exception) { - throw exception; + this.get = () => Promise.reject('get handler was not specified'); + this.post = () => Promise.reject('post handler was not specified'); + this.patch = () => Promise.reject('patch handler was not specified'); + this.put = () => Promise.reject('put handler was not specified'); + this.delete = () => Promise.reject('delete handler was not specified'); } } } +const httpRequest = new HttpRequest({ + restController: new HttpRestControllerAxios() +}); + export default HttpRequest; diff --git a/services/http-request/src/httpRequest.types.ts b/services/http-request/src/httpRequest.types.ts index c4db91b0..c0a66241 100644 --- a/services/http-request/src/httpRequest.types.ts +++ b/services/http-request/src/httpRequest.types.ts @@ -1,5 +1,5 @@ -import { IHTTPRestController } from './restController.types'; +import HttpRestController from './restController'; -export interface IHttpRequestOptions { - restController?: IHTTPRestController; +export interface IHttpRequestOptions { + restController?: RestController; } diff --git a/services/http-request/src/index.ts b/services/http-request/src/index.ts index d3ae3f61..a473fba8 100644 --- a/services/http-request/src/index.ts +++ b/services/http-request/src/index.ts @@ -1,4 +1,6 @@ import HttpRequest from './httpRequest'; import HttpRestController from './restController'; +import { HttpRestControllerAxios } from './constants/axiosRestController'; +import { HttpRestControllerFetch } from './constants/fetchRestController'; -export { HttpRequest, HttpRestController }; +export { HttpRequest, HttpRestController, HttpRestControllerAxios, HttpRestControllerFetch }; diff --git a/services/http-request/src/restController.ts b/services/http-request/src/restController.ts index 1ebc4d17..86070ca6 100644 --- a/services/http-request/src/restController.ts +++ b/services/http-request/src/restController.ts @@ -1,28 +1,9 @@ -import { IHTTPRestController, IHttpRestControllerOptions } from './restController.types'; - -class HttpRestController< - GetHandler, - PostHandler, - PatchHandler = undefined, - PutHandler = undefined, - DeleteHandler = undefined -> implements IHTTPRestController -{ - constructor( - public options: IHttpRestControllerOptions - ) { - this.get = options.get; - this.post = options.post; - this.patch = options.patch; - this.put = options.put; - this.delete = options.delete; - } - - get: GetHandler; - post: PostHandler; - patch: PatchHandler | undefined; - put: PutHandler | undefined; - delete: DeleteHandler | undefined; +abstract class HttpRestController { + abstract get(...args: any[]): Promise; + abstract post(...args: any[]): Promise; + abstract patch(...args: any[]): Promise; + abstract put(...args: any[]): Promise; + abstract delete(...args: any[]): Promise; } export default HttpRestController; diff --git a/services/http-request/src/restController.types.ts b/services/http-request/src/restController.types.ts deleted file mode 100644 index 222d9e6b..00000000 --- a/services/http-request/src/restController.types.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface IHTTPRestController { - get: GetHandler; - post: PostHandler; - patch?: PatchHandler | undefined; - put?: PutHandler | undefined; - delete?: DeleteHandler | undefined; -} - -export interface IHttpRestControllerOptions< - GetHandler, - PostHandler, - PatchHandler = undefined, - PutHandler = undefined, - DeleteHandler = undefined -> extends IHTTPRestController {} diff --git a/services/http-request/src/restControllerTemplates.ts b/services/http-request/src/restControllerTemplates.ts deleted file mode 100644 index 6c9dc16f..00000000 --- a/services/http-request/src/restControllerTemplates.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { IHTTPRestController } from './restController.types'; - -export type TFetchGetArguments = [url: string, options?: RequestInit]; -export type TFetchPostArgumentsBody = object | string | number | boolean | undefined; -export type TFetchPostArguments = [url: string, body: TFetchPostArgumentsBody, options?: RequestInit]; - -export type TFetchGetFn> = (...args: TFetchGetArguments) => Promise; -export type TFetchPostFn = (...args: TFetchPostArguments) => Promise; - -// Default rest controller with 'fetch' -export const fetchRestController: IHTTPRestController< - TFetchGetFn, - TFetchPostFn, - TFetchPostFn, - TFetchPostFn, - TFetchPostFn -> = { - get: async (...args) => { - const [url, options] = args; - - try { - const response = await fetch(url, options); - return response.json(); - } catch (error) { - throw error; - } - }, - post: async (...args) => { - const [url, body] = args; - try { - const response = await fetch(url, { - method: 'POST', - body: body ? JSON.stringify(body) : undefined - }); - return response.json(); - } catch (error) { - throw error; - } - }, - patch: async (...args) => { - const [url, body] = args; - try { - const response = await fetch(url, { - method: 'PATCH', - body: body ? JSON.stringify(body) : undefined - }); - return response.json(); - } catch (error) { - throw error; - } - }, - put: async (...args) => { - const [url, body] = args; - try { - const response = await fetch(url, { - method: 'PUT', - body: body ? JSON.stringify(body) : undefined - }); - return response.json(); - } catch (error) { - throw error; - } - }, - delete: async (...args) => { - const [url, body] = args; - try { - const response = await fetch(url, { - method: 'DELETE', - body: body ? JSON.stringify(body) : undefined - }); - return response.json(); - } catch (error) { - throw error; - } - } -}; From a899bb8809cf6a9e663c1b8cd2ab2fe7f76f4cd1 Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 12 May 2025 14:30:44 +0500 Subject: [PATCH 06/49] docs: readme upd --- services/http-request/README.md | 77 +++++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 13 deletions(-) diff --git a/services/http-request/README.md b/services/http-request/README.md index b39e247b..2afa69ce 100644 --- a/services/http-request/README.md +++ b/services/http-request/README.md @@ -9,29 +9,80 @@ npm i @byndyusoft-ui/http-request ## Usage -### To start using this service you need to create a new class instance of HttpRequest. -By default this service is using 'fetch' for sending requests. - +### To start using this service you need to create a new class instance of HttpRequest and provide restController option. +There are two classes ready to be used as restControllers: **HttpRestControllerFetch** and **HttpRestControllerAxios**. For fetch and axios. ```ts - const httpRequestService = new HttpRequest(); + const restController = new HttpRestControllerFetch(); + const httpRequest = new HttpRequest({ + restController +}); httpRequestService.get("http://localhost:3000/api/"); ``` -### You can define own HttpRestController and pass it like on example below. -❗GET and POST methods must be described, others (PATCH/PUT/DELETE) are optional. +### You can define own HttpRestController and pass it like this ```ts - const restController = new HttpRestController({ - get: () => {}, - post: () => {}, - /// ... - }) - + // myOwnHttpRestController.ts + import { HttpRestController } from '@byndyusoft-ui/http-request'; + + class HttpRestControllerCustom extends HttpRestController { + constructor() { + super(); + } + + async get(url: string, headers?: Headers): Promise { + return fetch(url, { method: 'GET', headers: { ...headers } }) as Promise; + } + + async post(url: string, body: object, headers?: Headers): Promise { + return fetch(url, { + method: 'POST', + body: JSON.stringify(body), + headers: { + ...this.headers, + ...headers + } + }) as Promise; + } + + async put(url: string, body: object, headers?: Headers): Promise { + return fetch(url, { + method: 'PUT', + body: JSON.stringify(body), + headers: { ...headers } + }) as Promise; + } + + async patch(url: string, body: object, headers?: Headers): Promise { + return fetch(url, { + method: 'PATCH', + body: JSON.stringify(body), + headers: { ...headers } + }) as Promise; + } + + async delete(url: string, body: object = {}, headers?: Headers): Promise { + return fetch(url, { + method: 'DELETE', + body: JSON.stringify(body), + headers: { ...headers } + }) as Promise; + } + } + + export default HttpRestControllerCustom; +``` +```ts + + // httpRequest.ts + import HttpRestControllerCustom from './myOwnHttpRestController.ts' + + const restController = new HttpRestControllerCustom(); const httpRequestService = new HttpRequest({ restController }); httpRequestService.get("http://localhost:3000/api/"); +``` -``` \ No newline at end of file From c37cc403f114060849c31198900f4c08445d43bc Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 12 May 2025 14:31:03 +0500 Subject: [PATCH 07/49] feat: axios added for package.json --- services/http-request/package.json | 49 ++++++++++++++++-------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/services/http-request/package.json b/services/http-request/package.json index d2bc8622..9e821375 100644 --- a/services/http-request/package.json +++ b/services/http-request/package.json @@ -3,12 +3,12 @@ "version": "0.0.1", "description": "Byndyusoft UI HTTP Service", "keywords": [ - "byndyusoft", - "byndyusoft-ui", - "http", - "request", - "axios", - "fetch" + "byndyusoft", + "byndyusoft-ui", + "http", + "request", + "axios", + "fetch" ], "author": "Byndyusoft Frontend Developer ", "homepage": "https://github.com/Byndyusoft/ui/tree/master/services/http-request#readme", @@ -16,26 +16,29 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "repository": { - "type": "git", - "url": "git+https://github.com/Byndyusoft/ui.git" + "type": "git", + "url": "git+https://github.com/Byndyusoft/ui.git" }, "scripts": { - "build": "rollup --config", - "clean": "rimraf dist", - "test": "jest --config ../../jest.config.js --roots services/http-request/src", - "lint:check": "npm run eslint:check && npm run prettier:check && npm run stylelint:check", - "lint:fix": "npm run eslint:fix && npm run prettier:fix && npm run stylelint:fix", - "eslint:check": "eslint src --config ../../eslint.config.js", - "eslint:fix": "eslint src --config ../../eslint.config.js --fix", - "prettier:check": "prettier --check '**/*.{ts,tsx,css,scss,json}' '!**/dist/**'", - "prettier:fix": "prettier --write '**/*.{ts,tsx,css,scss,json}' '!**/dist/**'", - "stylelint:check": "stylelint '**/*.{css,scss}' --allow-empty-input", - "stylelint:fix": "stylelint '**/*.{css,scss}' --fix --allow-empty-input" - }, + "build": "rollup --config", + "clean": "rimraf dist", + "test": "jest --config ../../jest.config.js --roots services/http-request/src", + "lint:check": "npm run eslint:check && npm run prettier:check && npm run stylelint:check", + "lint:fix": "npm run eslint:fix && npm run prettier:fix && npm run stylelint:fix", + "eslint:check": "eslint src --config ../../eslint.config.js", + "eslint:fix": "eslint src --config ../../eslint.config.js --fix", + "prettier:check": "prettier --check '**/*.{ts,tsx,css,scss,json}' '!**/dist/**'", + "prettier:fix": "prettier --write '**/*.{ts,tsx,css,scss,json}' '!**/dist/**'", + "stylelint:check": "stylelint '**/*.{css,scss}' --allow-empty-input", + "stylelint:fix": "stylelint '**/*.{css,scss}' --fix --allow-empty-input" + }, "bugs": { - "url": "https://github.com/Byndyusoft/ui/issues" + "url": "https://github.com/Byndyusoft/ui/issues" }, "publishConfig": { - "access": "public" + "access": "public" + }, + "dependencies": { + "axios": "^1.9.0" } - } \ No newline at end of file +} From a267aef21702aacc2305cbee3cb693a2c6781f7d Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 12 May 2025 16:01:50 +0500 Subject: [PATCH 08/49] feat: package lock upd --- package-lock.json | 62 +++++++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index a2617d2c..0fb55802 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3784,7 +3784,6 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "dev": true, "license": "MIT" }, "node_modules/atob": { @@ -3817,6 +3816,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "2.2.0", "dev": true, @@ -3989,7 +3999,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4268,7 +4277,6 @@ }, "node_modules/combined-stream": { "version": "1.0.8", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -4783,7 +4791,6 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -4854,7 +4861,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4945,7 +4951,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4953,7 +4958,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4966,7 +4970,6 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4977,7 +4980,6 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6131,6 +6133,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "dev": true, @@ -6167,7 +6189,6 @@ }, "node_modules/form-data": { "version": "4.0.2", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -6211,7 +6232,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6265,7 +6285,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -6288,7 +6307,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -6730,7 +6748,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6814,7 +6831,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6825,7 +6841,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -6839,7 +6854,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -8154,7 +8168,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8215,7 +8228,6 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -8223,7 +8235,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -8933,6 +8944,12 @@ "version": "16.13.1", "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "dev": true, @@ -11158,7 +11175,10 @@ "services/http-request": { "name": "@byndyusoft-ui/http-request", "version": "0.0.1", - "license": "Apache-2.0" + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.9.0" + } }, "styles/keyframes-css": { "name": "@byndyusoft-ui/keyframes-css", From 4cea0eaf39953df18026a547537670a13b18978d Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 12 May 2025 16:02:19 +0500 Subject: [PATCH 09/49] feat: refactor and setHeader method added --- services/http-request/README.md | 29 ++++++++--------- .../src/constants/axiosRestController.ts | 30 +++++++++-------- .../src/constants/fetchRestController.ts | 32 ++++++++++++------- services/http-request/src/httpRequest.ts | 9 ++---- services/http-request/src/restController.ts | 7 ++++ 5 files changed, 60 insertions(+), 47 deletions(-) diff --git a/services/http-request/README.md b/services/http-request/README.md index 2afa69ce..74462226 100644 --- a/services/http-request/README.md +++ b/services/http-request/README.md @@ -31,44 +31,41 @@ There are two classes ready to be used as restControllers: **HttpRestControllerF super(); } - async get(url: string, headers?: Headers): Promise { + get = async (url: string, headers?: Headers): Promise => { return fetch(url, { method: 'GET', headers: { ...headers } }) as Promise; - } + }; - async post(url: string, body: object, headers?: Headers): Promise { + post = async (url: string, body: object, headers?: Headers): Promise => { return fetch(url, { method: 'POST', body: JSON.stringify(body), - headers: { - ...this.headers, - ...headers - } + headers: { ...headers } }) as Promise; - } + }; - async put(url: string, body: object, headers?: Headers): Promise { + patch = async (url: string, body: object, headers?: Headers): Promise => { return fetch(url, { - method: 'PUT', + method: 'PATCH', body: JSON.stringify(body), headers: { ...headers } }) as Promise; - } + }; - async patch(url: string, body: object, headers?: Headers): Promise { + put = async (url: string, body: object, headers?: Headers): Promise => { return fetch(url, { - method: 'PATCH', + method: 'PUT', body: JSON.stringify(body), headers: { ...headers } }) as Promise; - } + }; - async delete(url: string, body: object = {}, headers?: Headers): Promise { + delete = async (url: string, body: object = {}, headers?: Headers): Promise => { return fetch(url, { method: 'DELETE', body: JSON.stringify(body), headers: { ...headers } }) as Promise; - } + }; } export default HttpRestControllerCustom; diff --git a/services/http-request/src/constants/axiosRestController.ts b/services/http-request/src/constants/axiosRestController.ts index 375e4491..dcf2e659 100644 --- a/services/http-request/src/constants/axiosRestController.ts +++ b/services/http-request/src/constants/axiosRestController.ts @@ -19,53 +19,57 @@ export class HttpRestControllerAxios extends HttpRestController { }); } - async get>(url: string, params: object = {}, options: object = {}): Promise { + setHeader = (key: string, value: string | null): void => { + this.axiosInstance.defaults.headers[key] = value; + }; + + get = async >(url: string, params: object = {}, options: object = {}): Promise => { return this.axiosInstance.get(url, { params, ...options }); - } + }; - async post>( + post = async >( url: string, body?: object, params: object = {}, options: object = {} - ): Promise { + ): Promise => { return this.axiosInstance.post(url, body, { params, ...options }); - } + }; - async patch>( + patch = async >( url: string, body?: object, params: object = {}, options: object = {} - ): Promise { + ): Promise => { return this.axiosInstance.patch(url, body, { params, ...options }); - } + }; - async put>( + put = async >( url: string, body?: object, params: object = {}, options: object = {} - ): Promise { + ): Promise => { return this.axiosInstance.put(url, body, { params, ...options }); - } + }; - async delete>(url: string, params: object = {}, options: object = {}): Promise { + delete = async >(url: string, params: object = {}, options: object = {}): Promise => { return this.axiosInstance.delete(url, { params, ...options }); - } + }; } diff --git a/services/http-request/src/constants/fetchRestController.ts b/services/http-request/src/constants/fetchRestController.ts index 96ce0189..1eb3ad47 100644 --- a/services/http-request/src/constants/fetchRestController.ts +++ b/services/http-request/src/constants/fetchRestController.ts @@ -8,11 +8,19 @@ export class HttpRestControllerFetch extends HttpRestController { public headers: Headers; - async get(url: string, headers?: Headers): Promise { + setHeader = (key: string, value: string | null): void => { + if (value) { + this.headers.set(key, value); + } else { + this.headers.delete(key); + } + }; + + get = async (url: string, headers?: Headers): Promise => { return fetch(url, { method: 'GET', headers: { ...this.headers, ...headers } }) as Promise; - } + }; - async post(url: string, body: object, headers?: Headers): Promise { + post = async (url: string, body: object, headers?: Headers): Promise => { return fetch(url, { method: 'POST', body: JSON.stringify(body), @@ -21,29 +29,29 @@ export class HttpRestControllerFetch extends HttpRestController { ...headers } }) as Promise; - } + }; - async put(url: string, body: object, headers?: Headers): Promise { + patch = async (url: string, body: object, headers?: Headers): Promise => { return fetch(url, { - method: 'PUT', + method: 'PATCH', body: JSON.stringify(body), headers: { ...this.headers, ...headers } }) as Promise; - } + }; - async patch(url: string, body: object, headers?: Headers): Promise { + put = async (url: string, body: object, headers?: Headers): Promise => { return fetch(url, { - method: 'PATCH', + method: 'PUT', body: JSON.stringify(body), headers: { ...this.headers, ...headers } }) as Promise; - } + }; - async delete(url: string, body: object = {}, headers?: Headers): Promise { + delete = async (url: string, body: object = {}, headers?: Headers): Promise => { return fetch(url, { method: 'DELETE', body: JSON.stringify(body), headers: { ...this.headers, ...headers } }) as Promise; - } + }; } diff --git a/services/http-request/src/httpRequest.ts b/services/http-request/src/httpRequest.ts index acffc92e..316dd292 100644 --- a/services/http-request/src/httpRequest.ts +++ b/services/http-request/src/httpRequest.ts @@ -1,5 +1,3 @@ -import { HttpRestControllerAxios } from './constants/axiosRestController'; -import { HttpRestControllerFetch } from './constants/fetchRestController'; import { IHttpRequestOptions } from './httpRequest.types'; import HttpRestController from './restController'; @@ -11,6 +9,7 @@ class HttpRequest { public patch: RestController['patch']; public put: RestController['put']; public delete: RestController['delete']; + public setHeader: RestController['setHeader']; constructor(options: IHttpRequestOptions) { if (options.restController) { @@ -22,18 +21,16 @@ class HttpRequest { this.patch = this.restController.patch; this.put = this.restController.put; this.delete = this.restController.delete; + this.setHeader = this.restController.setHeader; } else { this.get = () => Promise.reject('get handler was not specified'); this.post = () => Promise.reject('post handler was not specified'); this.patch = () => Promise.reject('patch handler was not specified'); this.put = () => Promise.reject('put handler was not specified'); this.delete = () => Promise.reject('delete handler was not specified'); + this.setHeader = () => undefined; } } } -const httpRequest = new HttpRequest({ - restController: new HttpRestControllerAxios() -}); - export default HttpRequest; diff --git a/services/http-request/src/restController.ts b/services/http-request/src/restController.ts index 86070ca6..c3d29746 100644 --- a/services/http-request/src/restController.ts +++ b/services/http-request/src/restController.ts @@ -4,6 +4,13 @@ abstract class HttpRestController { abstract patch(...args: any[]): Promise; abstract put(...args: any[]): Promise; abstract delete(...args: any[]): Promise; + public setHeader: (...args: any[]) => void; + + constructor() { + this.setHeader = () => { + console.error('setHeader for HttpRestController is undefined'); + }; + } } export default HttpRestController; From 5d11bb46696280f669f955fc4295777a5a285bd2 Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 12 May 2025 16:08:37 +0500 Subject: [PATCH 10/49] feat: fetch controller json response formatter --- .../src/constants/fetchRestController.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/services/http-request/src/constants/fetchRestController.ts b/services/http-request/src/constants/fetchRestController.ts index 1eb3ad47..ca036ac4 100644 --- a/services/http-request/src/constants/fetchRestController.ts +++ b/services/http-request/src/constants/fetchRestController.ts @@ -17,7 +17,9 @@ export class HttpRestControllerFetch extends HttpRestController { }; get = async (url: string, headers?: Headers): Promise => { - return fetch(url, { method: 'GET', headers: { ...this.headers, ...headers } }) as Promise; + return fetch(url, { method: 'GET', headers: { ...this.headers, ...headers } }).then(r => + r.json() + ) as Promise; }; post = async (url: string, body: object, headers?: Headers): Promise => { @@ -28,7 +30,7 @@ export class HttpRestControllerFetch extends HttpRestController { ...this.headers, ...headers } - }) as Promise; + }).then(r => r.json()) as Promise; }; patch = async (url: string, body: object, headers?: Headers): Promise => { @@ -36,7 +38,7 @@ export class HttpRestControllerFetch extends HttpRestController { method: 'PATCH', body: JSON.stringify(body), headers: { ...this.headers, ...headers } - }) as Promise; + }).then(r => r.json()) as Promise; }; put = async (url: string, body: object, headers?: Headers): Promise => { @@ -44,7 +46,7 @@ export class HttpRestControllerFetch extends HttpRestController { method: 'PUT', body: JSON.stringify(body), headers: { ...this.headers, ...headers } - }) as Promise; + }).then(r => r.json()) as Promise; }; delete = async (url: string, body: object = {}, headers?: Headers): Promise => { @@ -52,6 +54,6 @@ export class HttpRestControllerFetch extends HttpRestController { method: 'DELETE', body: JSON.stringify(body), headers: { ...this.headers, ...headers } - }) as Promise; + }).then(r => r.json()) as Promise; }; } From 57b93f255489b6fe401cef698fdff1ff07b2fca4 Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 12 May 2025 16:49:31 +0500 Subject: [PATCH 11/49] feat: workaround for Headers collection for fetch --- .../src/constants/fetchRestController.ts | 61 ++++++++++++------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/services/http-request/src/constants/fetchRestController.ts b/services/http-request/src/constants/fetchRestController.ts index ca036ac4..fa65eaf3 100644 --- a/services/http-request/src/constants/fetchRestController.ts +++ b/services/http-request/src/constants/fetchRestController.ts @@ -1,59 +1,74 @@ import HttpRestController from '../restController'; export class HttpRestControllerFetch extends HttpRestController { + public headers: Headers = new Headers(); + constructor() { super(); - this.headers = new Headers(); } - public headers: Headers; - setHeader = (key: string, value: string | null): void => { if (value) { - this.headers.set(key, value); + this.headers.append(key, value); } else { this.headers.delete(key); } }; - get = async (url: string, headers?: Headers): Promise => { - return fetch(url, { method: 'GET', headers: { ...this.headers, ...headers } }).then(r => - r.json() - ) as Promise; + private getCurrentHeaders() { + const currentHeaders: Record = {}; + this.headers.forEach((h, v) => { + currentHeaders[h] = v; + }); + + return currentHeaders; + } + + private mergeCurrentHeaders(headers: Headers | undefined) { + const collectedHeaders: Record = this.getCurrentHeaders(); + + if (headers) { + headers.forEach((h, v) => { + collectedHeaders[h] = v; + }); + } + + return collectedHeaders; + } + + get = async (url: string, headers?: Headers): Promise => { + return fetch(url, { method: 'GET', headers: this.mergeCurrentHeaders(headers) }) as Promise; }; - post = async (url: string, body: object, headers?: Headers): Promise => { + post = async (url: string, body: object, headers?: Headers): Promise => { return fetch(url, { method: 'POST', body: JSON.stringify(body), - headers: { - ...this.headers, - ...headers - } - }).then(r => r.json()) as Promise; + headers: this.mergeCurrentHeaders(headers) + }) as Promise; }; - patch = async (url: string, body: object, headers?: Headers): Promise => { + patch = async (url: string, body: object, headers?: Headers): Promise => { return fetch(url, { method: 'PATCH', body: JSON.stringify(body), - headers: { ...this.headers, ...headers } - }).then(r => r.json()) as Promise; + headers: this.mergeCurrentHeaders(headers) + }) as Promise; }; - put = async (url: string, body: object, headers?: Headers): Promise => { + put = async (url: string, body: object, headers?: Headers): Promise => { return fetch(url, { method: 'PUT', body: JSON.stringify(body), - headers: { ...this.headers, ...headers } - }).then(r => r.json()) as Promise; + headers: this.mergeCurrentHeaders(headers) + }) as Promise; }; - delete = async (url: string, body: object = {}, headers?: Headers): Promise => { + delete = async (url: string, body: object = {}, headers?: Headers): Promise => { return fetch(url, { method: 'DELETE', body: JSON.stringify(body), - headers: { ...this.headers, ...headers } - }).then(r => r.json()) as Promise; + headers: this.mergeCurrentHeaders(headers) + }) as Promise; }; } From 6833ccd8d2cdbc9658626224406ab9846fed95a4 Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 12 May 2025 16:53:53 +0500 Subject: [PATCH 12/49] docs: readme upd --- services/http-request/README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/services/http-request/README.md b/services/http-request/README.md index 74462226..8a0d69e3 100644 --- a/services/http-request/README.md +++ b/services/http-request/README.md @@ -20,6 +20,38 @@ There are two classes ready to be used as restControllers: **HttpRestControllerF httpRequestService.get("http://localhost:3000/api/"); ``` +### Example of usage with HttpRestControllerFetch + +```ts + const restController = new HttpRestControllerFetch(); + const HttpService = new HttpRequest({ + restController + }); + + const handleGetProducts = async (): Promise => { + const products = await httpService + .get('http://localhost:3322/products') + .then(async r => (await r.json()) as IProduct[]); + + setProducts(products); + }; +``` + +### Example of usage with HttpRestControllerAxios + +```ts + const restController = new HttpRestControllerAxios(); + const HttpService = new HttpRequest({ + restController + }); + + const handleGetProducts = async (): Promise => { + const products = await httpService.get('http://localhost:3322/products').then(r => r.data); + + setProducts(products); + }; +``` + ### You can define own HttpRestController and pass it like this ```ts From 4b951d937d7f4e73546aae441324a73b8790ff47 Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 12 May 2025 16:58:17 +0500 Subject: [PATCH 13/49] feat: request => service --- services/http-request/README.md | 24 +++++++++---------- .../src/{httpRequest.ts => httpService.ts} | 8 +++---- ...pRequest.types.ts => httpService.types.ts} | 2 +- services/http-request/src/index.ts | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) rename services/http-request/src/{httpRequest.ts => httpService.ts} (85%) rename services/http-request/src/{httpRequest.types.ts => httpService.types.ts} (61%) diff --git a/services/http-request/README.md b/services/http-request/README.md index 8a0d69e3..e7e430ea 100644 --- a/services/http-request/README.md +++ b/services/http-request/README.md @@ -1,30 +1,30 @@ -# `@byndyusoft-ui/http-request` +# `@byndyusoft-ui/http-service` -> Http request service +> Http service service ### Installation ```bash -npm i @byndyusoft-ui/http-request +npm i @byndyusoft-ui/http-service ``` ## Usage -### To start using this service you need to create a new class instance of HttpRequest and provide restController option. +### To start using this service you need to create a new class instance of HttpService and provide restController option. There are two classes ready to be used as restControllers: **HttpRestControllerFetch** and **HttpRestControllerAxios**. For fetch and axios. ```ts const restController = new HttpRestControllerFetch(); - const httpRequest = new HttpRequest({ + const httpService = new HttpService({ restController }); - httpRequestService.get("http://localhost:3000/api/"); + httpService.get("http://localhost:3000/api/"); ``` ### Example of usage with HttpRestControllerFetch ```ts const restController = new HttpRestControllerFetch(); - const HttpService = new HttpRequest({ + const HttpService = new HttpService({ restController }); @@ -41,7 +41,7 @@ There are two classes ready to be used as restControllers: **HttpRestControllerF ```ts const restController = new HttpRestControllerAxios(); - const HttpService = new HttpRequest({ + const HttpService = new HttpService({ restController }); @@ -56,7 +56,7 @@ There are two classes ready to be used as restControllers: **HttpRestControllerF ```ts // myOwnHttpRestController.ts - import { HttpRestController } from '@byndyusoft-ui/http-request'; + import { HttpRestController } from '@byndyusoft-ui/http-service'; class HttpRestControllerCustom extends HttpRestController { constructor() { @@ -104,14 +104,14 @@ There are two classes ready to be used as restControllers: **HttpRestControllerF ``` ```ts - // httpRequest.ts + // httpService.ts import HttpRestControllerCustom from './myOwnHttpRestController.ts' const restController = new HttpRestControllerCustom(); - const httpRequestService = new HttpRequest({ + const httpService = new HttpService({ restController }); - httpRequestService.get("http://localhost:3000/api/"); + httpService.get("http://localhost:3000/api/"); ``` diff --git a/services/http-request/src/httpRequest.ts b/services/http-request/src/httpService.ts similarity index 85% rename from services/http-request/src/httpRequest.ts rename to services/http-request/src/httpService.ts index 316dd292..a68085f4 100644 --- a/services/http-request/src/httpRequest.ts +++ b/services/http-request/src/httpService.ts @@ -1,7 +1,7 @@ -import { IHttpRequestOptions } from './httpRequest.types'; +import { IHttpServiceOptions } from './httpService.types'; import HttpRestController from './restController'; -class HttpRequest { +class HttpService { public restController: RestController | undefined; public get: RestController['get']; @@ -11,7 +11,7 @@ class HttpRequest { public delete: RestController['delete']; public setHeader: RestController['setHeader']; - constructor(options: IHttpRequestOptions) { + constructor(options: IHttpServiceOptions) { if (options.restController) { const restController = options.restController; this.restController = restController; @@ -33,4 +33,4 @@ class HttpRequest { } } -export default HttpRequest; +export default HttpService; diff --git a/services/http-request/src/httpRequest.types.ts b/services/http-request/src/httpService.types.ts similarity index 61% rename from services/http-request/src/httpRequest.types.ts rename to services/http-request/src/httpService.types.ts index c0a66241..0173ddde 100644 --- a/services/http-request/src/httpRequest.types.ts +++ b/services/http-request/src/httpService.types.ts @@ -1,5 +1,5 @@ import HttpRestController from './restController'; -export interface IHttpRequestOptions { +export interface IHttpServiceOptions { restController?: RestController; } diff --git a/services/http-request/src/index.ts b/services/http-request/src/index.ts index a473fba8..5589ab56 100644 --- a/services/http-request/src/index.ts +++ b/services/http-request/src/index.ts @@ -1,6 +1,6 @@ -import HttpRequest from './httpRequest'; +import HttpService from './httpService'; import HttpRestController from './restController'; import { HttpRestControllerAxios } from './constants/axiosRestController'; import { HttpRestControllerFetch } from './constants/fetchRestController'; -export { HttpRequest, HttpRestController, HttpRestControllerAxios, HttpRestControllerFetch }; +export { HttpService, HttpRestController, HttpRestControllerAxios, HttpRestControllerFetch }; From 3654e844a66af9d11e472c9230bd76ba41682c7d Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 12 May 2025 16:59:10 +0500 Subject: [PATCH 14/49] feat: request => service --- services/http-request/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/http-request/package.json b/services/http-request/package.json index 9e821375..183ee5f7 100644 --- a/services/http-request/package.json +++ b/services/http-request/package.json @@ -1,5 +1,5 @@ { - "name": "@byndyusoft-ui/http-request", + "name": "@byndyusoft-ui/http-service", "version": "0.0.1", "description": "Byndyusoft UI HTTP Service", "keywords": [ @@ -11,7 +11,7 @@ "fetch" ], "author": "Byndyusoft Frontend Developer ", - "homepage": "https://github.com/Byndyusoft/ui/tree/master/services/http-request#readme", + "homepage": "https://github.com/Byndyusoft/ui/tree/master/services/http-service#readme", "license": "Apache-2.0", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -22,7 +22,7 @@ "scripts": { "build": "rollup --config", "clean": "rimraf dist", - "test": "jest --config ../../jest.config.js --roots services/http-request/src", + "test": "jest --config ../../jest.config.js --roots services/http-service/src", "lint:check": "npm run eslint:check && npm run prettier:check && npm run stylelint:check", "lint:fix": "npm run eslint:fix && npm run prettier:fix && npm run stylelint:fix", "eslint:check": "eslint src --config ../../eslint.config.js", From e2928135b8a22c35faade5cac36b09179126a2f8 Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 12 May 2025 17:00:41 +0500 Subject: [PATCH 15/49] feat: request => service --- services/http-request/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/http-request/README.md b/services/http-request/README.md index e7e430ea..1341deca 100644 --- a/services/http-request/README.md +++ b/services/http-request/README.md @@ -1,6 +1,6 @@ # `@byndyusoft-ui/http-service` -> Http service service +> Http service ### Installation ```bash From 2f791a39e70a5ed4a8d98a5aa014090fe6605928 Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 12 May 2025 17:01:29 +0500 Subject: [PATCH 16/49] docs: upd --- services/http-request/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/http-request/README.md b/services/http-request/README.md index 1341deca..705cf52e 100644 --- a/services/http-request/README.md +++ b/services/http-request/README.md @@ -14,8 +14,8 @@ There are two classes ready to be used as restControllers: **HttpRestControllerF ```ts const restController = new HttpRestControllerFetch(); const httpService = new HttpService({ - restController -}); + restController + }); httpService.get("http://localhost:3000/api/"); ``` From 21d70a480cfce0ff07bdfb582816f6b9bd08ff10 Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 12 May 2025 17:25:25 +0500 Subject: [PATCH 17/49] feat: folder rename --- services/{http-request => http-service}/.npmignore | 0 services/{http-request => http-service}/README.md | 0 services/{http-request => http-service}/package.json | 0 services/{http-request => http-service}/rollup.config.js | 0 .../src/constants/axiosRestController.ts | 0 .../src/constants/fetchRestController.ts | 0 services/{http-request => http-service}/src/httpService.ts | 0 services/{http-request => http-service}/src/httpService.types.ts | 0 services/{http-request => http-service}/src/index.ts | 0 services/{http-request => http-service}/src/restController.ts | 0 services/{http-request => http-service}/tsconfig.json | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename services/{http-request => http-service}/.npmignore (100%) rename services/{http-request => http-service}/README.md (100%) rename services/{http-request => http-service}/package.json (100%) rename services/{http-request => http-service}/rollup.config.js (100%) rename services/{http-request => http-service}/src/constants/axiosRestController.ts (100%) rename services/{http-request => http-service}/src/constants/fetchRestController.ts (100%) rename services/{http-request => http-service}/src/httpService.ts (100%) rename services/{http-request => http-service}/src/httpService.types.ts (100%) rename services/{http-request => http-service}/src/index.ts (100%) rename services/{http-request => http-service}/src/restController.ts (100%) rename services/{http-request => http-service}/tsconfig.json (100%) diff --git a/services/http-request/.npmignore b/services/http-service/.npmignore similarity index 100% rename from services/http-request/.npmignore rename to services/http-service/.npmignore diff --git a/services/http-request/README.md b/services/http-service/README.md similarity index 100% rename from services/http-request/README.md rename to services/http-service/README.md diff --git a/services/http-request/package.json b/services/http-service/package.json similarity index 100% rename from services/http-request/package.json rename to services/http-service/package.json diff --git a/services/http-request/rollup.config.js b/services/http-service/rollup.config.js similarity index 100% rename from services/http-request/rollup.config.js rename to services/http-service/rollup.config.js diff --git a/services/http-request/src/constants/axiosRestController.ts b/services/http-service/src/constants/axiosRestController.ts similarity index 100% rename from services/http-request/src/constants/axiosRestController.ts rename to services/http-service/src/constants/axiosRestController.ts diff --git a/services/http-request/src/constants/fetchRestController.ts b/services/http-service/src/constants/fetchRestController.ts similarity index 100% rename from services/http-request/src/constants/fetchRestController.ts rename to services/http-service/src/constants/fetchRestController.ts diff --git a/services/http-request/src/httpService.ts b/services/http-service/src/httpService.ts similarity index 100% rename from services/http-request/src/httpService.ts rename to services/http-service/src/httpService.ts diff --git a/services/http-request/src/httpService.types.ts b/services/http-service/src/httpService.types.ts similarity index 100% rename from services/http-request/src/httpService.types.ts rename to services/http-service/src/httpService.types.ts diff --git a/services/http-request/src/index.ts b/services/http-service/src/index.ts similarity index 100% rename from services/http-request/src/index.ts rename to services/http-service/src/index.ts diff --git a/services/http-request/src/restController.ts b/services/http-service/src/restController.ts similarity index 100% rename from services/http-request/src/restController.ts rename to services/http-service/src/restController.ts diff --git a/services/http-request/tsconfig.json b/services/http-service/tsconfig.json similarity index 100% rename from services/http-request/tsconfig.json rename to services/http-service/tsconfig.json From f29ecafcbf5e21e1869ef0647c9ebe68eb437996 Mon Sep 17 00:00:00 2001 From: ancientbag Date: Mon, 12 May 2025 17:35:24 +0500 Subject: [PATCH 18/49] docs: readme upd --- services/http-service/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/http-service/README.md b/services/http-service/README.md index 705cf52e..2dad8a94 100644 --- a/services/http-service/README.md +++ b/services/http-service/README.md @@ -24,7 +24,7 @@ There are two classes ready to be used as restControllers: **HttpRestControllerF ```ts const restController = new HttpRestControllerFetch(); - const HttpService = new HttpService({ + const httpService = new HttpService({ restController }); @@ -41,7 +41,7 @@ There are two classes ready to be used as restControllers: **HttpRestControllerF ```ts const restController = new HttpRestControllerAxios(); - const HttpService = new HttpService({ + const httpService = new HttpService({ restController }); From 1ab1ed4f9a4d78e874414848727d2db4c5cbcb38 Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Tue, 20 May 2025 19:02:59 +0300 Subject: [PATCH 19/49] feat(http-service): add token refresh --- .../src/constants/axiosRestController.ts | 118 +++++++++++++++--- services/http-service/src/httpService.ts | 6 +- services/http-service/src/httpStatus.ts | 50 ++++++++ services/http-service/src/restController.ts | 7 +- services/http-service/src/token.types.ts | 22 ++++ 5 files changed, 183 insertions(+), 20 deletions(-) create mode 100644 services/http-service/src/httpStatus.ts create mode 100644 services/http-service/src/token.types.ts diff --git a/services/http-service/src/constants/axiosRestController.ts b/services/http-service/src/constants/axiosRestController.ts index dcf2e659..27ad6fac 100644 --- a/services/http-service/src/constants/axiosRestController.ts +++ b/services/http-service/src/constants/axiosRestController.ts @@ -1,5 +1,7 @@ -import axios, { AxiosInstance, AxiosResponse } from 'axios'; +import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; import HttpRestController from '../restController'; +import { ITokenData, ITokenPayload } from '../token.types'; +import { httpStatus } from '../httpStatus'; const DEFAULT_REQUEST_TIMEOUT = 60000; @@ -8,6 +10,7 @@ export class HttpRestControllerAxios extends HttpRestController { constructor() { super(); + this.axiosInstance = axios.create({ headers: { 'X-Requested-With': 'XMLHttpRequest', @@ -17,59 +20,142 @@ export class HttpRestControllerAxios extends HttpRestController { }, timeout: DEFAULT_REQUEST_TIMEOUT }); + + this.setupInterceptors(); } + private tokenData: ITokenData | null = null; + + private refreshTokenRequest: Promise | null = null; + + private isTokenExpired(): boolean { + const MILLISECONDS_PER_SECOND = 1000; + const expireTime = this.tokenData == null ? null : this.tokenData.expireTime(); + + if (!expireTime) return false; + + return Math.round(new Date().getTime() / MILLISECONDS_PER_SECOND) > expireTime; + } + + private async refreshTokens(): Promise { + if (this.refreshTokenRequest) { + return this.refreshTokenRequest; + } + + if (!this.tokenData?.refreshToken) { + throw new Error('No refresh token available'); + } + + try { + this.refreshTokenRequest = this + .post(this.tokenData.tokenRefreshUrl, { refreshToken: this.tokenData.refreshToken }) + .then(response => { + this.tokenData?.setNewTokenPayload(response.data); + + return response.data.accessToken; + }); + + return this.refreshTokenRequest; + } catch (error) { + throw new Error(`Token refresh failed: ${error}`); + } finally { + this.refreshTokenRequest = null; + } + } + + private setupInterceptors(): void { + this.axiosInstance.interceptors.request.use(async request => { + const accessToken = this.tokenData?.accessToken(); + const isExpired = this.isTokenExpired(); + const isNotRefreshUrl = request.url !== this.tokenData?.tokenRefreshUrl; + + if (accessToken && !isExpired) { + this.setHeader('Authorization', `Bearer ${accessToken}`); + } + + if (isExpired && isNotRefreshUrl) { + const newAccessToken = await this.refreshTokens(); + this.setHeader('Authorization', `Bearer ${newAccessToken}`); + } + + return request; + }); + + this.axiosInstance.interceptors.response.use( + response => response, + async (error: AxiosError) => { + const requestConfig = error.config; + + if (error.response?.status === (httpStatus.UNAUTHORIZED as number)) { + try { + const newAccessToken = await this.refreshTokens(); + + requestConfig?.headers.set('Authorization', `Bearer ${newAccessToken}`); + + return this.axiosInstance(requestConfig ?? {}); + } catch (refreshError) { + console.error('Session expired. Redirect to login...'); + this.tokenData?.clearTokens(); + window.location.href = '/login'; + + return Promise.reject(refreshError); + } + } + + throw error; + } + ); + } + + setTokenData(tokenData: ITokenData): void { + this.tokenData = tokenData; + }; + setHeader = (key: string, value: string | null): void => { this.axiosInstance.defaults.headers[key] = value; }; - get = async >(url: string, params: object = {}, options: object = {}): Promise => { - return this.axiosInstance.get(url, { + get = async >(url: string, params: object = {}, options: object = {}): Promise => + this.axiosInstance.get(url, { params, ...options }); - }; post = async >( url: string, body?: object, params: object = {}, options: object = {} - ): Promise => { - return this.axiosInstance.post(url, body, { + ): Promise => this.axiosInstance.post(url, body, { params, ...options }); - }; patch = async >( url: string, body?: object, params: object = {}, options: object = {} - ): Promise => { - return this.axiosInstance.patch(url, body, { + ): Promise => + this.axiosInstance.patch(url, body, { params, ...options }); - }; put = async >( url: string, body?: object, params: object = {}, options: object = {} - ): Promise => { - return this.axiosInstance.put(url, body, { + ): Promise => + this.axiosInstance.put(url, body, { params, ...options }); - }; - delete = async >(url: string, params: object = {}, options: object = {}): Promise => { - return this.axiosInstance.delete(url, { + delete = async >(url: string, params: object = {}, options: object = {}): Promise => + this.axiosInstance.delete(url, { params, ...options }); - }; } diff --git a/services/http-service/src/httpService.ts b/services/http-service/src/httpService.ts index a68085f4..a7afb909 100644 --- a/services/http-service/src/httpService.ts +++ b/services/http-service/src/httpService.ts @@ -10,11 +10,11 @@ class HttpService { public put: RestController['put']; public delete: RestController['delete']; public setHeader: RestController['setHeader']; + public setTokenData: RestController['setTokenData']; constructor(options: IHttpServiceOptions) { if (options.restController) { - const restController = options.restController; - this.restController = restController; + this.restController = options.restController; this.get = this.restController.get; this.post = this.restController.post; @@ -22,6 +22,7 @@ class HttpService { this.put = this.restController.put; this.delete = this.restController.delete; this.setHeader = this.restController.setHeader; + this.setTokenData = this.restController.setTokenData; } else { this.get = () => Promise.reject('get handler was not specified'); this.post = () => Promise.reject('post handler was not specified'); @@ -29,6 +30,7 @@ class HttpService { this.put = () => Promise.reject('put handler was not specified'); this.delete = () => Promise.reject('delete handler was not specified'); this.setHeader = () => undefined; + this.setTokenData = () => undefined; } } } diff --git a/services/http-service/src/httpStatus.ts b/services/http-service/src/httpStatus.ts new file mode 100644 index 00000000..8d07018e --- /dev/null +++ b/services/http-service/src/httpStatus.ts @@ -0,0 +1,50 @@ +export declare enum httpStatus { + CONTINUE = 100, + SWITCHING_PROTOCOLS = 101, + PROCESSING = 102, + EARLYHINTS = 103, + OK = 200, + CREATED = 201, + ACCEPTED = 202, + NON_AUTHORITATIVE_INFORMATION = 203, + NO_CONTENT = 204, + RESET_CONTENT = 205, + PARTIAL_CONTENT = 206, + AMBIGUOUS = 300, + MOVED_PERMANENTLY = 301, + FOUND = 302, + SEE_OTHER = 303, + NOT_MODIFIED = 304, + TEMPORARY_REDIRECT = 307, + PERMANENT_REDIRECT = 308, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + PAYMENT_REQUIRED = 402, + FORBIDDEN = 403, + NOT_FOUND = 404, + METHOD_NOT_ALLOWED = 405, + NOT_ACCEPTABLE = 406, + PROXY_AUTHENTICATION_REQUIRED = 407, + REQUEST_TIMEOUT = 408, + CONFLICT = 409, + GONE = 410, + LENGTH_REQUIRED = 411, + PRECONDITION_FAILED = 412, + PAYLOAD_TOO_LARGE = 413, + URI_TOO_LONG = 414, + UNSUPPORTED_MEDIA_TYPE = 415, + REQUESTED_RANGE_NOT_SATISFIABLE = 416, + EXPECTATION_FAILED = 417, + I_AM_A_TEAPOT = 418, + MISDIRECTED = 421, + UNPROCESSABLE_ENTITY = 422, + FAILED_DEPENDENCY = 424, + PRECONDITION_REQUIRED = 428, + TOO_MANY_REQUESTS = 429, + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, + BAD_GATEWAY = 502, + SERVICE_UNAVAILABLE = 503, + GATEWAY_TIMEOUT = 504, + HTTP_VERSION_NOT_SUPPORTED = 505 +} diff --git a/services/http-service/src/restController.ts b/services/http-service/src/restController.ts index c3d29746..e944d6af 100644 --- a/services/http-service/src/restController.ts +++ b/services/http-service/src/restController.ts @@ -1,12 +1,15 @@ +import { ITokenData } from './token.types'; + abstract class HttpRestController { abstract get(...args: any[]): Promise; abstract post(...args: any[]): Promise; abstract patch(...args: any[]): Promise; abstract put(...args: any[]): Promise; abstract delete(...args: any[]): Promise; - public setHeader: (...args: any[]) => void; + abstract setTokenData(arg: ITokenData): void; + setHeader: (...args: any[]) => void; - constructor() { + protected constructor() { this.setHeader = () => { console.error('setHeader for HttpRestController is undefined'); }; diff --git a/services/http-service/src/token.types.ts b/services/http-service/src/token.types.ts new file mode 100644 index 00000000..ee4e09c8 --- /dev/null +++ b/services/http-service/src/token.types.ts @@ -0,0 +1,22 @@ +export type TValueOrGetter = T extends Function ? never : T | (() => T); +export type TGetter = () => T; + +export interface ITokenPayload { + accessToken: string; + refreshToken: string; + expiresIn: number; +} + +export type TAccessTokenGetter = TGetter; +export type TRefreshTokenGetter = TGetter; +export type TTokenExpireTimeGetter = TGetter; +export type TTokenRefreshUrlGetter = string; + +export interface ITokenData { + accessToken: TAccessTokenGetter; + refreshToken: TRefreshTokenGetter; + expireTime: TTokenExpireTimeGetter; + tokenRefreshUrl: TTokenRefreshUrlGetter; + setNewTokenPayload: (arg: ITokenPayload) => void; + clearTokens: () => void; +} From b63bc53c445b0d8ec8fc68e4b7b4b15b75847b49 Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Wed, 21 May 2025 13:12:44 +0300 Subject: [PATCH 20/49] feat(http-service): update token refresh --- package-lock.json | 13 +++++++-- .../src/constants/axiosRestController.ts | 28 +++++++++++++------ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0fb55802..258c6fd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -707,8 +707,8 @@ "resolved": "components/highlighter", "link": true }, - "node_modules/@byndyusoft-ui/http-request": { - "resolved": "services/http-request", + "node_modules/@byndyusoft-ui/http-service": { + "resolved": "services/http-service", "link": true }, "node_modules/@byndyusoft-ui/keyframes-css": { @@ -11175,6 +11175,15 @@ "services/http-request": { "name": "@byndyusoft-ui/http-request", "version": "0.0.1", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.9.0" + } + }, + "services/http-service": { + "name": "@byndyusoft-ui/http-service", + "version": "0.0.1", "license": "Apache-2.0", "dependencies": { "axios": "^1.9.0" diff --git a/services/http-service/src/constants/axiosRestController.ts b/services/http-service/src/constants/axiosRestController.ts index 27ad6fac..624b8178 100644 --- a/services/http-service/src/constants/axiosRestController.ts +++ b/services/http-service/src/constants/axiosRestController.ts @@ -53,10 +53,13 @@ export class HttpRestControllerAxios extends HttpRestController { this.tokenData?.setNewTokenPayload(response.data); return response.data.accessToken; - }); + }) return this.refreshTokenRequest; } catch (error) { + this.tokenData?.clearTokens(); + window.location.href = '/login'; + throw new Error(`Token refresh failed: ${error}`); } finally { this.refreshTokenRequest = null; @@ -70,12 +73,18 @@ export class HttpRestControllerAxios extends HttpRestController { const isNotRefreshUrl = request.url !== this.tokenData?.tokenRefreshUrl; if (accessToken && !isExpired) { - this.setHeader('Authorization', `Bearer ${accessToken}`); + request.headers['Authorization'] = `Bearer ${accessToken}`; } if (isExpired && isNotRefreshUrl) { - const newAccessToken = await this.refreshTokens(); - this.setHeader('Authorization', `Bearer ${newAccessToken}`); + try { + const newAccessToken = await this.refreshTokens(); + request.headers['Authorization'] = `Bearer ${newAccessToken}`; + } catch (refreshError) { + console.error('Session expired'); + + return Promise.reject(refreshError); + } } return request; @@ -85,18 +94,19 @@ export class HttpRestControllerAxios extends HttpRestController { response => response, async (error: AxiosError) => { const requestConfig = error.config; + const isNotRefreshUrl = error.config?.url !== this.tokenData?.tokenRefreshUrl; - if (error.response?.status === (httpStatus.UNAUTHORIZED as number)) { + if (error.response?.status === (httpStatus.UNAUTHORIZED as number) && isNotRefreshUrl) { try { const newAccessToken = await this.refreshTokens(); - requestConfig?.headers.set('Authorization', `Bearer ${newAccessToken}`); + if (requestConfig) { + requestConfig.headers['Authorization'] = `Bearer ${newAccessToken}`; + } return this.axiosInstance(requestConfig ?? {}); } catch (refreshError) { - console.error('Session expired. Redirect to login...'); - this.tokenData?.clearTokens(); - window.location.href = '/login'; + console.error('Session expired'); return Promise.reject(refreshError); } From 9fc569b5780f60f227512686e16b40b2362394de Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Wed, 21 May 2025 13:29:48 +0300 Subject: [PATCH 21/49] feat(http-service): remove arrow functions --- .../src/constants/axiosRestController.ts | 45 ++++++++----------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/services/http-service/src/constants/axiosRestController.ts b/services/http-service/src/constants/axiosRestController.ts index 624b8178..ba813cd3 100644 --- a/services/http-service/src/constants/axiosRestController.ts +++ b/services/http-service/src/constants/axiosRestController.ts @@ -125,47 +125,38 @@ export class HttpRestControllerAxios extends HttpRestController { this.axiosInstance.defaults.headers[key] = value; }; - get = async >(url: string, params: object = {}, options: object = {}): Promise => - this.axiosInstance.get(url, { - params, - ...options - }); + async get>(url: string, params: object = {}, options: object = {}): Promise { + return this.axiosInstance.get(url, { params, ...options }); + }; - post = async >( + async post>( url: string, body?: object, params: object = {}, options: object = {} - ): Promise => this.axiosInstance.post(url, body, { - params, - ...options - }); + ): Promise { + return this.axiosInstance.post(url, body, { params, ...options }); + }; - patch = async >( + async patch>( url: string, body?: object, params: object = {}, options: object = {} - ): Promise => - this.axiosInstance.patch(url, body, { - params, - ...options - }); + ): Promise { + return this.axiosInstance.patch(url, body, { params, ...options }); + }; - put = async >( + async put>( url: string, body?: object, params: object = {}, options: object = {} - ): Promise => - this.axiosInstance.put(url, body, { - params, - ...options - }); + ): Promise { + return this.axiosInstance.put(url, body, { params, ...options }); + } - delete = async >(url: string, params: object = {}, options: object = {}): Promise => - this.axiosInstance.delete(url, { - params, - ...options - }); + async delete>(url: string, params: object = {}, options: object = {}): Promise { + return this.axiosInstance.delete(url, { params, ...options }); + }; } From 47abc5ac6fd0d65ad4058c83b9ce73d1a4349c4e Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Mon, 26 May 2025 21:27:59 +0300 Subject: [PATCH 22/49] feat(http-client): client and request interfaces, attemps not result --- .../src/constants/axiosRestController.ts | 4 +- .../src/constants/fetchRestController.ts | 4 + services/http-service/src/httpClient.types.ts | 139 ++++++++++++++++++ services/http-service/src/httpMethod.types.ts | 7 + services/http-service/src/httpStatus.ts | 6 +- 5 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 services/http-service/src/httpClient.types.ts create mode 100644 services/http-service/src/httpMethod.types.ts diff --git a/services/http-service/src/constants/axiosRestController.ts b/services/http-service/src/constants/axiosRestController.ts index ba813cd3..935c52bf 100644 --- a/services/http-service/src/constants/axiosRestController.ts +++ b/services/http-service/src/constants/axiosRestController.ts @@ -1,7 +1,7 @@ import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; import HttpRestController from '../restController'; import { ITokenData, ITokenPayload } from '../token.types'; -import { httpStatus } from '../httpStatus'; +import { HttpStatus } from '../httpStatus'; const DEFAULT_REQUEST_TIMEOUT = 60000; @@ -96,7 +96,7 @@ export class HttpRestControllerAxios extends HttpRestController { const requestConfig = error.config; const isNotRefreshUrl = error.config?.url !== this.tokenData?.tokenRefreshUrl; - if (error.response?.status === (httpStatus.UNAUTHORIZED as number) && isNotRefreshUrl) { + if (error.response?.status === (HttpStatus.UNAUTHORIZED as number) && isNotRefreshUrl) { try { const newAccessToken = await this.refreshTokens(); diff --git a/services/http-service/src/constants/fetchRestController.ts b/services/http-service/src/constants/fetchRestController.ts index fa65eaf3..ebdb45b1 100644 --- a/services/http-service/src/constants/fetchRestController.ts +++ b/services/http-service/src/constants/fetchRestController.ts @@ -1,4 +1,6 @@ import HttpRestController from '../restController'; +import { Headers, Response } from 'happy-dom'; +import { ITokenData } from '../token.types'; export class HttpRestControllerFetch extends HttpRestController { public headers: Headers = new Headers(); @@ -71,4 +73,6 @@ export class HttpRestControllerFetch extends HttpRestController { headers: this.mergeCurrentHeaders(headers) }) as Promise; }; + + setTokenData(arg: ITokenData): void {} } diff --git a/services/http-service/src/httpClient.types.ts b/services/http-service/src/httpClient.types.ts new file mode 100644 index 00000000..51af1c71 --- /dev/null +++ b/services/http-service/src/httpClient.types.ts @@ -0,0 +1,139 @@ +import { HttpMethod } from './httpMethod.types'; +import { Headers } from 'happy-dom'; +import IHeadersInit from 'happy-dom/lib/fetch/types/IHeadersInit'; + +export type IHeaders = Record; + +export type TQueryParamValue = string | number | boolean; + +export type TQueryParams = Record; + +export const DEFAULT_REQUEST_TIMEOUT = 60000; + +export interface IRequestClientOptions { + url: string; + method: HttpMethod; + headers: IHeaders; + queryParams: TQueryParams; + body?: Object; +} + +export type TRequestClient = (arg: IRequestClientOptions) => void; //Promise; + +class HttpRequest { + protected requestClient: TRequestClient; + protected urlValue: string = ''; + protected method: HttpMethod; + protected headersValue: IHeaders = {}; + protected queryParamsValue: TQueryParams = {}; + + constructor(requestClient: TRequestClient, method: HttpMethod) { + this.requestClient = requestClient; + this.method = method; + } + + url(url: string): this { + this.urlValue = url; + + return this; + } + + headers(headers: IHeaders): this { + Object.assign(this.headersValue, headers); + + return this; + } + + queryParams(queryParams: TQueryParams): this { + Object.assign(this.queryParamsValue, queryParams); + + return this; + } + + send(): void /*Promise*/ { + return this.requestClient({ + url: this.urlValue, + method: this.method, + headers: this.headersValue, + queryParams: this.queryParamsValue, + }); + } +} + +class HttpRequestWithBody extends HttpRequest { + private bodyValue?: Object = {}; + + body(body: Object): this { + this.bodyValue = body; + + return this; + } + + send(): void/*Promise*/ { + return super.requestClient({ + url: this.urlValue, + method: super.method, + headers: super.headersValue, + queryParams: super.queryParamsValue, + body: this.bodyValue + }) + } +} + +interface IHttpClientInit { + headers?: IHeadersInit; +} + +export interface IHttpClient { + headers: Headers; + requestClient(arg: IRequestClientOptions): void; // Promise; + addHeader(key: string, value: string): HttpClient; + get(): HttpRequest; + post(): HttpRequestWithBody; + put(): HttpRequestWithBody; + patch(): HttpRequestWithBody; + delete(): HttpRequest; +} + +export class HttpClient implements IHttpClient { + headers; + requestClient; + + constructor({ headers }: IHttpClientInit) { + this.headers = new Headers(headers); + this.requestClient = function(arg: IRequestClientOptions): void { + // fetch implementation + // encodeURI(this.baseUrl.toString() + this.path), + }; + } + + addHeader(key: string, value: string): HttpClient { + this.headers.append(key, value); + + return this; + }; + + get() { + return new HttpRequest(this.requestClient, HttpMethod.GET); + } + + post() { + return new HttpRequestWithBody(this.requestClient, HttpMethod.POST); + } + + put() { + return new HttpRequestWithBody(this.requestClient, HttpMethod.PUT); + } + + patch() { + return new HttpRequestWithBody(this.requestClient, HttpMethod.PATCH); + } + + delete() { + return new HttpRequest(this.requestClient, HttpMethod.DELETE); + } +} + +const test = new HttpClient({}); + +const getTest = test.post<{test: 'hello'}>().headers({ 'Content-Type': 'application/json' }).body(({ hello: 'test' })); diff --git a/services/http-service/src/httpMethod.types.ts b/services/http-service/src/httpMethod.types.ts new file mode 100644 index 00000000..91f60eab --- /dev/null +++ b/services/http-service/src/httpMethod.types.ts @@ -0,0 +1,7 @@ +export enum HttpMethod { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + PATCH = 'PATCH', + DELETE = 'DELETE' +} diff --git a/services/http-service/src/httpStatus.ts b/services/http-service/src/httpStatus.ts index 8d07018e..f6233c44 100644 --- a/services/http-service/src/httpStatus.ts +++ b/services/http-service/src/httpStatus.ts @@ -1,8 +1,4 @@ -export declare enum httpStatus { - CONTINUE = 100, - SWITCHING_PROTOCOLS = 101, - PROCESSING = 102, - EARLYHINTS = 103, +export declare enum HttpStatus { OK = 200, CREATED = 201, ACCEPTED = 202, From 2f9cdce14b73e3447f69a19c8a8896690a8724fd Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Tue, 27 May 2025 19:02:18 +0300 Subject: [PATCH 23/49] feat(http-client): client with axios, client with fetch --- services/http-service/src/axiosHttpClient.ts | 45 ++++++ .../src/{ => constants}/httpService.ts | 4 +- services/http-service/src/fetchHttpClient.ts | 58 +++++++ services/http-service/src/httpClient.types.ts | 144 ++---------------- services/http-service/src/httpRequest.ts | 77 ++++++++++ services/http-service/src/index.ts | 8 +- 6 files changed, 197 insertions(+), 139 deletions(-) create mode 100644 services/http-service/src/axiosHttpClient.ts rename services/http-service/src/{ => constants}/httpService.ts (93%) create mode 100644 services/http-service/src/fetchHttpClient.ts create mode 100644 services/http-service/src/httpRequest.ts diff --git a/services/http-service/src/axiosHttpClient.ts b/services/http-service/src/axiosHttpClient.ts new file mode 100644 index 00000000..426e9ff5 --- /dev/null +++ b/services/http-service/src/axiosHttpClient.ts @@ -0,0 +1,45 @@ +import axios, { AxiosInstance } from 'axios'; +import { IHttpClient, IHttpClientInit, DEFAULT_REQUEST_TIMEOUT } from './httpClient.types'; +import { HttpRequest, HttpRequestWithBody, IRequestClientOptions } from './httpRequest'; +import { HttpMethod } from './httpMethod.types'; + +export class AxiosHttpClient implements IHttpClient { + requestClient; + private axiosInstance: AxiosInstance; + + constructor({ baseURL, headers, timeout }: IHttpClientInit) { + this.axiosInstance = axios.create({ + baseURL, + headers, + timeout: timeout ?? DEFAULT_REQUEST_TIMEOUT + }); + + this.requestClient = function(arg: IRequestClientOptions): Promise { + return this.axiosInstance({ url: arg.url, method: arg.method, headers: arg.headers, params: arg.params, data: arg.body }); + }; + } + + setHeader(key: string, value: string): void { + this.axiosInstance.defaults.headers[key] = value; + }; + + get() { + return new HttpRequest(this.requestClient, HttpMethod.GET); + } + + post() { + return new HttpRequestWithBody(this.requestClient, HttpMethod.POST); + } + + put() { + return new HttpRequestWithBody(this.requestClient, HttpMethod.PUT); + } + + patch() { + return new HttpRequestWithBody(this.requestClient, HttpMethod.PATCH); + } + + delete() { + return new HttpRequest(this.requestClient, HttpMethod.DELETE); + } +} \ No newline at end of file diff --git a/services/http-service/src/httpService.ts b/services/http-service/src/constants/httpService.ts similarity index 93% rename from services/http-service/src/httpService.ts rename to services/http-service/src/constants/httpService.ts index a7afb909..c1ca5b40 100644 --- a/services/http-service/src/httpService.ts +++ b/services/http-service/src/constants/httpService.ts @@ -1,5 +1,5 @@ -import { IHttpServiceOptions } from './httpService.types'; -import HttpRestController from './restController'; +import { IHttpServiceOptions } from '../httpService.types'; +import HttpRestController from '../restController'; class HttpService { public restController: RestController | undefined; diff --git a/services/http-service/src/fetchHttpClient.ts b/services/http-service/src/fetchHttpClient.ts new file mode 100644 index 00000000..edb63625 --- /dev/null +++ b/services/http-service/src/fetchHttpClient.ts @@ -0,0 +1,58 @@ +import { HttpRequest, HttpRequestWithBody, IHeaders, IRequestClientOptions, TQueryParams } from './httpRequest'; +import { HttpMethod } from './httpMethod.types'; +import { DEFAULT_REQUEST_TIMEOUT, IHttpClient, IHttpClientInit } from './httpClient.types'; + +export class FetchHttpClient implements IHttpClient { + baseURL: string; + headers: IHeaders = {}; + timeout: number; + requestClient; + + constructor({ baseURL, headers, timeout }: IHttpClientInit) { + this.baseURL = baseURL; + this.headers = Object.assign(this.headers, headers); + this.timeout = timeout ?? DEFAULT_REQUEST_TIMEOUT; + this.requestClient = function(arg: IRequestClientOptions): Promise { + const url = encodeURI(`${this.baseURL}${arg.url}`) + this.buildQueryString(arg.params); + const headers = Object.assign(this.headers, arg.headers); + + return fetch(url, { method: arg.method, headers, body: JSON.stringify(arg.body) }) as Promise; + }; + } + + buildQueryString(queryParams: TQueryParams) { + const params = Object.keys(queryParams); + if (params.length > 0) { + return `?${ + params + .map(param => `${encodeURIComponent(param)}=${encodeURIComponent(queryParams[param])}`) + .join('&') + }`; + } + return ''; + } + + setHeader(key: string, value: string): void { + this.headers[key] = value; + } + + get() { + return new HttpRequest(this.requestClient, HttpMethod.GET); + } + + post() { + return new HttpRequestWithBody(this.requestClient, HttpMethod.POST); + } + + put() { + return new HttpRequestWithBody(this.requestClient, HttpMethod.PUT); + } + + patch() { + return new HttpRequestWithBody(this.requestClient, HttpMethod.PATCH); + } + + delete() { + return new HttpRequest(this.requestClient, HttpMethod.DELETE); + } +} diff --git a/services/http-service/src/httpClient.types.ts b/services/http-service/src/httpClient.types.ts index 51af1c71..986c92c4 100644 --- a/services/http-service/src/httpClient.types.ts +++ b/services/http-service/src/httpClient.types.ts @@ -1,139 +1,21 @@ -import { HttpMethod } from './httpMethod.types'; -import { Headers } from 'happy-dom'; -import IHeadersInit from 'happy-dom/lib/fetch/types/IHeadersInit'; - -export type IHeaders = Record; - -export type TQueryParamValue = string | number | boolean; - -export type TQueryParams = Record; +import { HttpRequest, HttpRequestWithBody, IRequestClientOptions, IHeaders } from './httpRequest'; export const DEFAULT_REQUEST_TIMEOUT = 60000; -export interface IRequestClientOptions { - url: string; - method: HttpMethod; - headers: IHeaders; - queryParams: TQueryParams; - body?: Object; -} - -export type TRequestClient = (arg: IRequestClientOptions) => void; //Promise; - -class HttpRequest { - protected requestClient: TRequestClient; - protected urlValue: string = ''; - protected method: HttpMethod; - protected headersValue: IHeaders = {}; - protected queryParamsValue: TQueryParams = {}; - - constructor(requestClient: TRequestClient, method: HttpMethod) { - this.requestClient = requestClient; - this.method = method; - } - - url(url: string): this { - this.urlValue = url; - - return this; - } - - headers(headers: IHeaders): this { - Object.assign(this.headersValue, headers); - - return this; - } - - queryParams(queryParams: TQueryParams): this { - Object.assign(this.queryParamsValue, queryParams); - - return this; - } - - send(): void /*Promise*/ { - return this.requestClient({ - url: this.urlValue, - method: this.method, - headers: this.headersValue, - queryParams: this.queryParamsValue, - }); - } -} - -class HttpRequestWithBody extends HttpRequest { - private bodyValue?: Object = {}; - - body(body: Object): this { - this.bodyValue = body; - - return this; - } - - send(): void/*Promise*/ { - return super.requestClient({ - url: this.urlValue, - method: super.method, - headers: super.headersValue, - queryParams: super.queryParamsValue, - body: this.bodyValue - }) - } +export interface IHttpClientInit { + baseURL: string; + headers?: IHeaders; + timeout?: number; } -interface IHttpClientInit { - headers?: IHeadersInit; -} +// TODO: добавить абстрактный класс вместо интерфейса со статичными методами? export interface IHttpClient { - headers: Headers; - requestClient(arg: IRequestClientOptions): void; // Promise; - addHeader(key: string, value: string): HttpClient; - get(): HttpRequest; - post(): HttpRequestWithBody; - put(): HttpRequestWithBody; - patch(): HttpRequestWithBody; - delete(): HttpRequest; -} - -export class HttpClient implements IHttpClient { - headers; - requestClient; - - constructor({ headers }: IHttpClientInit) { - this.headers = new Headers(headers); - this.requestClient = function(arg: IRequestClientOptions): void { - // fetch implementation - // encodeURI(this.baseUrl.toString() + this.path), - }; - } - - addHeader(key: string, value: string): HttpClient { - this.headers.append(key, value); - - return this; - }; - - get() { - return new HttpRequest(this.requestClient, HttpMethod.GET); - } - - post() { - return new HttpRequestWithBody(this.requestClient, HttpMethod.POST); - } - - put() { - return new HttpRequestWithBody(this.requestClient, HttpMethod.PUT); - } - - patch() { - return new HttpRequestWithBody(this.requestClient, HttpMethod.PATCH); - } - - delete() { - return new HttpRequest(this.requestClient, HttpMethod.DELETE); - } + requestClient(arg: IRequestClientOptions): Promise; + setHeader(key: string, value: string): void; + get(): HttpRequest; + post(): HttpRequestWithBody; + put(): HttpRequestWithBody; + patch(): HttpRequestWithBody; + delete(): HttpRequest; } - -const test = new HttpClient({}); - -const getTest = test.post<{test: 'hello'}>().headers({ 'Content-Type': 'application/json' }).body(({ hello: 'test' })); diff --git a/services/http-service/src/httpRequest.ts b/services/http-service/src/httpRequest.ts new file mode 100644 index 00000000..aa574321 --- /dev/null +++ b/services/http-service/src/httpRequest.ts @@ -0,0 +1,77 @@ +import { HttpMethod } from './httpMethod.types'; + +export type IHeaders = Record; + +export type TQueryParamValue = string | number | boolean; + +export type TQueryParams = Record; + +export interface IRequestClientOptions { + url: string; + method: HttpMethod; + headers: IHeaders; + params: TQueryParams; + body?: Object; +} + +export type TRequestClient = (arg: IRequestClientOptions) => Promise; + +export class HttpRequest { + protected requestClient: TRequestClient; + protected urlValue: string = ''; + protected method: HttpMethod; + protected headersValue: IHeaders = {}; + protected paramsValue: TQueryParams = {}; + + constructor(requestClient: TRequestClient, method: HttpMethod) { + this.requestClient = requestClient; + this.method = method; + } + + url(url: string): this { + this.urlValue = url; + + return this; + } + + headers(headers: IHeaders): this { + Object.assign(this.headersValue, headers); + + return this; + } + + params(queryParams: TQueryParams): this { + Object.assign(this.paramsValue, queryParams); + + return this; + } + + send(): Promise { + return this.requestClient({ + url: this.urlValue, + method: this.method, + headers: this.headersValue, + params: this.paramsValue, + }); + } +} + +export class HttpRequestWithBody extends HttpRequest { + private bodyValue?: Object = {}; + + body(body: Object): this { + this.bodyValue = body; + + return this; + } + + send(): Promise { + return super.requestClient({ + url: this.urlValue, + method: super.method, + headers: super.headersValue, + params: super.paramsValue, + body: this.bodyValue + }) + } +} diff --git a/services/http-service/src/index.ts b/services/http-service/src/index.ts index 5589ab56..c550045e 100644 --- a/services/http-service/src/index.ts +++ b/services/http-service/src/index.ts @@ -1,6 +1,2 @@ -import HttpService from './httpService'; -import HttpRestController from './restController'; -import { HttpRestControllerAxios } from './constants/axiosRestController'; -import { HttpRestControllerFetch } from './constants/fetchRestController'; - -export { HttpService, HttpRestController, HttpRestControllerAxios, HttpRestControllerFetch }; +export { AxiosHttpClient } from './axiosHttpClient'; +export { FetchHttpClient } from './fetchHttpClient'; From d98551552ba0e11a40b51a54096cad4f15d57d9d Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Wed, 28 May 2025 12:11:19 +0300 Subject: [PATCH 24/49] feat(http-client): add http client abstract class --- services/http-service/src/axiosHttpClient.ts | 31 ++++--------- services/http-service/src/fetchHttpClient.ts | 43 +++--------------- services/http-service/src/httpClient.types.ts | 45 +++++++++++++++++-- 3 files changed, 55 insertions(+), 64 deletions(-) diff --git a/services/http-service/src/axiosHttpClient.ts b/services/http-service/src/axiosHttpClient.ts index 426e9ff5..ad716f37 100644 --- a/services/http-service/src/axiosHttpClient.ts +++ b/services/http-service/src/axiosHttpClient.ts @@ -1,13 +1,14 @@ import axios, { AxiosInstance } from 'axios'; -import { IHttpClient, IHttpClientInit, DEFAULT_REQUEST_TIMEOUT } from './httpClient.types'; -import { HttpRequest, HttpRequestWithBody, IRequestClientOptions } from './httpRequest'; -import { HttpMethod } from './httpMethod.types'; +import { HttpClient, IHttpClientInit, DEFAULT_REQUEST_TIMEOUT } from './httpClient.types'; +import { IRequestClientOptions } from './httpRequest'; -export class AxiosHttpClient implements IHttpClient { +export class AxiosHttpClient extends HttpClient { requestClient; private axiosInstance: AxiosInstance; constructor({ baseURL, headers, timeout }: IHttpClientInit) { + super(); + this.axiosInstance = axios.create({ baseURL, headers, @@ -22,24 +23,8 @@ export class AxiosHttpClient implements IHttpClient { setHeader(key: string, value: string): void { this.axiosInstance.defaults.headers[key] = value; }; +} - get() { - return new HttpRequest(this.requestClient, HttpMethod.GET); - } +const test = new AxiosHttpClient({ baseURL: 'http://localhost:3000/' }); - post() { - return new HttpRequestWithBody(this.requestClient, HttpMethod.POST); - } - - put() { - return new HttpRequestWithBody(this.requestClient, HttpMethod.PUT); - } - - patch() { - return new HttpRequestWithBody(this.requestClient, HttpMethod.PATCH); - } - - delete() { - return new HttpRequest(this.requestClient, HttpMethod.DELETE); - } -} \ No newline at end of file +const request = test.post<{ test: 'test'}>().url('helo').body({ mama: 'me' }).send(); \ No newline at end of file diff --git a/services/http-service/src/fetchHttpClient.ts b/services/http-service/src/fetchHttpClient.ts index edb63625..70026e9a 100644 --- a/services/http-service/src/fetchHttpClient.ts +++ b/services/http-service/src/fetchHttpClient.ts @@ -1,58 +1,27 @@ -import { HttpRequest, HttpRequestWithBody, IHeaders, IRequestClientOptions, TQueryParams } from './httpRequest'; -import { HttpMethod } from './httpMethod.types'; -import { DEFAULT_REQUEST_TIMEOUT, IHttpClient, IHttpClientInit } from './httpClient.types'; +import { IHeaders, IRequestClientOptions } from './httpRequest'; +import { DEFAULT_REQUEST_TIMEOUT, HttpClient, IHttpClientInit } from './httpClient.types'; -export class FetchHttpClient implements IHttpClient { +export class FetchHttpClient extends HttpClient { baseURL: string; headers: IHeaders = {}; timeout: number; requestClient; constructor({ baseURL, headers, timeout }: IHttpClientInit) { + super(); + this.baseURL = baseURL; this.headers = Object.assign(this.headers, headers); this.timeout = timeout ?? DEFAULT_REQUEST_TIMEOUT; this.requestClient = function(arg: IRequestClientOptions): Promise { - const url = encodeURI(`${this.baseURL}${arg.url}`) + this.buildQueryString(arg.params); + const url = encodeURI(`${this.baseURL}${arg.url}`) + HttpClient.buildQueryString(arg.params); const headers = Object.assign(this.headers, arg.headers); return fetch(url, { method: arg.method, headers, body: JSON.stringify(arg.body) }) as Promise; }; } - buildQueryString(queryParams: TQueryParams) { - const params = Object.keys(queryParams); - if (params.length > 0) { - return `?${ - params - .map(param => `${encodeURIComponent(param)}=${encodeURIComponent(queryParams[param])}`) - .join('&') - }`; - } - return ''; - } - setHeader(key: string, value: string): void { this.headers[key] = value; } - - get() { - return new HttpRequest(this.requestClient, HttpMethod.GET); - } - - post() { - return new HttpRequestWithBody(this.requestClient, HttpMethod.POST); - } - - put() { - return new HttpRequestWithBody(this.requestClient, HttpMethod.PUT); - } - - patch() { - return new HttpRequestWithBody(this.requestClient, HttpMethod.PATCH); - } - - delete() { - return new HttpRequest(this.requestClient, HttpMethod.DELETE); - } } diff --git a/services/http-service/src/httpClient.types.ts b/services/http-service/src/httpClient.types.ts index 986c92c4..5585a8b7 100644 --- a/services/http-service/src/httpClient.types.ts +++ b/services/http-service/src/httpClient.types.ts @@ -1,4 +1,5 @@ -import { HttpRequest, HttpRequestWithBody, IRequestClientOptions, IHeaders } from './httpRequest'; +import { HttpRequest, HttpRequestWithBody, IRequestClientOptions, IHeaders, TQueryParams } from './httpRequest'; +import { HttpMethod } from './httpMethod.types'; export const DEFAULT_REQUEST_TIMEOUT = 60000; @@ -8,8 +9,6 @@ export interface IHttpClientInit { timeout?: number; } -// TODO: добавить абстрактный класс вместо интерфейса со статичными методами? - export interface IHttpClient { requestClient(arg: IRequestClientOptions): Promise; setHeader(key: string, value: string): void; @@ -17,5 +16,43 @@ export interface IHttpClient { post(): HttpRequestWithBody; put(): HttpRequestWithBody; patch(): HttpRequestWithBody; - delete(): HttpRequest; + delete(): HttpRequest; +} + +export abstract class HttpClient { + abstract requestClient(arg: IRequestClientOptions): Promise; + + abstract setHeader(key: string, value: string): void; + + static buildQueryString(queryParams: TQueryParams) { + const params = Object.keys(queryParams); + if (params.length > 0) { + return `?${ + params + .map(param => `${encodeURIComponent(param)}=${encodeURIComponent(queryParams[param])}`) + .join('&') + }`; + } + return ''; + } + + get() { + return new HttpRequest(this.requestClient, HttpMethod.GET); + } + + post() { + return new HttpRequestWithBody(this.requestClient, HttpMethod.POST); + } + + put() { + return new HttpRequestWithBody(this.requestClient, HttpMethod.PUT); + } + + patch() { + return new HttpRequestWithBody(this.requestClient, HttpMethod.PATCH); + } + + delete() { + return new HttpRequest(this.requestClient, HttpMethod.DELETE); + } } From b4ec4a84ac10274b48a57f7c77bbae2e9a9ac4d1 Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Wed, 28 May 2025 13:12:40 +0300 Subject: [PATCH 25/49] feat(http-client): axios-http-client post request test --- package-lock.json | 39 +++++++++++++++++ package.json | 1 + .../src/__tests__/axiosHttpClient.tests.ts | 42 +++++++++++++++++++ services/http-service/src/axiosHttpClient.ts | 2 +- services/http-service/src/fetchHttpClient.ts | 2 +- .../{httpClient.types.ts => httpClient.ts} | 8 ++-- .../http-service/src/httpService.types.ts | 2 +- .../axiosRestController.ts | 2 +- .../fetchRestController.ts | 2 +- .../{constants => temporary}/httpService.ts | 2 +- .../src/{ => temporary}/restController.ts | 2 +- 11 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 services/http-service/src/__tests__/axiosHttpClient.tests.ts rename services/http-service/src/{httpClient.types.ts => httpClient.ts} (94%) rename services/http-service/src/{constants => temporary}/axiosRestController.ts (99%) rename services/http-service/src/{constants => temporary}/fetchRestController.ts (97%) rename services/http-service/src/{constants => temporary}/httpService.ts (96%) rename services/http-service/src/{ => temporary}/restController.ts (92%) diff --git a/package-lock.json b/package-lock.json index 258c6fd5..8334cf7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@types/estree": "^1.0.5", "@types/react": "^17.0.21", "@types/react-dom": "^17.0.17", + "axios-mock-adapter": "^2.1.0", "classnames": "^2.3.1", "eslint": "8.22.0", "happy-dom": "^17.4.4", @@ -3827,6 +3828,20 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axios-mock-adapter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.1.0.tgz", + "integrity": "sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, "node_modules/axobject-query": { "version": "2.2.0", "dev": true, @@ -7111,6 +7126,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/is-builtin-module": { "version": "3.2.0", "dev": true, diff --git a/package.json b/package.json index 2c96969e..28024777 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@types/estree": "^1.0.5", "@types/react": "^17.0.21", "@types/react-dom": "^17.0.17", + "axios-mock-adapter": "^2.1.0", "classnames": "^2.3.1", "eslint": "8.22.0", "happy-dom": "^17.4.4", diff --git a/services/http-service/src/__tests__/axiosHttpClient.tests.ts b/services/http-service/src/__tests__/axiosHttpClient.tests.ts new file mode 100644 index 00000000..1ad71c1b --- /dev/null +++ b/services/http-service/src/__tests__/axiosHttpClient.tests.ts @@ -0,0 +1,42 @@ +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { AxiosHttpClient } from '../axiosHttpClient'; + +describe('services/AxiosHttpClient', () => { + let mockAxios: MockAdapter; + + beforeEach(() => { + mockAxios = new MockAdapter(axios); + }) + + afterEach(() => { + mockAxios.restore(); + }); + + it('should send POST request with correct headers, body, and URL', async () => { + const baseUrl = 'https://some-url.ru'; + const path = '/test-path'; + const data = { key: 'value' }; + const headers = { 'Authorization': 'Bearer token' }; + + mockAxios.onPost(baseUrl + path, data, { headers }).reply(200, { success: true }); + + const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl }); + + const response = await httpClientInstance + .post() + .url(path) + .headers(headers) + .body(data) + .send(); + + expect(mockAxios.history.post.length).toBe(1); + + const lastRequest = mockAxios.history.post[0]; + + expect(lastRequest.url).toBe(baseUrl + path); + expect(lastRequest.data).toEqual(JSON.stringify(data)); + expect(lastRequest.headers).toMatchObject(headers); + expect(response).toEqual({ success: true }); + }); +}); diff --git a/services/http-service/src/axiosHttpClient.ts b/services/http-service/src/axiosHttpClient.ts index ad716f37..eca16a29 100644 --- a/services/http-service/src/axiosHttpClient.ts +++ b/services/http-service/src/axiosHttpClient.ts @@ -1,5 +1,5 @@ import axios, { AxiosInstance } from 'axios'; -import { HttpClient, IHttpClientInit, DEFAULT_REQUEST_TIMEOUT } from './httpClient.types'; +import { HttpClient, IHttpClientInit, DEFAULT_REQUEST_TIMEOUT } from './httpClient'; import { IRequestClientOptions } from './httpRequest'; export class AxiosHttpClient extends HttpClient { diff --git a/services/http-service/src/fetchHttpClient.ts b/services/http-service/src/fetchHttpClient.ts index 70026e9a..972c956b 100644 --- a/services/http-service/src/fetchHttpClient.ts +++ b/services/http-service/src/fetchHttpClient.ts @@ -1,5 +1,5 @@ import { IHeaders, IRequestClientOptions } from './httpRequest'; -import { DEFAULT_REQUEST_TIMEOUT, HttpClient, IHttpClientInit } from './httpClient.types'; +import { DEFAULT_REQUEST_TIMEOUT, HttpClient, IHttpClientInit } from './httpClient'; export class FetchHttpClient extends HttpClient { baseURL: string; diff --git a/services/http-service/src/httpClient.types.ts b/services/http-service/src/httpClient.ts similarity index 94% rename from services/http-service/src/httpClient.types.ts rename to services/http-service/src/httpClient.ts index 5585a8b7..5baf53dc 100644 --- a/services/http-service/src/httpClient.types.ts +++ b/services/http-service/src/httpClient.ts @@ -36,19 +36,19 @@ export abstract class HttpClient { return ''; } - get() { + get() { return new HttpRequest(this.requestClient, HttpMethod.GET); } - post() { + post() { return new HttpRequestWithBody(this.requestClient, HttpMethod.POST); } - put() { + put() { return new HttpRequestWithBody(this.requestClient, HttpMethod.PUT); } - patch() { + patch() { return new HttpRequestWithBody(this.requestClient, HttpMethod.PATCH); } diff --git a/services/http-service/src/httpService.types.ts b/services/http-service/src/httpService.types.ts index 0173ddde..5ac61fec 100644 --- a/services/http-service/src/httpService.types.ts +++ b/services/http-service/src/httpService.types.ts @@ -1,4 +1,4 @@ -import HttpRestController from './restController'; +import HttpRestController from './temporary/restController'; export interface IHttpServiceOptions { restController?: RestController; diff --git a/services/http-service/src/constants/axiosRestController.ts b/services/http-service/src/temporary/axiosRestController.ts similarity index 99% rename from services/http-service/src/constants/axiosRestController.ts rename to services/http-service/src/temporary/axiosRestController.ts index 935c52bf..856ce6b1 100644 --- a/services/http-service/src/constants/axiosRestController.ts +++ b/services/http-service/src/temporary/axiosRestController.ts @@ -1,5 +1,5 @@ import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; -import HttpRestController from '../restController'; +import HttpRestController from './restController'; import { ITokenData, ITokenPayload } from '../token.types'; import { HttpStatus } from '../httpStatus'; diff --git a/services/http-service/src/constants/fetchRestController.ts b/services/http-service/src/temporary/fetchRestController.ts similarity index 97% rename from services/http-service/src/constants/fetchRestController.ts rename to services/http-service/src/temporary/fetchRestController.ts index ebdb45b1..33f814ed 100644 --- a/services/http-service/src/constants/fetchRestController.ts +++ b/services/http-service/src/temporary/fetchRestController.ts @@ -1,4 +1,4 @@ -import HttpRestController from '../restController'; +import HttpRestController from './restController'; import { Headers, Response } from 'happy-dom'; import { ITokenData } from '../token.types'; diff --git a/services/http-service/src/constants/httpService.ts b/services/http-service/src/temporary/httpService.ts similarity index 96% rename from services/http-service/src/constants/httpService.ts rename to services/http-service/src/temporary/httpService.ts index c1ca5b40..4165a931 100644 --- a/services/http-service/src/constants/httpService.ts +++ b/services/http-service/src/temporary/httpService.ts @@ -1,5 +1,5 @@ import { IHttpServiceOptions } from '../httpService.types'; -import HttpRestController from '../restController'; +import HttpRestController from './restController'; class HttpService { public restController: RestController | undefined; diff --git a/services/http-service/src/restController.ts b/services/http-service/src/temporary/restController.ts similarity index 92% rename from services/http-service/src/restController.ts rename to services/http-service/src/temporary/restController.ts index e944d6af..5a1066ff 100644 --- a/services/http-service/src/restController.ts +++ b/services/http-service/src/temporary/restController.ts @@ -1,4 +1,4 @@ -import { ITokenData } from './token.types'; +import { ITokenData } from '../token.types'; abstract class HttpRestController { abstract get(...args: any[]): Promise; From e82b1103d88d389822cb5c5cd52054e11b79b600 Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Thu, 29 May 2025 10:02:51 +0300 Subject: [PATCH 26/49] feat(http-client): tests --- package-lock.json | 84 ++++++++++++++----- package.json | 1 - services/http-service/package.json | 3 + .../src/__tests__/axiosHttpClient.tests.ts | 12 +-- services/http-service/src/axiosHttpClient.ts | 4 - 5 files changed, 74 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index df5d0711..e6cde5d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5193,6 +5193,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -5473,7 +5479,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5807,10 +5812,23 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=16" @@ -6309,6 +6327,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -6403,7 +6430,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -6538,7 +6564,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6548,7 +6573,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6614,7 +6638,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -6627,7 +6650,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7909,6 +7931,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-extra": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", @@ -7950,7 +7987,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8018,7 +8054,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -8043,7 +8078,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -8265,7 +8299,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8382,7 +8415,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8395,7 +8427,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -8411,7 +8442,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -10286,7 +10316,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10390,6 +10419,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -14688,12 +14738,6 @@ "version": "0.1.0", "license": "Apache-2.0" }, - "styles/utilities": { - "name": "@byndyusoft-ui/css-utilities", - "version": "0.0.1", - "extraneous": true, - "license": "Apache-2.0" - }, "styles/utilities": { "name": "@byndyusoft-ui/css-utilities", "version": "0.0.1", diff --git a/package.json b/package.json index 28024777..2c96969e 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "@types/estree": "^1.0.5", "@types/react": "^17.0.21", "@types/react-dom": "^17.0.17", - "axios-mock-adapter": "^2.1.0", "classnames": "^2.3.1", "eslint": "8.22.0", "happy-dom": "^17.4.4", diff --git a/services/http-service/package.json b/services/http-service/package.json index 183ee5f7..6b53ab6f 100644 --- a/services/http-service/package.json +++ b/services/http-service/package.json @@ -38,6 +38,9 @@ "publishConfig": { "access": "public" }, + "devDependencies": { + "axios-mock-adapter": "^2.1.0" + }, "dependencies": { "axios": "^1.9.0" } diff --git a/services/http-service/src/__tests__/axiosHttpClient.tests.ts b/services/http-service/src/__tests__/axiosHttpClient.tests.ts index 1ad71c1b..7bc6fb18 100644 --- a/services/http-service/src/__tests__/axiosHttpClient.tests.ts +++ b/services/http-service/src/__tests__/axiosHttpClient.tests.ts @@ -13,13 +13,15 @@ describe('services/AxiosHttpClient', () => { mockAxios.restore(); }); - it('should send POST request with correct headers, body, and URL', async () => { + test('should send POST request with correct headers, body, and URL', async () => { const baseUrl = 'https://some-url.ru'; const path = '/test-path'; - const data = { key: 'value' }; + const body = { key: 'value' }; const headers = { 'Authorization': 'Bearer token' }; - mockAxios.onPost(baseUrl + path, data, { headers }).reply(200, { success: true }); + mockAxios + .onPost(baseUrl + path, { key: 'response' }, { headers }) + .reply(200, { success: true }); const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl }); @@ -27,7 +29,7 @@ describe('services/AxiosHttpClient', () => { .post() .url(path) .headers(headers) - .body(data) + .body(body) .send(); expect(mockAxios.history.post.length).toBe(1); @@ -35,7 +37,7 @@ describe('services/AxiosHttpClient', () => { const lastRequest = mockAxios.history.post[0]; expect(lastRequest.url).toBe(baseUrl + path); - expect(lastRequest.data).toEqual(JSON.stringify(data)); + expect(lastRequest.data).toEqual(JSON.stringify(body)); expect(lastRequest.headers).toMatchObject(headers); expect(response).toEqual({ success: true }); }); diff --git a/services/http-service/src/axiosHttpClient.ts b/services/http-service/src/axiosHttpClient.ts index eca16a29..2f3999cd 100644 --- a/services/http-service/src/axiosHttpClient.ts +++ b/services/http-service/src/axiosHttpClient.ts @@ -24,7 +24,3 @@ export class AxiosHttpClient extends HttpClient { this.axiosInstance.defaults.headers[key] = value; }; } - -const test = new AxiosHttpClient({ baseURL: 'http://localhost:3000/' }); - -const request = test.post<{ test: 'test'}>().url('helo').body({ mama: 'me' }).send(); \ No newline at end of file From 988143f6b7f16b41c73af614370221564ece12c5 Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Thu, 29 May 2025 13:55:25 +0300 Subject: [PATCH 27/49] feat(http-client): reorganize code, rename directory --- package-lock.json | 18 ++++++++++++++++-- services/http-service/src/index.ts | 2 -- services/{http-service => http}/.npmignore | 0 services/{http-service => http}/README.md | 4 ++-- services/{http-service => http}/package.json | 0 .../{http-service => http}/rollup.config.js | 0 .../src/__tests__/axiosHttpClient.tests.ts | 2 +- services/http/src/index.ts | 3 +++ .../services/axiosClient}/axiosHttpClient.ts | 4 ++-- .../http/src/services/axiosClient/index.ts | 2 ++ .../services/fetchClient}/fetchHttpClient.ts | 4 ++-- .../http/src/services/fetchClient/index.ts | 2 ++ .../src => http/src/services}/httpClient.ts | 2 +- .../src => http/src/services}/httpRequest.ts | 2 +- .../src/temporary/axiosRestController.ts | 6 +++--- .../src/temporary/fetchRestController.ts | 2 +- .../src/temporary/httpService.ts | 2 +- .../src/temporary}/httpService.types.ts | 2 +- .../src/temporary/restController.ts | 2 +- .../src => http/src/types}/httpMethod.types.ts | 0 .../src/types/httpStatus.types.ts} | 2 +- .../src => http/src/types}/token.types.ts | 0 services/{http-service => http}/tsconfig.json | 0 services/http/vitest.config.ts | 13 +++++++++++++ 24 files changed, 53 insertions(+), 21 deletions(-) delete mode 100644 services/http-service/src/index.ts rename services/{http-service => http}/.npmignore (100%) rename services/{http-service => http}/README.md (96%) rename services/{http-service => http}/package.json (100%) rename services/{http-service => http}/rollup.config.js (100%) rename services/{http-service => http}/src/__tests__/axiosHttpClient.tests.ts (95%) create mode 100644 services/http/src/index.ts rename services/{http-service/src => http/src/services/axiosClient}/axiosHttpClient.ts (91%) create mode 100644 services/http/src/services/axiosClient/index.ts rename services/{http-service/src => http/src/services/fetchClient}/fetchHttpClient.ts (91%) create mode 100644 services/http/src/services/fetchClient/index.ts rename services/{http-service/src => http/src/services}/httpClient.ts (96%) rename services/{http-service/src => http/src/services}/httpRequest.ts (96%) rename services/{http-service => http}/src/temporary/axiosRestController.ts (95%) rename services/{http-service => http}/src/temporary/fetchRestController.ts (97%) rename services/{http-service => http}/src/temporary/httpService.ts (96%) rename services/{http-service/src => http/src/temporary}/httpService.types.ts (65%) rename services/{http-service => http}/src/temporary/restController.ts (91%) rename services/{http-service/src => http/src/types}/httpMethod.types.ts (100%) rename services/{http-service/src/httpStatus.ts => http/src/types/httpStatus.types.ts} (96%) rename services/{http-service/src => http/src/types}/token.types.ts (100%) rename services/{http-service => http}/tsconfig.json (100%) create mode 100644 services/http/vitest.config.ts diff --git a/package-lock.json b/package-lock.json index e6cde5d0..1be16d3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,6 @@ "@types/estree": "^1.0.5", "@types/react": "^17.0.21", "@types/react-dom": "^17.0.17", - "axios-mock-adapter": "^2.1.0", "classnames": "^2.3.1", "eslint": "8.22.0", "happy-dom": "^17.4.4", @@ -726,7 +725,7 @@ "link": true }, "node_modules/@byndyusoft-ui/http-service": { - "resolved": "services/http-service", + "resolved": "services/http", "link": true }, "node_modules/@byndyusoft-ui/keyframes-css": { @@ -14711,6 +14710,17 @@ "version": "0.3.0", "license": "Apache-2.0" }, + "services/http": { + "name": "@byndyusoft-ui/http-service", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.9.0" + }, + "devDependencies": { + "axios-mock-adapter": "^2.1.0" + } + }, "services/http-request": { "name": "@byndyusoft-ui/http-request", "version": "0.0.1", @@ -14723,9 +14733,13 @@ "services/http-service": { "name": "@byndyusoft-ui/http-service", "version": "0.0.1", + "extraneous": true, "license": "Apache-2.0", "dependencies": { "axios": "^1.9.0" + }, + "devDependencies": { + "axios-mock-adapter": "^2.1.0" } }, "styles/keyframes-css": { diff --git a/services/http-service/src/index.ts b/services/http-service/src/index.ts deleted file mode 100644 index c550045e..00000000 --- a/services/http-service/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { AxiosHttpClient } from './axiosHttpClient'; -export { FetchHttpClient } from './fetchHttpClient'; diff --git a/services/http-service/.npmignore b/services/http/.npmignore similarity index 100% rename from services/http-service/.npmignore rename to services/http/.npmignore diff --git a/services/http-service/README.md b/services/http/README.md similarity index 96% rename from services/http-service/README.md rename to services/http/README.md index 2dad8a94..35b5c183 100644 --- a/services/http-service/README.md +++ b/services/http/README.md @@ -4,7 +4,7 @@ ### Installation ```bash -npm i @byndyusoft-ui/http-service +npm i @byndyusoft-ui/http ``` ## Usage @@ -56,7 +56,7 @@ There are two classes ready to be used as restControllers: **HttpRestControllerF ```ts // myOwnHttpRestController.ts - import { HttpRestController } from '@byndyusoft-ui/http-service'; + import { HttpRestController } from '@byndyusoft-ui/http'; class HttpRestControllerCustom extends HttpRestController { constructor() { diff --git a/services/http-service/package.json b/services/http/package.json similarity index 100% rename from services/http-service/package.json rename to services/http/package.json diff --git a/services/http-service/rollup.config.js b/services/http/rollup.config.js similarity index 100% rename from services/http-service/rollup.config.js rename to services/http/rollup.config.js diff --git a/services/http-service/src/__tests__/axiosHttpClient.tests.ts b/services/http/src/__tests__/axiosHttpClient.tests.ts similarity index 95% rename from services/http-service/src/__tests__/axiosHttpClient.tests.ts rename to services/http/src/__tests__/axiosHttpClient.tests.ts index 7bc6fb18..e266e995 100644 --- a/services/http-service/src/__tests__/axiosHttpClient.tests.ts +++ b/services/http/src/__tests__/axiosHttpClient.tests.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import { AxiosHttpClient } from '../axiosHttpClient'; +import AxiosHttpClient from '../services/axiosClient'; describe('services/AxiosHttpClient', () => { let mockAxios: MockAdapter; diff --git a/services/http/src/index.ts b/services/http/src/index.ts new file mode 100644 index 00000000..4f3037b6 --- /dev/null +++ b/services/http/src/index.ts @@ -0,0 +1,3 @@ +export { default as AxiosClient } from './services/axiosClient'; +export { default as FetchClient } from './services/fetchClient'; +export { HttpStatusTypes } from './types/httpStatus.types'; diff --git a/services/http-service/src/axiosHttpClient.ts b/services/http/src/services/axiosClient/axiosHttpClient.ts similarity index 91% rename from services/http-service/src/axiosHttpClient.ts rename to services/http/src/services/axiosClient/axiosHttpClient.ts index 2f3999cd..74e7d342 100644 --- a/services/http-service/src/axiosHttpClient.ts +++ b/services/http/src/services/axiosClient/axiosHttpClient.ts @@ -1,6 +1,6 @@ import axios, { AxiosInstance } from 'axios'; -import { HttpClient, IHttpClientInit, DEFAULT_REQUEST_TIMEOUT } from './httpClient'; -import { IRequestClientOptions } from './httpRequest'; +import { HttpClient, IHttpClientInit, DEFAULT_REQUEST_TIMEOUT } from '../httpClient'; +import { IRequestClientOptions } from '../httpRequest'; export class AxiosHttpClient extends HttpClient { requestClient; diff --git a/services/http/src/services/axiosClient/index.ts b/services/http/src/services/axiosClient/index.ts new file mode 100644 index 00000000..d6f1c403 --- /dev/null +++ b/services/http/src/services/axiosClient/index.ts @@ -0,0 +1,2 @@ +import { AxiosHttpClient as AxiosClient } from './axiosHttpClient'; +export default AxiosClient; diff --git a/services/http-service/src/fetchHttpClient.ts b/services/http/src/services/fetchClient/fetchHttpClient.ts similarity index 91% rename from services/http-service/src/fetchHttpClient.ts rename to services/http/src/services/fetchClient/fetchHttpClient.ts index 972c956b..30ceb332 100644 --- a/services/http-service/src/fetchHttpClient.ts +++ b/services/http/src/services/fetchClient/fetchHttpClient.ts @@ -1,5 +1,5 @@ -import { IHeaders, IRequestClientOptions } from './httpRequest'; -import { DEFAULT_REQUEST_TIMEOUT, HttpClient, IHttpClientInit } from './httpClient'; +import { IHeaders, IRequestClientOptions } from '../httpRequest'; +import { DEFAULT_REQUEST_TIMEOUT, HttpClient, IHttpClientInit } from '../httpClient'; export class FetchHttpClient extends HttpClient { baseURL: string; diff --git a/services/http/src/services/fetchClient/index.ts b/services/http/src/services/fetchClient/index.ts new file mode 100644 index 00000000..ab64bf6e --- /dev/null +++ b/services/http/src/services/fetchClient/index.ts @@ -0,0 +1,2 @@ +import { FetchHttpClient as FetchClient } from './fetchHttpClient'; +export default FetchClient; diff --git a/services/http-service/src/httpClient.ts b/services/http/src/services/httpClient.ts similarity index 96% rename from services/http-service/src/httpClient.ts rename to services/http/src/services/httpClient.ts index 5baf53dc..9ef7f644 100644 --- a/services/http-service/src/httpClient.ts +++ b/services/http/src/services/httpClient.ts @@ -1,5 +1,5 @@ import { HttpRequest, HttpRequestWithBody, IRequestClientOptions, IHeaders, TQueryParams } from './httpRequest'; -import { HttpMethod } from './httpMethod.types'; +import { HttpMethod } from '../types/httpMethod.types'; export const DEFAULT_REQUEST_TIMEOUT = 60000; diff --git a/services/http-service/src/httpRequest.ts b/services/http/src/services/httpRequest.ts similarity index 96% rename from services/http-service/src/httpRequest.ts rename to services/http/src/services/httpRequest.ts index aa574321..6c227bf6 100644 --- a/services/http-service/src/httpRequest.ts +++ b/services/http/src/services/httpRequest.ts @@ -1,4 +1,4 @@ -import { HttpMethod } from './httpMethod.types'; +import { HttpMethod } from '../types/httpMethod.types'; export type IHeaders = Record; diff --git a/services/http-service/src/temporary/axiosRestController.ts b/services/http/src/temporary/axiosRestController.ts similarity index 95% rename from services/http-service/src/temporary/axiosRestController.ts rename to services/http/src/temporary/axiosRestController.ts index 856ce6b1..03f6e8a2 100644 --- a/services/http-service/src/temporary/axiosRestController.ts +++ b/services/http/src/temporary/axiosRestController.ts @@ -1,7 +1,7 @@ import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; import HttpRestController from './restController'; -import { ITokenData, ITokenPayload } from '../token.types'; -import { HttpStatus } from '../httpStatus'; +import { ITokenData, ITokenPayload } from '../types/token.types'; +import { HttpStatusTypes } from '../types/httpStatus.types'; const DEFAULT_REQUEST_TIMEOUT = 60000; @@ -96,7 +96,7 @@ export class HttpRestControllerAxios extends HttpRestController { const requestConfig = error.config; const isNotRefreshUrl = error.config?.url !== this.tokenData?.tokenRefreshUrl; - if (error.response?.status === (HttpStatus.UNAUTHORIZED as number) && isNotRefreshUrl) { + if (error.response?.status === (HttpStatusTypes.UNAUTHORIZED as number) && isNotRefreshUrl) { try { const newAccessToken = await this.refreshTokens(); diff --git a/services/http-service/src/temporary/fetchRestController.ts b/services/http/src/temporary/fetchRestController.ts similarity index 97% rename from services/http-service/src/temporary/fetchRestController.ts rename to services/http/src/temporary/fetchRestController.ts index 33f814ed..2d7d817a 100644 --- a/services/http-service/src/temporary/fetchRestController.ts +++ b/services/http/src/temporary/fetchRestController.ts @@ -1,6 +1,6 @@ import HttpRestController from './restController'; import { Headers, Response } from 'happy-dom'; -import { ITokenData } from '../token.types'; +import { ITokenData } from '../types/token.types'; export class HttpRestControllerFetch extends HttpRestController { public headers: Headers = new Headers(); diff --git a/services/http-service/src/temporary/httpService.ts b/services/http/src/temporary/httpService.ts similarity index 96% rename from services/http-service/src/temporary/httpService.ts rename to services/http/src/temporary/httpService.ts index 4165a931..a7afb909 100644 --- a/services/http-service/src/temporary/httpService.ts +++ b/services/http/src/temporary/httpService.ts @@ -1,4 +1,4 @@ -import { IHttpServiceOptions } from '../httpService.types'; +import { IHttpServiceOptions } from './httpService.types'; import HttpRestController from './restController'; class HttpService { diff --git a/services/http-service/src/httpService.types.ts b/services/http/src/temporary/httpService.types.ts similarity index 65% rename from services/http-service/src/httpService.types.ts rename to services/http/src/temporary/httpService.types.ts index 5ac61fec..0173ddde 100644 --- a/services/http-service/src/httpService.types.ts +++ b/services/http/src/temporary/httpService.types.ts @@ -1,4 +1,4 @@ -import HttpRestController from './temporary/restController'; +import HttpRestController from './restController'; export interface IHttpServiceOptions { restController?: RestController; diff --git a/services/http-service/src/temporary/restController.ts b/services/http/src/temporary/restController.ts similarity index 91% rename from services/http-service/src/temporary/restController.ts rename to services/http/src/temporary/restController.ts index 5a1066ff..8d336277 100644 --- a/services/http-service/src/temporary/restController.ts +++ b/services/http/src/temporary/restController.ts @@ -1,4 +1,4 @@ -import { ITokenData } from '../token.types'; +import { ITokenData } from '../types/token.types'; abstract class HttpRestController { abstract get(...args: any[]): Promise; diff --git a/services/http-service/src/httpMethod.types.ts b/services/http/src/types/httpMethod.types.ts similarity index 100% rename from services/http-service/src/httpMethod.types.ts rename to services/http/src/types/httpMethod.types.ts diff --git a/services/http-service/src/httpStatus.ts b/services/http/src/types/httpStatus.types.ts similarity index 96% rename from services/http-service/src/httpStatus.ts rename to services/http/src/types/httpStatus.types.ts index f6233c44..1b9a08b5 100644 --- a/services/http-service/src/httpStatus.ts +++ b/services/http/src/types/httpStatus.types.ts @@ -1,4 +1,4 @@ -export declare enum HttpStatus { +export declare enum HttpStatusTypes { OK = 200, CREATED = 201, ACCEPTED = 202, diff --git a/services/http-service/src/token.types.ts b/services/http/src/types/token.types.ts similarity index 100% rename from services/http-service/src/token.types.ts rename to services/http/src/types/token.types.ts diff --git a/services/http-service/tsconfig.json b/services/http/tsconfig.json similarity index 100% rename from services/http-service/tsconfig.json rename to services/http/tsconfig.json diff --git a/services/http/vitest.config.ts b/services/http/vitest.config.ts new file mode 100644 index 00000000..f49baebe --- /dev/null +++ b/services/http/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineProject, mergeConfig } from 'vitest/config'; +import configShared from '../../vitest.config'; + +/** + * vitest.config for correct vitest workspace detection. + * export default configShared works too. + */ +export default mergeConfig(configShared, defineProject({ + test: { + include: ['**/*.tests.(ts|tsx)'], + setupFiles: ['../../setupTests.ts'], + } +})); From 6efad91c2c21689d7f40ec34fc94888d9b7008ab Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Thu, 29 May 2025 17:04:43 +0300 Subject: [PATCH 28/49] feat(http-client): fixes --- services/http/package.json | 10 +++++----- services/http/src/index.ts | 2 +- services/http/src/temporary/axiosRestController.ts | 4 ++-- services/http/src/types/httpMethod.types.ts | 4 +++- .../{httpStatus.types.ts => httpStatusCode.types.ts} | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) rename services/http/src/types/{httpStatus.types.ts => httpStatusCode.types.ts} (96%) diff --git a/services/http/package.json b/services/http/package.json index 6b53ab6f..5cc79fdd 100644 --- a/services/http/package.json +++ b/services/http/package.json @@ -1,7 +1,7 @@ { - "name": "@byndyusoft-ui/http-service", + "name": "@byndyusoft-ui/http", "version": "0.0.1", - "description": "Byndyusoft UI HTTP Service", + "description": "Byndyusoft UI HTTP Client", "keywords": [ "byndyusoft", "byndyusoft-ui", @@ -11,7 +11,7 @@ "fetch" ], "author": "Byndyusoft Frontend Developer ", - "homepage": "https://github.com/Byndyusoft/ui/tree/master/services/http-service#readme", + "homepage": "https://github.com/Byndyusoft/ui/tree/master/services/http#readme", "license": "Apache-2.0", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -22,7 +22,7 @@ "scripts": { "build": "rollup --config", "clean": "rimraf dist", - "test": "jest --config ../../jest.config.js --roots services/http-service/src", + "test": "jest --config ../../jest.config.js --roots services/http/src", "lint:check": "npm run eslint:check && npm run prettier:check && npm run stylelint:check", "lint:fix": "npm run eslint:fix && npm run prettier:fix && npm run stylelint:fix", "eslint:check": "eslint src --config ../../eslint.config.js", @@ -38,7 +38,7 @@ "publishConfig": { "access": "public" }, - "devDependencies": { + "peerDependencies": { "axios-mock-adapter": "^2.1.0" }, "dependencies": { diff --git a/services/http/src/index.ts b/services/http/src/index.ts index 4f3037b6..28fd6628 100644 --- a/services/http/src/index.ts +++ b/services/http/src/index.ts @@ -1,3 +1,3 @@ export { default as AxiosClient } from './services/axiosClient'; export { default as FetchClient } from './services/fetchClient'; -export { HttpStatusTypes } from './types/httpStatus.types'; +export { HttpStatusCode } from './types/httpStatusCode.types'; diff --git a/services/http/src/temporary/axiosRestController.ts b/services/http/src/temporary/axiosRestController.ts index 03f6e8a2..25d463bd 100644 --- a/services/http/src/temporary/axiosRestController.ts +++ b/services/http/src/temporary/axiosRestController.ts @@ -1,7 +1,7 @@ import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; import HttpRestController from './restController'; import { ITokenData, ITokenPayload } from '../types/token.types'; -import { HttpStatusTypes } from '../types/httpStatus.types'; +import { HttpStatusCode } from '../types/httpStatusCode.types'; const DEFAULT_REQUEST_TIMEOUT = 60000; @@ -96,7 +96,7 @@ export class HttpRestControllerAxios extends HttpRestController { const requestConfig = error.config; const isNotRefreshUrl = error.config?.url !== this.tokenData?.tokenRefreshUrl; - if (error.response?.status === (HttpStatusTypes.UNAUTHORIZED as number) && isNotRefreshUrl) { + if (error.response?.status === (HttpStatusCode.UNAUTHORIZED as number) && isNotRefreshUrl) { try { const newAccessToken = await this.refreshTokens(); diff --git a/services/http/src/types/httpMethod.types.ts b/services/http/src/types/httpMethod.types.ts index 91f60eab..80fc9e33 100644 --- a/services/http/src/types/httpMethod.types.ts +++ b/services/http/src/types/httpMethod.types.ts @@ -3,5 +3,7 @@ export enum HttpMethod { POST = 'POST', PUT = 'PUT', PATCH = 'PATCH', - DELETE = 'DELETE' + DELETE = 'DELETE', + OPTIONS = 'OPTIONS', + HEAD = 'HEAD' } diff --git a/services/http/src/types/httpStatus.types.ts b/services/http/src/types/httpStatusCode.types.ts similarity index 96% rename from services/http/src/types/httpStatus.types.ts rename to services/http/src/types/httpStatusCode.types.ts index 1b9a08b5..00f52143 100644 --- a/services/http/src/types/httpStatus.types.ts +++ b/services/http/src/types/httpStatusCode.types.ts @@ -1,4 +1,4 @@ -export declare enum HttpStatusTypes { +export enum HttpStatusCode { OK = 200, CREATED = 201, ACCEPTED = 202, From 255ef803d2d80b2c4a50a3cdc06e62ac428652de Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Thu, 29 May 2025 17:05:56 +0300 Subject: [PATCH 29/49] feat(http-service): config fixes --- package-lock.json | 11 +++++------ vitest.config.ts | 12 ++++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1be16d3b..bc05a706 100644 --- a/package-lock.json +++ b/package-lock.json @@ -724,7 +724,7 @@ "resolved": "components/highlighter", "link": true }, - "node_modules/@byndyusoft-ui/http-service": { + "node_modules/@byndyusoft-ui/http": { "resolved": "services/http", "link": true }, @@ -5239,8 +5239,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.1.0.tgz", "integrity": "sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==", - "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "is-buffer": "^2.0.5" @@ -7708,7 +7708,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-diff": { @@ -8820,7 +8819,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true, "funding": [ { "type": "github", @@ -8836,6 +8834,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -14711,13 +14710,13 @@ "license": "Apache-2.0" }, "services/http": { - "name": "@byndyusoft-ui/http-service", + "name": "@byndyusoft-ui/http", "version": "0.0.1", "license": "Apache-2.0", "dependencies": { "axios": "^1.9.0" }, - "devDependencies": { + "peerDependencies": { "axios-mock-adapter": "^2.1.0" } }, diff --git a/vitest.config.ts b/vitest.config.ts index 8bdd1c02..60a504b4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,12 +7,12 @@ export default defineConfig({ globals: true, environment: 'jsdom', //https://vitest.dev/guide/workspace - workspace: [ - 'hooks/use-timeout', //workspace example. 'hooks/*' plus separate vitest.config.ts in each workspace like in README.md will work too. - getWorkspaceConfig('components'), - getWorkspaceConfig('hooks'), - getWorkspaceConfig('packages') - ] + // workspace: [ + // 'hooks/use-timeout', //workspace example. 'hooks/*' plus separate vitest.config.ts in each workspace like in README.md will work too. + // getWorkspaceConfig('components'), + // getWorkspaceConfig('hooks'), + // getWorkspaceConfig('packages') + // ] } }); From bde45abd74abec53023fa1ff2435a20ae49943cf Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Sat, 31 May 2025 14:32:01 +0300 Subject: [PATCH 30/49] fix(http-client): vitest config --- README.md | 2 +- package-lock.json | 45 ++++++++++++------- .../{vitest.config.ts => vitest.config.mjs} | 2 +- vitest.config.mjs | 12 ++--- 4 files changed, 38 insertions(+), 23 deletions(-) rename services/http/{vitest.config.ts => vitest.config.mjs} (86%) diff --git a/README.md b/README.md index c3f839a8..6c5e7a86 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ There are two ways to define workspaces: - Inline in root `vitest.config.mts` -- In root config with glob string and vitest.config.ts in each workspace/package. Example: +- In root config with glob string and vitest.config.mjs in each workspace/package. Example: ```ts import { defineProject, mergeConfig } from 'vitest/config'; import configShared from '../../vitest.config'; diff --git a/package-lock.json b/package-lock.json index e0bcbea6..f758a4f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3742,7 +3742,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4436,6 +4435,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "dev": true, @@ -4513,7 +4521,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4635,7 +4642,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4643,7 +4649,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4701,7 +4706,6 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4712,7 +4716,6 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5626,7 +5629,6 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "dev": true, "license": "MIT" }, "node_modules/fast-diff": { @@ -5864,7 +5866,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5920,7 +5921,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5943,7 +5943,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -6156,7 +6155,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6251,7 +6249,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6262,7 +6259,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -6276,7 +6272,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7706,7 +7701,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7805,6 +7799,27 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/mime-db": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.24.0.tgz", + "integrity": "sha512-0XGpuLCNPqkv3vYiRjh1w6h4RbIGWyCh8OnXejta9INkFX0M8ENYth8O0As8rSGDxzEO1PafhiaqQdtqhtA2lw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.12.tgz", + "integrity": "sha512-irQD8Ww11AaU8vbCRjMuaq4huvb2ITxVg/VDBrvf8keFtbWZ3zbGO0tvsCMbD7JlR8mOYw0WbAqi4sL8KGUd5w==", + "license": "MIT", + "dependencies": { + "mime-db": "~1.24.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "dev": true, diff --git a/services/http/vitest.config.ts b/services/http/vitest.config.mjs similarity index 86% rename from services/http/vitest.config.ts rename to services/http/vitest.config.mjs index f49baebe..16a7a659 100644 --- a/services/http/vitest.config.ts +++ b/services/http/vitest.config.mjs @@ -1,5 +1,5 @@ import { defineProject, mergeConfig } from 'vitest/config'; -import configShared from '../../vitest.config'; +import configShared from '../../vitest.config.mjs'; /** * vitest.config for correct vitest workspace detection. diff --git a/vitest.config.mjs b/vitest.config.mjs index 918d8823..46d26600 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -9,12 +9,12 @@ export default defineConfig({ globals: true, environment: 'jsdom', //https://vitest.dev/guide/workspace - workspace: [ - 'hooks/use-timeout', //workspace example. 'hooks/*' plus separate vitest.config.ts in each workspace like in README.md will work too. - getWorkspaceConfig('components'), - getWorkspaceConfig('hooks'), - getWorkspaceConfig('packages') - ], + // workspace: [ + // 'hooks/use-timeout', //workspace example. 'hooks/*' plus separate vitest.config.mjs in each workspace like in README.md will work too. + // getWorkspaceConfig('components'), + // getWorkspaceConfig('hooks'), + // getWorkspaceConfig('packages') + // ], include: ['src/**/*.{test,tests,spec}.[jt]s?(x)', 'src/**/__tests__/**.*'], } }); From ab7dde51f93ddaef9412dff521e865b33a350f50 Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Sat, 31 May 2025 17:02:18 +0300 Subject: [PATCH 31/49] feat(http-client): axios client and request client fix --- .../src/__tests__/axiosHttpClient.tests.ts | 20 ++- .../services/axiosClient/axiosHttpClient.ts | 6 +- .../services/fetchClient/fetchHttpClient.ts | 3 +- services/http/src/services/httpClient.ts | 2 +- services/http/src/services/httpRequest.ts | 8 +- .../http/src/temporary/axiosRestController.ts | 162 ------------------ .../http/src/temporary/fetchRestController.ts | 78 --------- services/http/src/temporary/httpService.ts | 38 ---- .../http/src/temporary/httpService.types.ts | 5 - services/http/src/temporary/restController.ts | 19 -- 10 files changed, 24 insertions(+), 317 deletions(-) delete mode 100644 services/http/src/temporary/axiosRestController.ts delete mode 100644 services/http/src/temporary/fetchRestController.ts delete mode 100644 services/http/src/temporary/httpService.ts delete mode 100644 services/http/src/temporary/httpService.types.ts delete mode 100644 services/http/src/temporary/restController.ts diff --git a/services/http/src/__tests__/axiosHttpClient.tests.ts b/services/http/src/__tests__/axiosHttpClient.tests.ts index e266e995..be587666 100644 --- a/services/http/src/__tests__/axiosHttpClient.tests.ts +++ b/services/http/src/__tests__/axiosHttpClient.tests.ts @@ -2,6 +2,10 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import AxiosHttpClient from '../services/axiosClient'; +interface IResponseData { + success: boolean; +} + describe('services/AxiosHttpClient', () => { let mockAxios: MockAdapter; @@ -18,15 +22,16 @@ describe('services/AxiosHttpClient', () => { const path = '/test-path'; const body = { key: 'value' }; const headers = { 'Authorization': 'Bearer token' }; + const responseData = { success: true }; mockAxios - .onPost(baseUrl + path, { key: 'response' }, { headers }) - .reply(200, { success: true }); + .onPost(path) + .reply(200, responseData); - const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl }); + const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl, headers: { test: 'value' } }); const response = await httpClientInstance - .post() + .post() .url(path) .headers(headers) .body(body) @@ -36,9 +41,12 @@ describe('services/AxiosHttpClient', () => { const lastRequest = mockAxios.history.post[0]; - expect(lastRequest.url).toBe(baseUrl + path); + console.log(lastRequest.headers); + + expect(lastRequest.baseURL).toBe(baseUrl); + expect(lastRequest.url).toBe(path); expect(lastRequest.data).toEqual(JSON.stringify(body)); - expect(lastRequest.headers).toMatchObject(headers); + expect(lastRequest.headers).toMatchObject(Object.assign(headers, { test: 'value' })); expect(response).toEqual({ success: true }); }); }); diff --git a/services/http/src/services/axiosClient/axiosHttpClient.ts b/services/http/src/services/axiosClient/axiosHttpClient.ts index 74e7d342..90d72b39 100644 --- a/services/http/src/services/axiosClient/axiosHttpClient.ts +++ b/services/http/src/services/axiosClient/axiosHttpClient.ts @@ -15,9 +15,9 @@ export class AxiosHttpClient extends HttpClient { timeout: timeout ?? DEFAULT_REQUEST_TIMEOUT }); - this.requestClient = function(arg: IRequestClientOptions): Promise { - return this.axiosInstance({ url: arg.url, method: arg.method, headers: arg.headers, params: arg.params, data: arg.body }); - }; + this.requestClient = (arg: IRequestClientOptions): Promise => + this.axiosInstance({ url: arg.url, method: arg.method, headers: arg.headers, params: arg.params, data: arg.body }) + .then(response => response.data) } setHeader(key: string, value: string): void { diff --git a/services/http/src/services/fetchClient/fetchHttpClient.ts b/services/http/src/services/fetchClient/fetchHttpClient.ts index 30ceb332..9b9da705 100644 --- a/services/http/src/services/fetchClient/fetchHttpClient.ts +++ b/services/http/src/services/fetchClient/fetchHttpClient.ts @@ -17,7 +17,8 @@ export class FetchHttpClient extends HttpClient { const url = encodeURI(`${this.baseURL}${arg.url}`) + HttpClient.buildQueryString(arg.params); const headers = Object.assign(this.headers, arg.headers); - return fetch(url, { method: arg.method, headers, body: JSON.stringify(arg.body) }) as Promise; + return fetch(url, { method: arg.method, headers, body: JSON.stringify(arg.body) }) + .then(response => response.json()) as Promise; }; } diff --git a/services/http/src/services/httpClient.ts b/services/http/src/services/httpClient.ts index 9ef7f644..7f77830c 100644 --- a/services/http/src/services/httpClient.ts +++ b/services/http/src/services/httpClient.ts @@ -20,7 +20,7 @@ export interface IHttpClient { } export abstract class HttpClient { - abstract requestClient(arg: IRequestClientOptions): Promise; + abstract requestClient: (arg: IRequestClientOptions) => Promise; abstract setHeader(key: string, value: string): void; diff --git a/services/http/src/services/httpRequest.ts b/services/http/src/services/httpRequest.ts index 6c227bf6..c7b881c2 100644 --- a/services/http/src/services/httpRequest.ts +++ b/services/http/src/services/httpRequest.ts @@ -66,11 +66,11 @@ export class HttpRequestWithBody extends HttpRequest { } send(): Promise { - return super.requestClient({ + return this.requestClient({ url: this.urlValue, - method: super.method, - headers: super.headersValue, - params: super.paramsValue, + method: this.method, + headers: this.headersValue, + params: this.paramsValue, body: this.bodyValue }) } diff --git a/services/http/src/temporary/axiosRestController.ts b/services/http/src/temporary/axiosRestController.ts deleted file mode 100644 index 25d463bd..00000000 --- a/services/http/src/temporary/axiosRestController.ts +++ /dev/null @@ -1,162 +0,0 @@ -import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; -import HttpRestController from './restController'; -import { ITokenData, ITokenPayload } from '../types/token.types'; -import { HttpStatusCode } from '../types/httpStatusCode.types'; - -const DEFAULT_REQUEST_TIMEOUT = 60000; - -export class HttpRestControllerAxios extends HttpRestController { - public axiosInstance: AxiosInstance; - - constructor() { - super(); - - this.axiosInstance = axios.create({ - headers: { - 'X-Requested-With': 'XMLHttpRequest', - 'Cache-control': 'no-cache', - Pragma: 'no-cache', - 'Access-Control-Expose-Headers': 'Content-Disposition' - }, - timeout: DEFAULT_REQUEST_TIMEOUT - }); - - this.setupInterceptors(); - } - - private tokenData: ITokenData | null = null; - - private refreshTokenRequest: Promise | null = null; - - private isTokenExpired(): boolean { - const MILLISECONDS_PER_SECOND = 1000; - const expireTime = this.tokenData == null ? null : this.tokenData.expireTime(); - - if (!expireTime) return false; - - return Math.round(new Date().getTime() / MILLISECONDS_PER_SECOND) > expireTime; - } - - private async refreshTokens(): Promise { - if (this.refreshTokenRequest) { - return this.refreshTokenRequest; - } - - if (!this.tokenData?.refreshToken) { - throw new Error('No refresh token available'); - } - - try { - this.refreshTokenRequest = this - .post(this.tokenData.tokenRefreshUrl, { refreshToken: this.tokenData.refreshToken }) - .then(response => { - this.tokenData?.setNewTokenPayload(response.data); - - return response.data.accessToken; - }) - - return this.refreshTokenRequest; - } catch (error) { - this.tokenData?.clearTokens(); - window.location.href = '/login'; - - throw new Error(`Token refresh failed: ${error}`); - } finally { - this.refreshTokenRequest = null; - } - } - - private setupInterceptors(): void { - this.axiosInstance.interceptors.request.use(async request => { - const accessToken = this.tokenData?.accessToken(); - const isExpired = this.isTokenExpired(); - const isNotRefreshUrl = request.url !== this.tokenData?.tokenRefreshUrl; - - if (accessToken && !isExpired) { - request.headers['Authorization'] = `Bearer ${accessToken}`; - } - - if (isExpired && isNotRefreshUrl) { - try { - const newAccessToken = await this.refreshTokens(); - request.headers['Authorization'] = `Bearer ${newAccessToken}`; - } catch (refreshError) { - console.error('Session expired'); - - return Promise.reject(refreshError); - } - } - - return request; - }); - - this.axiosInstance.interceptors.response.use( - response => response, - async (error: AxiosError) => { - const requestConfig = error.config; - const isNotRefreshUrl = error.config?.url !== this.tokenData?.tokenRefreshUrl; - - if (error.response?.status === (HttpStatusCode.UNAUTHORIZED as number) && isNotRefreshUrl) { - try { - const newAccessToken = await this.refreshTokens(); - - if (requestConfig) { - requestConfig.headers['Authorization'] = `Bearer ${newAccessToken}`; - } - - return this.axiosInstance(requestConfig ?? {}); - } catch (refreshError) { - console.error('Session expired'); - - return Promise.reject(refreshError); - } - } - - throw error; - } - ); - } - - setTokenData(tokenData: ITokenData): void { - this.tokenData = tokenData; - }; - - setHeader = (key: string, value: string | null): void => { - this.axiosInstance.defaults.headers[key] = value; - }; - - async get>(url: string, params: object = {}, options: object = {}): Promise { - return this.axiosInstance.get(url, { params, ...options }); - }; - - async post>( - url: string, - body?: object, - params: object = {}, - options: object = {} - ): Promise { - return this.axiosInstance.post(url, body, { params, ...options }); - }; - - async patch>( - url: string, - body?: object, - params: object = {}, - options: object = {} - ): Promise { - return this.axiosInstance.patch(url, body, { params, ...options }); - }; - - async put>( - url: string, - body?: object, - params: object = {}, - options: object = {} - ): Promise { - return this.axiosInstance.put(url, body, { params, ...options }); - } - - async delete>(url: string, params: object = {}, options: object = {}): Promise { - return this.axiosInstance.delete(url, { params, ...options }); - }; -} diff --git a/services/http/src/temporary/fetchRestController.ts b/services/http/src/temporary/fetchRestController.ts deleted file mode 100644 index 2d7d817a..00000000 --- a/services/http/src/temporary/fetchRestController.ts +++ /dev/null @@ -1,78 +0,0 @@ -import HttpRestController from './restController'; -import { Headers, Response } from 'happy-dom'; -import { ITokenData } from '../types/token.types'; - -export class HttpRestControllerFetch extends HttpRestController { - public headers: Headers = new Headers(); - - constructor() { - super(); - } - - setHeader = (key: string, value: string | null): void => { - if (value) { - this.headers.append(key, value); - } else { - this.headers.delete(key); - } - }; - - private getCurrentHeaders() { - const currentHeaders: Record = {}; - this.headers.forEach((h, v) => { - currentHeaders[h] = v; - }); - - return currentHeaders; - } - - private mergeCurrentHeaders(headers: Headers | undefined) { - const collectedHeaders: Record = this.getCurrentHeaders(); - - if (headers) { - headers.forEach((h, v) => { - collectedHeaders[h] = v; - }); - } - - return collectedHeaders; - } - - get = async (url: string, headers?: Headers): Promise => { - return fetch(url, { method: 'GET', headers: this.mergeCurrentHeaders(headers) }) as Promise; - }; - - post = async (url: string, body: object, headers?: Headers): Promise => { - return fetch(url, { - method: 'POST', - body: JSON.stringify(body), - headers: this.mergeCurrentHeaders(headers) - }) as Promise; - }; - - patch = async (url: string, body: object, headers?: Headers): Promise => { - return fetch(url, { - method: 'PATCH', - body: JSON.stringify(body), - headers: this.mergeCurrentHeaders(headers) - }) as Promise; - }; - - put = async (url: string, body: object, headers?: Headers): Promise => { - return fetch(url, { - method: 'PUT', - body: JSON.stringify(body), - headers: this.mergeCurrentHeaders(headers) - }) as Promise; - }; - - delete = async (url: string, body: object = {}, headers?: Headers): Promise => { - return fetch(url, { - method: 'DELETE', - body: JSON.stringify(body), - headers: this.mergeCurrentHeaders(headers) - }) as Promise; - }; - - setTokenData(arg: ITokenData): void {} -} diff --git a/services/http/src/temporary/httpService.ts b/services/http/src/temporary/httpService.ts deleted file mode 100644 index a7afb909..00000000 --- a/services/http/src/temporary/httpService.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { IHttpServiceOptions } from './httpService.types'; -import HttpRestController from './restController'; - -class HttpService { - public restController: RestController | undefined; - - public get: RestController['get']; - public post: RestController['post']; - public patch: RestController['patch']; - public put: RestController['put']; - public delete: RestController['delete']; - public setHeader: RestController['setHeader']; - public setTokenData: RestController['setTokenData']; - - constructor(options: IHttpServiceOptions) { - if (options.restController) { - this.restController = options.restController; - - this.get = this.restController.get; - this.post = this.restController.post; - this.patch = this.restController.patch; - this.put = this.restController.put; - this.delete = this.restController.delete; - this.setHeader = this.restController.setHeader; - this.setTokenData = this.restController.setTokenData; - } else { - this.get = () => Promise.reject('get handler was not specified'); - this.post = () => Promise.reject('post handler was not specified'); - this.patch = () => Promise.reject('patch handler was not specified'); - this.put = () => Promise.reject('put handler was not specified'); - this.delete = () => Promise.reject('delete handler was not specified'); - this.setHeader = () => undefined; - this.setTokenData = () => undefined; - } - } -} - -export default HttpService; diff --git a/services/http/src/temporary/httpService.types.ts b/services/http/src/temporary/httpService.types.ts deleted file mode 100644 index 0173ddde..00000000 --- a/services/http/src/temporary/httpService.types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import HttpRestController from './restController'; - -export interface IHttpServiceOptions { - restController?: RestController; -} diff --git a/services/http/src/temporary/restController.ts b/services/http/src/temporary/restController.ts deleted file mode 100644 index 8d336277..00000000 --- a/services/http/src/temporary/restController.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ITokenData } from '../types/token.types'; - -abstract class HttpRestController { - abstract get(...args: any[]): Promise; - abstract post(...args: any[]): Promise; - abstract patch(...args: any[]): Promise; - abstract put(...args: any[]): Promise; - abstract delete(...args: any[]): Promise; - abstract setTokenData(arg: ITokenData): void; - setHeader: (...args: any[]) => void; - - protected constructor() { - this.setHeader = () => { - console.error('setHeader for HttpRestController is undefined'); - }; - } -} - -export default HttpRestController; From 5af9e9bbb5f98ffe7ff42417a928ba52a02edcce Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Sat, 31 May 2025 18:42:28 +0300 Subject: [PATCH 32/49] feat(http-client): add msw for tests --- package-lock.json | 510 +++++++++++++++++- services/http/package.json | 6 +- services/http/public/mockServiceWorker.js | 307 +++++++++++ .../src/__handlers__/httpClient.handlers.ts | 31 ++ .../src/__tests__/axiosHttpClient.tests.ts | 73 +-- 5 files changed, 872 insertions(+), 55 deletions(-) create mode 100644 services/http/public/mockServiceWorker.js create mode 100644 services/http/src/__handlers__/httpClient.handlers.ts diff --git a/package-lock.json b/package-lock.json index f758a4f3..0659796b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -650,6 +650,63 @@ "node": ">=6.9.0" } }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", + "license": "ISC", + "peer": true, + "dependencies": { + "cookie": "^0.7.2" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "license": "ISC", + "peer": true, + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "license": "ISC", + "peer": true, + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, + "node_modules/@bundled-es-modules/tough-cookie/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@bundled-es-modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/@byndyusoft-ui/css-utilities": { "resolved": "styles/utilities", "link": true @@ -1339,6 +1396,173 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@inquirer/confirm": { + "version": "5.1.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.12.tgz", + "integrity": "sha512-dpq+ielV9/bqgXRUbNH//KsY6WEw9DrGPmipkpmgC1Y46cwuBTNx7PXFWTjc3MQ+urcc0QxoVHcMI0FW4Ok0hg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.13", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz", + "integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@inquirer/core/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "peer": true + }, + "node_modules/@inquirer/core/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/core/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", + "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", + "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "dev": true, @@ -1477,6 +1701,24 @@ "react": ">=16" } }, + "node_modules/@mswjs/interceptors": { + "version": "0.38.7", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.7.tgz", + "integrity": "sha512-Jkb27iSn7JPdkqlTqKfhncFfnEZsIJVYxsFbUSWEkxdIPdsyngrhoDBk0/BGD2FQcRH99vlRrkHpNTyKqI+0/w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -1517,6 +1759,31 @@ "node": ">=12.4.0" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "license": "MIT", + "peer": true + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "license": "MIT", + "peer": true + }, "node_modules/@parcel/watcher": { "version": "2.5.1", "dev": true, @@ -2674,6 +2941,13 @@ "classnames": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT", + "peer": true + }, "node_modules/@types/doctrine": { "version": "0.0.9", "dev": true, @@ -2706,7 +2980,7 @@ }, "node_modules/@types/node": { "version": "20.5.1", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/normalize-package-data": { @@ -2752,6 +3026,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT", + "peer": true + }, "node_modules/@types/uuid": { "version": "9.0.8", "dev": true, @@ -3253,7 +3541,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3261,7 +3548,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3936,9 +4222,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "8.0.1", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -3951,12 +4246,10 @@ }, "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3964,7 +4257,6 @@ }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -3977,7 +4269,6 @@ }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -3993,7 +4284,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4004,7 +4294,6 @@ }, "node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/colord": { @@ -4100,6 +4389,16 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cosmiconfig": { "version": "8.3.6", "dev": true, @@ -4806,7 +5105,6 @@ }, "node_modules/escalade": { "version": "3.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5913,7 +6211,6 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -6178,6 +6475,16 @@ "dev": true, "license": "MIT" }, + "node_modules/graphql": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", + "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/happy-dom": { "version": "17.5.6", "dev": true, @@ -6280,6 +6587,13 @@ "node": ">= 0.4" } }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "license": "MIT", + "peer": true + }, "node_modules/hookified": { "version": "1.9.0", "dev": true, @@ -6803,6 +7117,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "license": "MIT", + "peer": true + }, "node_modules/is-number": { "version": "7.0.0", "dev": true, @@ -7881,6 +8202,74 @@ "dev": true, "license": "MIT" }, + "node_modules/msw": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.8.7.tgz", + "integrity": "sha512-0TGfV4oQiKpa3pDsQBDf0xvFP+sRrqEOnh2n1JWpHVKHJHLv6ZmY1HCZpCi7uDiJTeIHJMBpmBiRmBJN+ETPSQ==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.1", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.38.7", + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.26.1", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "dev": true, @@ -8166,6 +8555,13 @@ "node": ">= 0.8.0" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "license": "MIT", + "peer": true + }, "node_modules/own-keys": { "version": "1.0.1", "dev": true, @@ -8311,6 +8707,13 @@ "dev": true, "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "license": "MIT", + "peer": true + }, "node_modules/path-type": { "version": "4.0.0", "dev": true, @@ -8334,7 +8737,6 @@ }, "node_modules/picocolors": { "version": "1.1.1", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -8580,14 +8982,33 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "peer": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/punycode": { "version": "2.3.1", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT", + "peer": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "dev": true, @@ -8940,7 +9361,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8962,6 +9382,13 @@ "node": ">=0.10.5" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT", + "peer": true + }, "node_modules/resolve": { "version": "1.22.10", "dev": true, @@ -9515,6 +9942,16 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.9.0", "dev": true, @@ -9557,6 +9994,13 @@ } } }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "license": "MIT", + "peer": true + }, "node_modules/string_decoder": { "version": "1.3.0", "dev": true, @@ -9749,7 +10193,6 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -10515,7 +10958,7 @@ }, "node_modules/typescript": { "version": "4.9.5", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -10630,6 +11073,17 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util": { "version": "0.12.5", "dev": true, @@ -11242,7 +11696,6 @@ }, "node_modules/y18n": { "version": "5.0.8", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -11268,7 +11721,6 @@ }, "node_modules/yargs": { "version": "17.7.2", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -11293,12 +11745,10 @@ }, "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/yargs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11306,7 +11756,6 @@ }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11319,7 +11768,6 @@ }, "node_modules/yargs/node_modules/yargs-parser": { "version": "21.1.1", - "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -11344,6 +11792,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/types": { "name": "@byndyusoft-ui/types", "version": "0.3.0", @@ -11357,7 +11818,8 @@ "axios": "^1.9.0" }, "peerDependencies": { - "axios-mock-adapter": "^2.1.0" + "axios-mock-adapter": "^2.1.0", + "msw": "^2.8.7" } }, "services/http-request": { diff --git a/services/http/package.json b/services/http/package.json index 5cc79fdd..64106ad1 100644 --- a/services/http/package.json +++ b/services/http/package.json @@ -39,9 +39,13 @@ "access": "public" }, "peerDependencies": { - "axios-mock-adapter": "^2.1.0" + "axios-mock-adapter": "^2.1.0", + "msw": "^2.8.7" }, "dependencies": { "axios": "^1.9.0" + }, + "msw": { + "workerDirectory": "public" } } diff --git a/services/http/public/mockServiceWorker.js b/services/http/public/mockServiceWorker.js new file mode 100644 index 00000000..84491eb3 --- /dev/null +++ b/services/http/public/mockServiceWorker.js @@ -0,0 +1,307 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.8.7' +const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} diff --git a/services/http/src/__handlers__/httpClient.handlers.ts b/services/http/src/__handlers__/httpClient.handlers.ts new file mode 100644 index 00000000..4d313426 --- /dev/null +++ b/services/http/src/__handlers__/httpClient.handlers.ts @@ -0,0 +1,31 @@ +import { http, HttpResponse } from 'msw'; + +export const getRequest = http.get('https://test-url.com/get', ({ request }) => { + if (request.headers.get('Authorization') !== 'Bearer token' || request.headers.get('Header') !== 'Header value') { + return new HttpResponse(null, { status: 400, statusText: 'Wrong headers' }); + } + + return HttpResponse.json({ success: true }); +}); + +export const getRequestWithQuery = http.get('https://test-url.com/get/with-query', ({ request }) => { + const url = new URL(request.url) + + const testValue = url.searchParams.get('testKey') + + if (!testValue) { + return new HttpResponse(null, { status: 404 }) + } + + return HttpResponse.json({ testKey: testValue }) +}); + +export const postRequest = http.post('https://test-url.com/post', async ({ request }) => { + const newPost = await request.clone().json(); + + if (newPost && request.headers.get('Authorization') === 'Bearer token' && request.headers.get('Header') === 'Header value') { + return HttpResponse.json(newPost); + } + + return new HttpResponse(null, { status: 400, statusText: 'No body' }); +}); diff --git a/services/http/src/__tests__/axiosHttpClient.tests.ts b/services/http/src/__tests__/axiosHttpClient.tests.ts index be587666..7cae9b64 100644 --- a/services/http/src/__tests__/axiosHttpClient.tests.ts +++ b/services/http/src/__tests__/axiosHttpClient.tests.ts @@ -1,52 +1,65 @@ -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; +import { setupServer } from 'msw/node'; +import * as handlers from '../__handlers__/httpClient.handlers'; import AxiosHttpClient from '../services/axiosClient'; -interface IResponseData { - success: boolean; +interface IBody { + bodyKey: string; } -describe('services/AxiosHttpClient', () => { - let mockAxios: MockAdapter; +const requestBody: IBody = { + bodyKey: 'bodyValue' +} + +const baseUrl = 'https://test-url.com'; + +const baseHeaders = { 'Authorization': 'Bearer token' }; - beforeEach(() => { - mockAxios = new MockAdapter(axios); +const optionalHeaders = { Header: 'Header value'}; + +const server = setupServer(); + +describe('services/AxiosHttpClient', () => { + beforeAll(() => { + server.listen({ onUnhandledRequest: 'error' }); }) afterEach(() => { - mockAxios.restore(); + server.resetHandlers(); }); - test('should send POST request with correct headers, body, and URL', async () => { - const baseUrl = 'https://some-url.ru'; - const path = '/test-path'; - const body = { key: 'value' }; - const headers = { 'Authorization': 'Bearer token' }; - const responseData = { success: true }; + afterAll(() => { + server.close(); + }); - mockAxios - .onPost(path) - .reply(200, responseData); + test('should send POST request with correct headers, body, and URL', async () => { + server.use(handlers.postRequest); + const path = '/post'; - const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl, headers: { test: 'value' } }); + const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl, headers: baseHeaders }); const response = await httpClientInstance - .post() + .post() .url(path) - .headers(headers) - .body(body) + .headers(optionalHeaders) + .body(requestBody) .send(); - expect(mockAxios.history.post.length).toBe(1); + expect(response).toEqual(requestBody); + }); + + test('should send GET request with correct query string', async () => { + server.use(handlers.getRequestWithQuery); + const path = '/get/with-query'; + const queryParams = { testKey: 'testValue' }; - const lastRequest = mockAxios.history.post[0]; + const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl }); - console.log(lastRequest.headers); + const response = await httpClientInstance + .get() + .url(path) + .params(queryParams) + .send(); - expect(lastRequest.baseURL).toBe(baseUrl); - expect(lastRequest.url).toBe(path); - expect(lastRequest.data).toEqual(JSON.stringify(body)); - expect(lastRequest.headers).toMatchObject(Object.assign(headers, { test: 'value' })); - expect(response).toEqual({ success: true }); + expect(response).toEqual(queryParams); }); }); From 4315b14d72dd3c50fd1f0fa1b8bcc86a6a71e992 Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Mon, 2 Jun 2025 16:18:39 +0300 Subject: [PATCH 33/49] feat(http-client): fetch http client tests --- .../src/__handlers__/httpClient.handlers.ts | 58 +++++++- .../src/__tests__/axiosHttpClient.tests.ts | 84 ++++++++++-- .../src/__tests__/fetchHttpClient.tests.ts | 129 ++++++++++++++++++ .../services/fetchClient/fetchHttpClient.ts | 2 +- 4 files changed, 259 insertions(+), 14 deletions(-) create mode 100644 services/http/src/__tests__/fetchHttpClient.tests.ts diff --git a/services/http/src/__handlers__/httpClient.handlers.ts b/services/http/src/__handlers__/httpClient.handlers.ts index 4d313426..3d978c98 100644 --- a/services/http/src/__handlers__/httpClient.handlers.ts +++ b/services/http/src/__handlers__/httpClient.handlers.ts @@ -21,11 +21,63 @@ export const getRequestWithQuery = http.get('https://test-url.com/get/with-query }); export const postRequest = http.post('https://test-url.com/post', async ({ request }) => { - const newPost = await request.clone().json(); + const requestBody = await request.clone().json(); - if (newPost && request.headers.get('Authorization') === 'Bearer token' && request.headers.get('Header') === 'Header value') { - return HttpResponse.json(newPost); + if (requestBody && request.headers.get('Authorization') === 'Bearer token' && request.headers.get('Header') === 'Header value') { + return HttpResponse.json(requestBody); } return new HttpResponse(null, { status: 400, statusText: 'No body' }); }); + +export const putRequest = http.put('https://test-url.com/put', async ({ request }) => { + const url = new URL(request.url) + + const testValue = url.searchParams.get('testKey') + + if (!testValue) { + return new HttpResponse(null, { status: 404 }) + } + + const requestBody = await request.clone().json(); + + if (requestBody && request.headers.get('Authorization') === 'Bearer token' && request.headers.get('Header') === 'Header value') { + return HttpResponse.json(requestBody); + } + + return new HttpResponse(null, { status: 400, statusText: 'No body' }); +}); + +export const patchRequest = http.patch('https://test-url.com/patch', async ({ request }) => { + const url = new URL(request.url) + + const testValue = url.searchParams.get('testKey') + + if (!testValue) { + return new HttpResponse(null, { status: 404 }) + } + + const requestBody = await request.clone().json(); + + if (requestBody && request.headers.get('Authorization') === 'Bearer token' && request.headers.get('Header') === 'Header value') { + return HttpResponse.json(requestBody); + } + + return new HttpResponse(null, { status: 400, statusText: 'No body' }); +}); + +export const deleteRequest = http.delete('https://test-url.com/delete', async ({ request }) => { + const url = new URL(request.url) + + const testValue = url.searchParams.get('testKey'); + + if (!testValue) { + return new HttpResponse(null, { status: 404 }); + } + + if (request.headers.get('Authorization') !== 'Bearer token' || request.headers.get('Header') !== 'Header value') { + return new HttpResponse(null, { status: 400, statusText: 'Wrong headers' }); + } + + return new HttpResponse(null, { status: 200 }); +}); diff --git a/services/http/src/__tests__/axiosHttpClient.tests.ts b/services/http/src/__tests__/axiosHttpClient.tests.ts index 7cae9b64..5f57b6dc 100644 --- a/services/http/src/__tests__/axiosHttpClient.tests.ts +++ b/services/http/src/__tests__/axiosHttpClient.tests.ts @@ -10,12 +10,18 @@ const requestBody: IBody = { bodyKey: 'bodyValue' } +interface ISuccessResponse { + success: boolean; +} + const baseUrl = 'https://test-url.com'; const baseHeaders = { 'Authorization': 'Bearer token' }; const optionalHeaders = { Header: 'Header value'}; +const queryParams = { testKey: 'тестовоеЗначение' }; + const server = setupServer(); describe('services/AxiosHttpClient', () => { @@ -31,35 +37,93 @@ describe('services/AxiosHttpClient', () => { server.close(); }); - test('should send POST request with correct headers, body, and URL', async () => { - server.use(handlers.postRequest); - const path = '/post'; + test('should send GET request with correct headers, body, and URL', async () => { + server.use(handlers.getRequest); const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl, headers: baseHeaders }); const response = await httpClientInstance - .post() - .url(path) + .get() + .url('/get') .headers(optionalHeaders) - .body(requestBody) .send(); - expect(response).toEqual(requestBody); + expect(response).toEqual({ success: true }); }); test('should send GET request with correct query string', async () => { server.use(handlers.getRequestWithQuery); - const path = '/get/with-query'; - const queryParams = { testKey: 'testValue' }; const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl }); const response = await httpClientInstance .get() - .url(path) + .url('/get/with-query') .params(queryParams) .send(); expect(response).toEqual(queryParams); }); + + test('should send POST request with correct headers, body, and URL', async () => { + server.use(handlers.postRequest); + + const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + + const response = await httpClientInstance + .post() + .url('/post') + .headers(optionalHeaders) + .body(requestBody) + .send(); + + expect(response).toEqual(requestBody); + }); + + test('should send PUT request with correct headers, body, URL and query string', async () => { + server.use(handlers.putRequest); + + const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + + const response = await httpClientInstance + .put() + .url('/put') + .headers(optionalHeaders) + .params(queryParams) + .body(requestBody) + .send(); + + expect(response).toEqual(requestBody); + }); + + test('should send PATCH request with correct headers, body, URL and query string', async () => { + server.use(handlers.patchRequest); + + const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + + const response = await httpClientInstance + .patch() + .url('/patch') + .headers(optionalHeaders) + .params(queryParams) + .body(requestBody) + .send(); + + expect(response).toEqual(requestBody); + }); + + test('should send DELETE request with correct headers, body, URL and query string', async () => { + server.use(handlers.deleteRequest); + + const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + + const response = await httpClientInstance + .delete() + .url('/delete') + .headers(optionalHeaders) + .params(queryParams) + .send(); + + expect(response).toEqual(''); + }); }); diff --git a/services/http/src/__tests__/fetchHttpClient.tests.ts b/services/http/src/__tests__/fetchHttpClient.tests.ts new file mode 100644 index 00000000..ecc62d4b --- /dev/null +++ b/services/http/src/__tests__/fetchHttpClient.tests.ts @@ -0,0 +1,129 @@ +import { setupServer } from 'msw/node'; +import * as handlers from '../__handlers__/httpClient.handlers'; +import FetchHttpClient from '../services/fetchClient'; + +interface IBody { + bodyKey: string; +} + +const requestBody: IBody = { + bodyKey: 'bodyValue' +} + +interface ISuccessResponse { + success: boolean; +} + +const baseUrl = 'https://test-url.com'; + +const baseHeaders = { 'Authorization': 'Bearer token' }; + +const optionalHeaders = { Header: 'Header value'}; + +const queryParams = { testKey: 'тестовоеЗначение' }; + +const server = setupServer(); + +describe('services/FetchHttpClient', () => { + beforeAll(() => { + server.listen({ onUnhandledRequest: 'error' }); + }) + + afterEach(() => { + server.resetHandlers(); + }); + + afterAll(() => { + server.close(); + }); + + test('should send GET request with correct headers, body, and URL', async () => { + server.use(handlers.getRequest); + + const httpClientInstance = new FetchHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + + const response = await httpClientInstance + .get() + .url('/get') + .headers(optionalHeaders) + .send(); + + expect(response).toEqual({ success: true }); + }); + + test('should send GET request with correct query string', async () => { + server.use(handlers.getRequestWithQuery); + + const httpClientInstance = new FetchHttpClient({ baseURL: baseUrl }); + + const response = await httpClientInstance + .get() + .url('/get/with-query') + .params(queryParams) + .send(); + + expect(response).toEqual(queryParams); + }); + + test('should send POST request with correct headers, body, and URL', async () => { + server.use(handlers.postRequest); + + const httpClientInstance = new FetchHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + + const response = await httpClientInstance + .post() + .url('/post') + .headers(optionalHeaders) + .body(requestBody) + .send(); + + expect(response).toEqual(requestBody); + }); + + test('should send PUT request with correct headers, body, URL and query string', async () => { + server.use(handlers.putRequest); + + const httpClientInstance = new FetchHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + + const response = await httpClientInstance + .put() + .url('/put') + .headers(optionalHeaders) + .params(queryParams) + .body(requestBody) + .send(); + + expect(response).toEqual(requestBody); + }); + + test('should send PATCH request with correct headers, body, URL and query string', async () => { + server.use(handlers.patchRequest); + + const httpClientInstance = new FetchHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + + const response = await httpClientInstance + .patch() + .url('/patch') + .headers(optionalHeaders) + .params(queryParams) + .body(requestBody) + .send(); + + expect(response).toEqual(requestBody); + }); + + test('should send DELETE request with correct headers, body, URL and query string', async () => { + server.use(handlers.deleteRequest); + + const httpClientInstance = new FetchHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + + const response = await httpClientInstance + .delete() + .url('/delete') + .headers(optionalHeaders) + .params(queryParams) + .send(); + + expect(response).toEqual(''); + }); +}); diff --git a/services/http/src/services/fetchClient/fetchHttpClient.ts b/services/http/src/services/fetchClient/fetchHttpClient.ts index 9b9da705..f5ba138c 100644 --- a/services/http/src/services/fetchClient/fetchHttpClient.ts +++ b/services/http/src/services/fetchClient/fetchHttpClient.ts @@ -13,7 +13,7 @@ export class FetchHttpClient extends HttpClient { this.baseURL = baseURL; this.headers = Object.assign(this.headers, headers); this.timeout = timeout ?? DEFAULT_REQUEST_TIMEOUT; - this.requestClient = function(arg: IRequestClientOptions): Promise { + this.requestClient = (arg: IRequestClientOptions): Promise => { const url = encodeURI(`${this.baseURL}${arg.url}`) + HttpClient.buildQueryString(arg.params); const headers = Object.assign(this.headers, arg.headers); From e168143b3d172f4847e0c325b4ecc090caa4492d Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Mon, 2 Jun 2025 18:49:37 +0300 Subject: [PATCH 34/49] feat(http-client): add fixtures --- .../src/__fixtures__/httpClient.fixtures.ts | 21 ++++++++ .../src/__handlers__/httpClient.handlers.ts | 41 +++++++++------ .../src/__tests__/axiosHttpClient.tests.ts | 50 ++++++++----------- .../src/__tests__/fetchHttpClient.tests.ts | 49 ++++++++---------- 4 files changed, 90 insertions(+), 71 deletions(-) create mode 100644 services/http/src/__fixtures__/httpClient.fixtures.ts diff --git a/services/http/src/__fixtures__/httpClient.fixtures.ts b/services/http/src/__fixtures__/httpClient.fixtures.ts new file mode 100644 index 00000000..7cf0eac1 --- /dev/null +++ b/services/http/src/__fixtures__/httpClient.fixtures.ts @@ -0,0 +1,21 @@ +export const requestBody = { + firstParameter: 'firstParameterValue' +} + +export const successResponse = { + success: true +} + +export const baseUrl = 'https://test-url.com'; +export const getPath = '/get'; +export const getPathWithQueryParams = '/get/with-query'; +export const postPath = '/post'; +export const putPath = '/put'; +export const patchPath = '/patch'; +export const deletePath = '/delete'; + +export const baseHeaders = { Authorization: 'Bearer token' }; + +export const optionalHeaders = { Header: 'Header value'}; + +export const queryParams = { testParam: 'тестовоеЗначение' }; diff --git a/services/http/src/__handlers__/httpClient.handlers.ts b/services/http/src/__handlers__/httpClient.handlers.ts index 3d978c98..8dc855ae 100644 --- a/services/http/src/__handlers__/httpClient.handlers.ts +++ b/services/http/src/__handlers__/httpClient.handlers.ts @@ -1,6 +1,15 @@ import { http, HttpResponse } from 'msw'; - -export const getRequest = http.get('https://test-url.com/get', ({ request }) => { +import { + baseUrl, + getPath, + postPath, + putPath, + patchPath, + deletePath, + getPathWithQueryParams +} from '../__fixtures__/httpClient.fixtures'; + +export const getRequest = http.get(`${baseUrl}${getPath}`, ({ request }) => { if (request.headers.get('Authorization') !== 'Bearer token' || request.headers.get('Header') !== 'Header value') { return new HttpResponse(null, { status: 400, statusText: 'Wrong headers' }); } @@ -8,19 +17,19 @@ export const getRequest = http.get('https://test-url.com/get', ({ request }) => return HttpResponse.json({ success: true }); }); -export const getRequestWithQuery = http.get('https://test-url.com/get/with-query', ({ request }) => { +export const getRequestWithQuery = http.get(`${baseUrl}${getPathWithQueryParams}`, ({ request }) => { const url = new URL(request.url) - const testValue = url.searchParams.get('testKey') + const testParamValue = url.searchParams.get('testParam') - if (!testValue) { + if (!testParamValue) { return new HttpResponse(null, { status: 404 }) } - return HttpResponse.json({ testKey: testValue }) + return HttpResponse.json({ testParam: testParamValue }) }); -export const postRequest = http.post('https://test-url.com/post', async ({ request }) => { +export const postRequest = http.post(`${baseUrl}${postPath}`, async ({ request }) => { const requestBody = await request.clone().json(); if (requestBody && request.headers.get('Authorization') === 'Bearer token' && request.headers.get('Header') === 'Header value') { @@ -30,12 +39,12 @@ export const postRequest = http.post('https://test-url.com/post', async ({ reque return new HttpResponse(null, { status: 400, statusText: 'No body' }); }); -export const putRequest = http.put('https://test-url.com/put', async ({ request }) => { +export const putRequest = http.put(`${baseUrl}${putPath}`, async ({ request }) => { const url = new URL(request.url) - const testValue = url.searchParams.get('testKey') + const testParamValue = url.searchParams.get('testParam') - if (!testValue) { + if (!testParamValue) { return new HttpResponse(null, { status: 404 }) } @@ -48,12 +57,12 @@ export const putRequest = http.put('https://test-url.com/put', async ({ request return new HttpResponse(null, { status: 400, statusText: 'No body' }); }); -export const patchRequest = http.patch('https://test-url.com/patch', async ({ request }) => { +export const patchRequest = http.patch(`${baseUrl}${patchPath}`, async ({ request }) => { const url = new URL(request.url) - const testValue = url.searchParams.get('testKey') + const testParamValue = url.searchParams.get('testParam') - if (!testValue) { + if (!testParamValue) { return new HttpResponse(null, { status: 404 }) } @@ -66,12 +75,12 @@ export const patchRequest = http.patch('https://test-url.com/patch', async ({ re return new HttpResponse(null, { status: 400, statusText: 'No body' }); }); -export const deleteRequest = http.delete('https://test-url.com/delete', async ({ request }) => { +export const deleteRequest = http.delete(`${baseUrl}${deletePath}`, async ({ request }) => { const url = new URL(request.url) - const testValue = url.searchParams.get('testKey'); + const testParamValue = url.searchParams.get('testParam'); - if (!testValue) { + if (!testParamValue) { return new HttpResponse(null, { status: 404 }); } diff --git a/services/http/src/__tests__/axiosHttpClient.tests.ts b/services/http/src/__tests__/axiosHttpClient.tests.ts index 5f57b6dc..d892e3f1 100644 --- a/services/http/src/__tests__/axiosHttpClient.tests.ts +++ b/services/http/src/__tests__/axiosHttpClient.tests.ts @@ -1,26 +1,20 @@ import { setupServer } from 'msw/node'; import * as handlers from '../__handlers__/httpClient.handlers'; import AxiosHttpClient from '../services/axiosClient'; - -interface IBody { - bodyKey: string; -} - -const requestBody: IBody = { - bodyKey: 'bodyValue' -} - -interface ISuccessResponse { - success: boolean; -} - -const baseUrl = 'https://test-url.com'; - -const baseHeaders = { 'Authorization': 'Bearer token' }; - -const optionalHeaders = { Header: 'Header value'}; - -const queryParams = { testKey: 'тестовоеЗначение' }; +import { + baseUrl, + baseHeaders, + optionalHeaders, + queryParams, + getPath, + getPathWithQueryParams, + postPath, + putPath, + patchPath, + deletePath, + requestBody, + successResponse +} from '../__fixtures__/httpClient.fixtures'; const server = setupServer(); @@ -43,12 +37,12 @@ describe('services/AxiosHttpClient', () => { const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl, headers: baseHeaders }); const response = await httpClientInstance - .get() - .url('/get') + .get<{}>() + .url(getPath) .headers(optionalHeaders) .send(); - expect(response).toEqual({ success: true }); + expect(response).toEqual(successResponse); }); test('should send GET request with correct query string', async () => { @@ -58,7 +52,7 @@ describe('services/AxiosHttpClient', () => { const response = await httpClientInstance .get() - .url('/get/with-query') + .url(getPathWithQueryParams) .params(queryParams) .send(); @@ -72,7 +66,7 @@ describe('services/AxiosHttpClient', () => { const response = await httpClientInstance .post() - .url('/post') + .url(postPath) .headers(optionalHeaders) .body(requestBody) .send(); @@ -87,7 +81,7 @@ describe('services/AxiosHttpClient', () => { const response = await httpClientInstance .put() - .url('/put') + .url(putPath) .headers(optionalHeaders) .params(queryParams) .body(requestBody) @@ -103,7 +97,7 @@ describe('services/AxiosHttpClient', () => { const response = await httpClientInstance .patch() - .url('/patch') + .url(patchPath) .headers(optionalHeaders) .params(queryParams) .body(requestBody) @@ -119,7 +113,7 @@ describe('services/AxiosHttpClient', () => { const response = await httpClientInstance .delete() - .url('/delete') + .url(deletePath) .headers(optionalHeaders) .params(queryParams) .send(); diff --git a/services/http/src/__tests__/fetchHttpClient.tests.ts b/services/http/src/__tests__/fetchHttpClient.tests.ts index ecc62d4b..aa0fa1fc 100644 --- a/services/http/src/__tests__/fetchHttpClient.tests.ts +++ b/services/http/src/__tests__/fetchHttpClient.tests.ts @@ -2,25 +2,20 @@ import { setupServer } from 'msw/node'; import * as handlers from '../__handlers__/httpClient.handlers'; import FetchHttpClient from '../services/fetchClient'; -interface IBody { - bodyKey: string; -} - -const requestBody: IBody = { - bodyKey: 'bodyValue' -} - -interface ISuccessResponse { - success: boolean; -} - -const baseUrl = 'https://test-url.com'; - -const baseHeaders = { 'Authorization': 'Bearer token' }; - -const optionalHeaders = { Header: 'Header value'}; - -const queryParams = { testKey: 'тестовоеЗначение' }; +import { + baseUrl, + baseHeaders, + optionalHeaders, + queryParams, + getPath, + getPathWithQueryParams, + postPath, + putPath, + patchPath, + deletePath, + requestBody, + successResponse +} from '../__fixtures__/httpClient.fixtures'; const server = setupServer(); @@ -43,12 +38,12 @@ describe('services/FetchHttpClient', () => { const httpClientInstance = new FetchHttpClient({ baseURL: baseUrl, headers: baseHeaders }); const response = await httpClientInstance - .get() - .url('/get') + .get() + .url(getPath) .headers(optionalHeaders) .send(); - expect(response).toEqual({ success: true }); + expect(response).toEqual(successResponse); }); test('should send GET request with correct query string', async () => { @@ -58,7 +53,7 @@ describe('services/FetchHttpClient', () => { const response = await httpClientInstance .get() - .url('/get/with-query') + .url(getPathWithQueryParams) .params(queryParams) .send(); @@ -72,7 +67,7 @@ describe('services/FetchHttpClient', () => { const response = await httpClientInstance .post() - .url('/post') + .url(postPath) .headers(optionalHeaders) .body(requestBody) .send(); @@ -87,7 +82,7 @@ describe('services/FetchHttpClient', () => { const response = await httpClientInstance .put() - .url('/put') + .url(putPath) .headers(optionalHeaders) .params(queryParams) .body(requestBody) @@ -103,7 +98,7 @@ describe('services/FetchHttpClient', () => { const response = await httpClientInstance .patch() - .url('/patch') + .url(patchPath) .headers(optionalHeaders) .params(queryParams) .body(requestBody) @@ -119,7 +114,7 @@ describe('services/FetchHttpClient', () => { const response = await httpClientInstance .delete() - .url('/delete') + .url(deletePath) .headers(optionalHeaders) .params(queryParams) .send(); From f1f3ec0a21bf1c724bf84a230564bde4b0b81de2 Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Tue, 3 Jun 2025 14:35:23 +0300 Subject: [PATCH 35/49] feat(http-client): readme and naming --- services/http/README.md | 126 +++++------------- ...ient.tests.ts => httpClientAxios.tests.ts} | 16 +-- ...ient.tests.ts => httpClientFetch.tests.ts} | 16 +-- services/http/src/index.ts | 4 +- .../http/src/services/axiosClient/index.ts | 2 - .../http/src/services/fetchClient/index.ts | 2 - .../httpClientAxios.ts} | 2 +- .../src/services/httpClientAxios/index.ts | 2 + .../httpClientFetch.ts} | 2 +- .../src/services/httpClientFetch/index.ts | 2 + services/http/src/services/httpRequest.ts | 4 +- 11 files changed, 58 insertions(+), 120 deletions(-) rename services/http/src/__tests__/{axiosHttpClient.tests.ts => httpClientAxios.tests.ts} (86%) rename services/http/src/__tests__/{fetchHttpClient.tests.ts => httpClientFetch.tests.ts} (86%) delete mode 100644 services/http/src/services/axiosClient/index.ts delete mode 100644 services/http/src/services/fetchClient/index.ts rename services/http/src/services/{axiosClient/axiosHttpClient.ts => httpClientAxios/httpClientAxios.ts} (94%) create mode 100644 services/http/src/services/httpClientAxios/index.ts rename services/http/src/services/{fetchClient/fetchHttpClient.ts => httpClientFetch/httpClientFetch.ts} (95%) create mode 100644 services/http/src/services/httpClientFetch/index.ts diff --git a/services/http/README.md b/services/http/README.md index 35b5c183..7247df3e 100644 --- a/services/http/README.md +++ b/services/http/README.md @@ -1,4 +1,4 @@ -# `@byndyusoft-ui/http-service` +# `@byndyusoft-ui/http` > Http service ### Installation @@ -9,109 +9,47 @@ npm i @byndyusoft-ui/http ## Usage -### To start using this service you need to create a new class instance of HttpService and provide restController option. -There are two classes ready to be used as restControllers: **HttpRestControllerFetch** and **HttpRestControllerAxios**. For fetch and axios. -```ts - const restController = new HttpRestControllerFetch(); - const httpService = new HttpService({ - restController - }); - - httpService.get("http://localhost:3000/api/"); -``` +### To start using this service you need to create a new class instance of http client. +There are two classes ready to be used as http clients: **HttpClientAxios** and **HttpClientFetch**. -### Example of usage with HttpRestControllerFetch +### Example of usage with HttpClientAxios ```ts - const restController = new HttpRestControllerFetch(); - const httpService = new HttpService({ - restController - }); - - const handleGetProducts = async (): Promise => { - const products = await httpService - .get('http://localhost:3322/products') - .then(async r => (await r.json()) as IProduct[]); - - setProducts(products); - }; -``` +import HttpClientAxios from '@byndyusoft-ui/http'; -### Example of usage with HttpRestControllerAxios +interface IRequestBody { + name: string; +} -```ts - const restController = new HttpRestControllerAxios(); - const httpService = new HttpService({ - restController - }); +interface IResponse { + message: string; +} - const handleGetProducts = async (): Promise => { - const products = await httpService.get('http://localhost:3322/products').then(r => r.data); +const httpClient = new HttpClientAxios({ baseUr: 'https://base-url.com' }); - setProducts(products); - }; +const postData = async (name: string): Promise => await httpClient + .post() + .url('/post-some-data') + .headers({ Authorization: 'Bearer token'}) + .body({ name }) + .send(); ``` -### You can define own HttpRestController and pass it like this +### Example of usage with HttpClientFetch ```ts - // myOwnHttpRestController.ts - import { HttpRestController } from '@byndyusoft-ui/http'; - - class HttpRestControllerCustom extends HttpRestController { - constructor() { - super(); - } - - get = async (url: string, headers?: Headers): Promise => { - return fetch(url, { method: 'GET', headers: { ...headers } }) as Promise; - }; - - post = async (url: string, body: object, headers?: Headers): Promise => { - return fetch(url, { - method: 'POST', - body: JSON.stringify(body), - headers: { ...headers } - }) as Promise; - }; - - patch = async (url: string, body: object, headers?: Headers): Promise => { - return fetch(url, { - method: 'PATCH', - body: JSON.stringify(body), - headers: { ...headers } - }) as Promise; - }; - - put = async (url: string, body: object, headers?: Headers): Promise => { - return fetch(url, { - method: 'PUT', - body: JSON.stringify(body), - headers: { ...headers } - }) as Promise; - }; - - delete = async (url: string, body: object = {}, headers?: Headers): Promise => { - return fetch(url, { - method: 'DELETE', - body: JSON.stringify(body), - headers: { ...headers } - }) as Promise; - }; + import HttpClientFetch from '@byndyusoft-ui/http'; + + interface IResponse { + message: string; } - - export default HttpRestControllerCustom; -``` -```ts - - // httpService.ts - import HttpRestControllerCustom from './myOwnHttpRestController.ts' - - const restController = new HttpRestControllerCustom(); - const httpService = new HttpService({ - restController - }); - - httpService.get("http://localhost:3000/api/"); + + const httpClient = new HttpClientFetch({ baseUr: 'https://base-url.com' }); + + const getData = async (month: string): Promise => await httpClient + .get() + .url('/get-some-data') + .headers({ Authorization: 'Bearer token'}) + .params({ month }) + .send(); ``` - diff --git a/services/http/src/__tests__/axiosHttpClient.tests.ts b/services/http/src/__tests__/httpClientAxios.tests.ts similarity index 86% rename from services/http/src/__tests__/axiosHttpClient.tests.ts rename to services/http/src/__tests__/httpClientAxios.tests.ts index d892e3f1..4bcbfb78 100644 --- a/services/http/src/__tests__/axiosHttpClient.tests.ts +++ b/services/http/src/__tests__/httpClientAxios.tests.ts @@ -1,6 +1,6 @@ import { setupServer } from 'msw/node'; import * as handlers from '../__handlers__/httpClient.handlers'; -import AxiosHttpClient from '../services/axiosClient'; +import HttpClientAxios from '../services/httpClientAxios'; import { baseUrl, baseHeaders, @@ -18,7 +18,7 @@ import { const server = setupServer(); -describe('services/AxiosHttpClient', () => { +describe('services/HttpClientAxios', () => { beforeAll(() => { server.listen({ onUnhandledRequest: 'error' }); }) @@ -34,7 +34,7 @@ describe('services/AxiosHttpClient', () => { test('should send GET request with correct headers, body, and URL', async () => { server.use(handlers.getRequest); - const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + const httpClientInstance = new HttpClientAxios({ baseURL: baseUrl, headers: baseHeaders }); const response = await httpClientInstance .get<{}>() @@ -48,7 +48,7 @@ describe('services/AxiosHttpClient', () => { test('should send GET request with correct query string', async () => { server.use(handlers.getRequestWithQuery); - const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl }); + const httpClientInstance = new HttpClientAxios({ baseURL: baseUrl }); const response = await httpClientInstance .get() @@ -62,7 +62,7 @@ describe('services/AxiosHttpClient', () => { test('should send POST request with correct headers, body, and URL', async () => { server.use(handlers.postRequest); - const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + const httpClientInstance = new HttpClientAxios({ baseURL: baseUrl, headers: baseHeaders }); const response = await httpClientInstance .post() @@ -77,7 +77,7 @@ describe('services/AxiosHttpClient', () => { test('should send PUT request with correct headers, body, URL and query string', async () => { server.use(handlers.putRequest); - const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + const httpClientInstance = new HttpClientAxios({ baseURL: baseUrl, headers: baseHeaders }); const response = await httpClientInstance .put() @@ -93,7 +93,7 @@ describe('services/AxiosHttpClient', () => { test('should send PATCH request with correct headers, body, URL and query string', async () => { server.use(handlers.patchRequest); - const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + const httpClientInstance = new HttpClientAxios({ baseURL: baseUrl, headers: baseHeaders }); const response = await httpClientInstance .patch() @@ -109,7 +109,7 @@ describe('services/AxiosHttpClient', () => { test('should send DELETE request with correct headers, body, URL and query string', async () => { server.use(handlers.deleteRequest); - const httpClientInstance = new AxiosHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + const httpClientInstance = new HttpClientAxios({ baseURL: baseUrl, headers: baseHeaders }); const response = await httpClientInstance .delete() diff --git a/services/http/src/__tests__/fetchHttpClient.tests.ts b/services/http/src/__tests__/httpClientFetch.tests.ts similarity index 86% rename from services/http/src/__tests__/fetchHttpClient.tests.ts rename to services/http/src/__tests__/httpClientFetch.tests.ts index aa0fa1fc..49d94b62 100644 --- a/services/http/src/__tests__/fetchHttpClient.tests.ts +++ b/services/http/src/__tests__/httpClientFetch.tests.ts @@ -1,6 +1,6 @@ import { setupServer } from 'msw/node'; import * as handlers from '../__handlers__/httpClient.handlers'; -import FetchHttpClient from '../services/fetchClient'; +import HttpClientFetch from '../services/httpClientFetch'; import { baseUrl, @@ -19,7 +19,7 @@ import { const server = setupServer(); -describe('services/FetchHttpClient', () => { +describe('services/HttpClientFetch', () => { beforeAll(() => { server.listen({ onUnhandledRequest: 'error' }); }) @@ -35,7 +35,7 @@ describe('services/FetchHttpClient', () => { test('should send GET request with correct headers, body, and URL', async () => { server.use(handlers.getRequest); - const httpClientInstance = new FetchHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + const httpClientInstance = new HttpClientFetch({ baseURL: baseUrl, headers: baseHeaders }); const response = await httpClientInstance .get() @@ -49,7 +49,7 @@ describe('services/FetchHttpClient', () => { test('should send GET request with correct query string', async () => { server.use(handlers.getRequestWithQuery); - const httpClientInstance = new FetchHttpClient({ baseURL: baseUrl }); + const httpClientInstance = new HttpClientFetch({ baseURL: baseUrl }); const response = await httpClientInstance .get() @@ -63,7 +63,7 @@ describe('services/FetchHttpClient', () => { test('should send POST request with correct headers, body, and URL', async () => { server.use(handlers.postRequest); - const httpClientInstance = new FetchHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + const httpClientInstance = new HttpClientFetch({ baseURL: baseUrl, headers: baseHeaders }); const response = await httpClientInstance .post() @@ -78,7 +78,7 @@ describe('services/FetchHttpClient', () => { test('should send PUT request with correct headers, body, URL and query string', async () => { server.use(handlers.putRequest); - const httpClientInstance = new FetchHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + const httpClientInstance = new HttpClientFetch({ baseURL: baseUrl, headers: baseHeaders }); const response = await httpClientInstance .put() @@ -94,7 +94,7 @@ describe('services/FetchHttpClient', () => { test('should send PATCH request with correct headers, body, URL and query string', async () => { server.use(handlers.patchRequest); - const httpClientInstance = new FetchHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + const httpClientInstance = new HttpClientFetch({ baseURL: baseUrl, headers: baseHeaders }); const response = await httpClientInstance .patch() @@ -110,7 +110,7 @@ describe('services/FetchHttpClient', () => { test('should send DELETE request with correct headers, body, URL and query string', async () => { server.use(handlers.deleteRequest); - const httpClientInstance = new FetchHttpClient({ baseURL: baseUrl, headers: baseHeaders }); + const httpClientInstance = new HttpClientFetch({ baseURL: baseUrl, headers: baseHeaders }); const response = await httpClientInstance .delete() diff --git a/services/http/src/index.ts b/services/http/src/index.ts index 28fd6628..a2352bf7 100644 --- a/services/http/src/index.ts +++ b/services/http/src/index.ts @@ -1,3 +1,3 @@ -export { default as AxiosClient } from './services/axiosClient'; -export { default as FetchClient } from './services/fetchClient'; +export { default as HttpClientAxios } from './services/httpClientAxios'; +export { default as HttpClientFetch } from './services/httpClientFetch'; export { HttpStatusCode } from './types/httpStatusCode.types'; diff --git a/services/http/src/services/axiosClient/index.ts b/services/http/src/services/axiosClient/index.ts deleted file mode 100644 index d6f1c403..00000000 --- a/services/http/src/services/axiosClient/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { AxiosHttpClient as AxiosClient } from './axiosHttpClient'; -export default AxiosClient; diff --git a/services/http/src/services/fetchClient/index.ts b/services/http/src/services/fetchClient/index.ts deleted file mode 100644 index ab64bf6e..00000000 --- a/services/http/src/services/fetchClient/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { FetchHttpClient as FetchClient } from './fetchHttpClient'; -export default FetchClient; diff --git a/services/http/src/services/axiosClient/axiosHttpClient.ts b/services/http/src/services/httpClientAxios/httpClientAxios.ts similarity index 94% rename from services/http/src/services/axiosClient/axiosHttpClient.ts rename to services/http/src/services/httpClientAxios/httpClientAxios.ts index 90d72b39..6ddbd2d8 100644 --- a/services/http/src/services/axiosClient/axiosHttpClient.ts +++ b/services/http/src/services/httpClientAxios/httpClientAxios.ts @@ -2,7 +2,7 @@ import axios, { AxiosInstance } from 'axios'; import { HttpClient, IHttpClientInit, DEFAULT_REQUEST_TIMEOUT } from '../httpClient'; import { IRequestClientOptions } from '../httpRequest'; -export class AxiosHttpClient extends HttpClient { +export class HttpClientAxios extends HttpClient { requestClient; private axiosInstance: AxiosInstance; diff --git a/services/http/src/services/httpClientAxios/index.ts b/services/http/src/services/httpClientAxios/index.ts new file mode 100644 index 00000000..79f55261 --- /dev/null +++ b/services/http/src/services/httpClientAxios/index.ts @@ -0,0 +1,2 @@ +import { HttpClientAxios as AxiosClient } from './httpClientAxios'; +export default AxiosClient; diff --git a/services/http/src/services/fetchClient/fetchHttpClient.ts b/services/http/src/services/httpClientFetch/httpClientFetch.ts similarity index 95% rename from services/http/src/services/fetchClient/fetchHttpClient.ts rename to services/http/src/services/httpClientFetch/httpClientFetch.ts index f5ba138c..e27253ca 100644 --- a/services/http/src/services/fetchClient/fetchHttpClient.ts +++ b/services/http/src/services/httpClientFetch/httpClientFetch.ts @@ -1,7 +1,7 @@ import { IHeaders, IRequestClientOptions } from '../httpRequest'; import { DEFAULT_REQUEST_TIMEOUT, HttpClient, IHttpClientInit } from '../httpClient'; -export class FetchHttpClient extends HttpClient { +export class HttpClientFetch extends HttpClient { baseURL: string; headers: IHeaders = {}; timeout: number; diff --git a/services/http/src/services/httpClientFetch/index.ts b/services/http/src/services/httpClientFetch/index.ts new file mode 100644 index 00000000..3e5275f6 --- /dev/null +++ b/services/http/src/services/httpClientFetch/index.ts @@ -0,0 +1,2 @@ +import { HttpClientFetch as FetchClient } from './httpClientFetch'; +export default FetchClient; diff --git a/services/http/src/services/httpRequest.ts b/services/http/src/services/httpRequest.ts index c7b881c2..af387ddd 100644 --- a/services/http/src/services/httpRequest.ts +++ b/services/http/src/services/httpRequest.ts @@ -57,9 +57,9 @@ export class HttpRequest { } export class HttpRequestWithBody extends HttpRequest { - private bodyValue?: Object = {}; + private bodyValue?: object = {}; - body(body: Object): this { + body(body: B): this { this.bodyValue = body; return this; From f0719453f83f372388a2976353f1e625a2fc63ca Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Tue, 3 Jun 2025 16:04:20 +0300 Subject: [PATCH 36/49] feat(http-client): first attemp to response types --- .../src/__tests__/httpClientAxios.tests.ts | 15 ++++++----- services/http/src/services/httpClient.ts | 17 +++--------- .../httpClientAxios/httpClientAxios.ts | 14 +++++++--- .../httpClientFetch/httpClientFetch.ts | 5 ++-- services/http/src/services/httpRequest.ts | 19 +++++--------- services/http/src/types/httpClient.types.ts | 26 +++++++++++++++++++ 6 files changed, 58 insertions(+), 38 deletions(-) create mode 100644 services/http/src/types/httpClient.types.ts diff --git a/services/http/src/__tests__/httpClientAxios.tests.ts b/services/http/src/__tests__/httpClientAxios.tests.ts index 4bcbfb78..7d8c3270 100644 --- a/services/http/src/__tests__/httpClientAxios.tests.ts +++ b/services/http/src/__tests__/httpClientAxios.tests.ts @@ -15,6 +15,7 @@ import { requestBody, successResponse } from '../__fixtures__/httpClient.fixtures'; +import { HttpStatusCode } from '../types/httpStatusCode.types'; const server = setupServer(); @@ -37,12 +38,12 @@ describe('services/HttpClientAxios', () => { const httpClientInstance = new HttpClientAxios({ baseURL: baseUrl, headers: baseHeaders }); const response = await httpClientInstance - .get<{}>() + .get() .url(getPath) .headers(optionalHeaders) .send(); - expect(response).toEqual(successResponse); + expect(response.data).toEqual(successResponse); }); test('should send GET request with correct query string', async () => { @@ -56,7 +57,7 @@ describe('services/HttpClientAxios', () => { .params(queryParams) .send(); - expect(response).toEqual(queryParams); + expect(response.data).toEqual(queryParams); }); test('should send POST request with correct headers, body, and URL', async () => { @@ -71,7 +72,7 @@ describe('services/HttpClientAxios', () => { .body(requestBody) .send(); - expect(response).toEqual(requestBody); + expect(response.data).toEqual(requestBody); }); test('should send PUT request with correct headers, body, URL and query string', async () => { @@ -87,7 +88,7 @@ describe('services/HttpClientAxios', () => { .body(requestBody) .send(); - expect(response).toEqual(requestBody); + expect(response.data).toEqual(requestBody); }); test('should send PATCH request with correct headers, body, URL and query string', async () => { @@ -103,7 +104,7 @@ describe('services/HttpClientAxios', () => { .body(requestBody) .send(); - expect(response).toEqual(requestBody); + expect(response.data).toEqual(requestBody); }); test('should send DELETE request with correct headers, body, URL and query string', async () => { @@ -118,6 +119,6 @@ describe('services/HttpClientAxios', () => { .params(queryParams) .send(); - expect(response).toEqual(''); + expect(response.status).toEqual(HttpStatusCode.OK); }); }); diff --git a/services/http/src/services/httpClient.ts b/services/http/src/services/httpClient.ts index 7f77830c..20f11729 100644 --- a/services/http/src/services/httpClient.ts +++ b/services/http/src/services/httpClient.ts @@ -1,26 +1,17 @@ -import { HttpRequest, HttpRequestWithBody, IRequestClientOptions, IHeaders, TQueryParams } from './httpRequest'; +import { HttpRequest, HttpRequestWithBody, IRequestClientOptions } from './httpRequest'; import { HttpMethod } from '../types/httpMethod.types'; +import { IHttpClientResponse, THeaders, TQueryParams } from '../types/httpClient.types'; export const DEFAULT_REQUEST_TIMEOUT = 60000; export interface IHttpClientInit { baseURL: string; - headers?: IHeaders; + headers?: THeaders; timeout?: number; } -export interface IHttpClient { - requestClient(arg: IRequestClientOptions): Promise; - setHeader(key: string, value: string): void; - get(): HttpRequest; - post(): HttpRequestWithBody; - put(): HttpRequestWithBody; - patch(): HttpRequestWithBody; - delete(): HttpRequest; -} - export abstract class HttpClient { - abstract requestClient: (arg: IRequestClientOptions) => Promise; + abstract requestClient: (arg: IRequestClientOptions) => Promise>; abstract setHeader(key: string, value: string): void; diff --git a/services/http/src/services/httpClientAxios/httpClientAxios.ts b/services/http/src/services/httpClientAxios/httpClientAxios.ts index 6ddbd2d8..b615e11b 100644 --- a/services/http/src/services/httpClientAxios/httpClientAxios.ts +++ b/services/http/src/services/httpClientAxios/httpClientAxios.ts @@ -1,6 +1,7 @@ -import axios, { AxiosInstance } from 'axios'; +import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; import { HttpClient, IHttpClientInit, DEFAULT_REQUEST_TIMEOUT } from '../httpClient'; import { IRequestClientOptions } from '../httpRequest'; +import { IHttpClientResponse, THeaders } from '../../types/httpClient.types'; export class HttpClientAxios extends HttpClient { requestClient; @@ -15,9 +16,14 @@ export class HttpClientAxios extends HttpClient { timeout: timeout ?? DEFAULT_REQUEST_TIMEOUT }); - this.requestClient = (arg: IRequestClientOptions): Promise => - this.axiosInstance({ url: arg.url, method: arg.method, headers: arg.headers, params: arg.params, data: arg.body }) - .then(response => response.data) + this.requestClient = (arg: IRequestClientOptions): Promise> => + this.axiosInstance({ url: arg.url, method: arg.method, headers: arg.headers, params: arg.params, data: arg.body }) + .then(response => ({ + data: response.data, + status: response.status, + statusText: response.statusText, + headers: response.headers as THeaders + })); } setHeader(key: string, value: string): void { diff --git a/services/http/src/services/httpClientFetch/httpClientFetch.ts b/services/http/src/services/httpClientFetch/httpClientFetch.ts index e27253ca..b2b54eba 100644 --- a/services/http/src/services/httpClientFetch/httpClientFetch.ts +++ b/services/http/src/services/httpClientFetch/httpClientFetch.ts @@ -1,9 +1,10 @@ -import { IHeaders, IRequestClientOptions } from '../httpRequest'; +import { IRequestClientOptions } from '../httpRequest'; import { DEFAULT_REQUEST_TIMEOUT, HttpClient, IHttpClientInit } from '../httpClient'; +import { THeaders } from '../../types/httpClient.types'; export class HttpClientFetch extends HttpClient { baseURL: string; - headers: IHeaders = {}; + headers: THeaders = {}; timeout: number; requestClient; diff --git a/services/http/src/services/httpRequest.ts b/services/http/src/services/httpRequest.ts index af387ddd..07b4b95e 100644 --- a/services/http/src/services/httpRequest.ts +++ b/services/http/src/services/httpRequest.ts @@ -1,26 +1,21 @@ import { HttpMethod } from '../types/httpMethod.types'; - -export type IHeaders = Record; - -export type TQueryParamValue = string | number | boolean; - -export type TQueryParams = Record; +import { IHttpClientResponse, THeaders, TQueryParams } from '../types/httpClient.types'; export interface IRequestClientOptions { url: string; method: HttpMethod; - headers: IHeaders; + headers: THeaders; params: TQueryParams; body?: Object; } -export type TRequestClient = (arg: IRequestClientOptions) => Promise; +export type TRequestClient = (arg: IRequestClientOptions) => Promise>; export class HttpRequest { protected requestClient: TRequestClient; protected urlValue: string = ''; protected method: HttpMethod; - protected headersValue: IHeaders = {}; + protected headersValue: THeaders = {}; protected paramsValue: TQueryParams = {}; constructor(requestClient: TRequestClient, method: HttpMethod) { @@ -34,7 +29,7 @@ export class HttpRequest { return this; } - headers(headers: IHeaders): this { + headers(headers: THeaders): this { Object.assign(this.headersValue, headers); return this; @@ -46,7 +41,7 @@ export class HttpRequest { return this; } - send(): Promise { + send(): Promise> { return this.requestClient({ url: this.urlValue, method: this.method, @@ -65,7 +60,7 @@ export class HttpRequestWithBody extends HttpRequest { return this; } - send(): Promise { + send(): Promise> { return this.requestClient({ url: this.urlValue, method: this.method, diff --git a/services/http/src/types/httpClient.types.ts b/services/http/src/types/httpClient.types.ts new file mode 100644 index 00000000..e7c4a218 --- /dev/null +++ b/services/http/src/types/httpClient.types.ts @@ -0,0 +1,26 @@ +import { HttpStatusCode } from './httpStatusCode.types'; + +export type THeaders = Record; + +export type TQueryParamValue = string | number | boolean; + +export type TQueryParams = Record; + +export interface IRequestConfig { + url?: string; + method?: string; + baseURL?: string; + headers?: THeaders; + params?: TQueryParams; + data?: D; +} + +export interface IHttpClientResponse { + data: T; + status: HttpStatusCode; + statusText: string; + headers: THeaders; + // config?: IRequestConfig; +} + +export interface IHttpClientError {} \ No newline at end of file From dbc019012dbc899ee2e1832209063777269b3909 Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Tue, 3 Jun 2025 17:46:03 +0300 Subject: [PATCH 37/49] feat(http-client): add response type to http-client-fetch --- .../src/__tests__/httpClientFetch.tests.ts | 13 ++++---- .../httpClientAxios/httpClientAxios.ts | 4 +-- .../httpClientFetch/httpClientFetch.ts | 30 ++++++++++++++++--- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/services/http/src/__tests__/httpClientFetch.tests.ts b/services/http/src/__tests__/httpClientFetch.tests.ts index 49d94b62..2367b88b 100644 --- a/services/http/src/__tests__/httpClientFetch.tests.ts +++ b/services/http/src/__tests__/httpClientFetch.tests.ts @@ -16,6 +16,7 @@ import { requestBody, successResponse } from '../__fixtures__/httpClient.fixtures'; +import { HttpStatusCode } from '../types/httpStatusCode.types'; const server = setupServer(); @@ -43,7 +44,7 @@ describe('services/HttpClientFetch', () => { .headers(optionalHeaders) .send(); - expect(response).toEqual(successResponse); + expect(response.data).toEqual(successResponse); }); test('should send GET request with correct query string', async () => { @@ -57,7 +58,7 @@ describe('services/HttpClientFetch', () => { .params(queryParams) .send(); - expect(response).toEqual(queryParams); + expect(response.data).toEqual(queryParams); }); test('should send POST request with correct headers, body, and URL', async () => { @@ -72,7 +73,7 @@ describe('services/HttpClientFetch', () => { .body(requestBody) .send(); - expect(response).toEqual(requestBody); + expect(response.data).toEqual(requestBody); }); test('should send PUT request with correct headers, body, URL and query string', async () => { @@ -88,7 +89,7 @@ describe('services/HttpClientFetch', () => { .body(requestBody) .send(); - expect(response).toEqual(requestBody); + expect(response.data).toEqual(requestBody); }); test('should send PATCH request with correct headers, body, URL and query string', async () => { @@ -104,7 +105,7 @@ describe('services/HttpClientFetch', () => { .body(requestBody) .send(); - expect(response).toEqual(requestBody); + expect(response.data).toEqual(requestBody); }); test('should send DELETE request with correct headers, body, URL and query string', async () => { @@ -119,6 +120,6 @@ describe('services/HttpClientFetch', () => { .params(queryParams) .send(); - expect(response).toEqual(''); + expect(response.status).toEqual(HttpStatusCode.OK); }); }); diff --git a/services/http/src/services/httpClientAxios/httpClientAxios.ts b/services/http/src/services/httpClientAxios/httpClientAxios.ts index b615e11b..ff232d55 100644 --- a/services/http/src/services/httpClientAxios/httpClientAxios.ts +++ b/services/http/src/services/httpClientAxios/httpClientAxios.ts @@ -1,4 +1,4 @@ -import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; +import axios, { AxiosInstance, AxiosError } from 'axios'; import { HttpClient, IHttpClientInit, DEFAULT_REQUEST_TIMEOUT } from '../httpClient'; import { IRequestClientOptions } from '../httpRequest'; import { IHttpClientResponse, THeaders } from '../../types/httpClient.types'; @@ -22,7 +22,7 @@ export class HttpClientAxios extends HttpClient { data: response.data, status: response.status, statusText: response.statusText, - headers: response.headers as THeaders + headers: Object.fromEntries(response.headers.entries()) })); } diff --git a/services/http/src/services/httpClientFetch/httpClientFetch.ts b/services/http/src/services/httpClientFetch/httpClientFetch.ts index b2b54eba..59c24311 100644 --- a/services/http/src/services/httpClientFetch/httpClientFetch.ts +++ b/services/http/src/services/httpClientFetch/httpClientFetch.ts @@ -1,6 +1,6 @@ import { IRequestClientOptions } from '../httpRequest'; import { DEFAULT_REQUEST_TIMEOUT, HttpClient, IHttpClientInit } from '../httpClient'; -import { THeaders } from '../../types/httpClient.types'; +import { THeaders, IHttpClientResponse } from '../../types/httpClient.types'; export class HttpClientFetch extends HttpClient { baseURL: string; @@ -14,12 +14,34 @@ export class HttpClientFetch extends HttpClient { this.baseURL = baseURL; this.headers = Object.assign(this.headers, headers); this.timeout = timeout ?? DEFAULT_REQUEST_TIMEOUT; - this.requestClient = (arg: IRequestClientOptions): Promise => { + this.requestClient = async (arg: IRequestClientOptions): Promise> => { const url = encodeURI(`${this.baseURL}${arg.url}`) + HttpClient.buildQueryString(arg.params); const headers = Object.assign(this.headers, arg.headers); - return fetch(url, { method: arg.method, headers, body: JSON.stringify(arg.body) }) - .then(response => response.json()) as Promise; + const response = await fetch(url, { method: arg.method, headers, body: JSON.stringify(arg.body) }); + + if (!response.ok) { + throw new Error(`HTTP error! Code: ${response.status}`); + } + + const contentType = response.headers.get('Content-Type'); + + let data: R; + + if (contentType?.includes('application/json')) { + data = await response.json(); + } else if (contentType?.includes('text/')) { + data = (await response.text()) as unknown as R; + } else { + data = (await response.blob()) as unknown as R; + } + + return { + data, + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + }; }; } From 966216c1e4b450d073a490b818f560bdd9ca81cd Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Tue, 3 Jun 2025 18:19:06 +0300 Subject: [PATCH 38/49] feat(http-client): http client error class --- services/http/package.json | 1 - services/http/src/types/httpClient.types.ts | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/services/http/package.json b/services/http/package.json index 64106ad1..31493490 100644 --- a/services/http/package.json +++ b/services/http/package.json @@ -39,7 +39,6 @@ "access": "public" }, "peerDependencies": { - "axios-mock-adapter": "^2.1.0", "msw": "^2.8.7" }, "dependencies": { diff --git a/services/http/src/types/httpClient.types.ts b/services/http/src/types/httpClient.types.ts index e7c4a218..9cf4dee3 100644 --- a/services/http/src/types/httpClient.types.ts +++ b/services/http/src/types/httpClient.types.ts @@ -23,4 +23,20 @@ export interface IHttpClientResponse { // config?: IRequestConfig; } -export interface IHttpClientError {} \ No newline at end of file +export class HttpClientError extends Error { + code?: string; + response?: IHttpClientResponse; + // config?: IRequestConfig + + constructor(args: { + message?: string; + code?: string; + response?: IHttpClientResponse; + // config?: IRequestConfig; + }) { + super(args.message); + + this.code = args.code; + this.response = args.response; + }; +} From e603b41962a750337d13f0f94698417eca93765c Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Wed, 4 Jun 2025 13:46:05 +0300 Subject: [PATCH 39/49] feat(http-client): client error type --- .../src/__fixtures__/httpClient.fixtures.ts | 3 ++ .../src/__handlers__/httpClient.handlers.ts | 25 ++++++++++++---- .../src/__tests__/httpClientAxios.tests.ts | 25 +++++++++++++++- .../src/__tests__/httpClientFetch.tests.ts | 24 ++++++++++++++- .../httpClientAxios/httpClientAxios.ts | 19 ++++++++++-- .../httpClientFetch/httpClientFetch.ts | 29 ++++++++++++------- services/http/src/types/httpClient.types.ts | 5 ++-- 7 files changed, 107 insertions(+), 23 deletions(-) diff --git a/services/http/src/__fixtures__/httpClient.fixtures.ts b/services/http/src/__fixtures__/httpClient.fixtures.ts index 7cf0eac1..e5a769c3 100644 --- a/services/http/src/__fixtures__/httpClient.fixtures.ts +++ b/services/http/src/__fixtures__/httpClient.fixtures.ts @@ -9,6 +9,7 @@ export const successResponse = { export const baseUrl = 'https://test-url.com'; export const getPath = '/get'; export const getPathWithQueryParams = '/get/with-query'; +export const getPathWithError = '/get/with-error'; export const postPath = '/post'; export const putPath = '/put'; export const patchPath = '/patch'; @@ -19,3 +20,5 @@ export const baseHeaders = { Authorization: 'Bearer token' }; export const optionalHeaders = { Header: 'Header value'}; export const queryParams = { testParam: 'тестовоеЗначение' }; + +export const errorDetails = { detail_1: 'description_1', detail_2: 'description_2' }; diff --git a/services/http/src/__handlers__/httpClient.handlers.ts b/services/http/src/__handlers__/httpClient.handlers.ts index 8dc855ae..0e520076 100644 --- a/services/http/src/__handlers__/httpClient.handlers.ts +++ b/services/http/src/__handlers__/httpClient.handlers.ts @@ -6,7 +6,10 @@ import { putPath, patchPath, deletePath, - getPathWithQueryParams + getPathWithQueryParams, + getPathWithError, + queryParams, + errorDetails } from '../__fixtures__/httpClient.fixtures'; export const getRequest = http.get(`${baseUrl}${getPath}`, ({ request }) => { @@ -18,15 +21,27 @@ export const getRequest = http.get(`${baseUrl}${getPath}`, ({ request }) => { }); export const getRequestWithQuery = http.get(`${baseUrl}${getPathWithQueryParams}`, ({ request }) => { - const url = new URL(request.url) + const url = new URL(request.url); - const testParamValue = url.searchParams.get('testParam') + const testParamValue = url.searchParams.get('testParam'); if (!testParamValue) { - return new HttpResponse(null, { status: 404 }) + return new HttpResponse(null, { status: 404 }); + } + + return HttpResponse.json({ testParam: testParamValue }); +}); + +export const getRequestWithError = http.get(`${baseUrl}${getPathWithError}`, ({ request }) => { + const url = new URL(request.url); + + const testParamValue = url.searchParams.get('testParam'); + + if (testParamValue === queryParams['testParam']) { + return new HttpResponse(JSON.stringify(errorDetails), { status: 400, headers: { 'content-type': 'application/json' } }); } - return HttpResponse.json({ testParam: testParamValue }) + return HttpResponse.json({ testParam: testParamValue }); }); export const postRequest = http.post(`${baseUrl}${postPath}`, async ({ request }) => { diff --git a/services/http/src/__tests__/httpClientAxios.tests.ts b/services/http/src/__tests__/httpClientAxios.tests.ts index 7d8c3270..d0ce5f9e 100644 --- a/services/http/src/__tests__/httpClientAxios.tests.ts +++ b/services/http/src/__tests__/httpClientAxios.tests.ts @@ -13,9 +13,12 @@ import { patchPath, deletePath, requestBody, - successResponse + successResponse, + getPathWithError, + errorDetails } from '../__fixtures__/httpClient.fixtures'; import { HttpStatusCode } from '../types/httpStatusCode.types'; +import { HttpClientError } from '../types/httpClient.types'; const server = setupServer(); @@ -60,6 +63,26 @@ describe('services/HttpClientAxios', () => { expect(response.data).toEqual(queryParams); }); + test('should get correct error on GET request', async () => { + server.use(handlers.getRequestWithError); + + const httpClientInstance = new HttpClientAxios({ baseURL: baseUrl }); + + try { + await httpClientInstance + .get() + .url(getPathWithError) + .params(queryParams) + .send(); + } catch (error) { + const clientError = error as HttpClientError; + + expect(clientError.code).toBe('ERR_BAD_REQUEST'); + expect(clientError.response?.status).toBe(HttpStatusCode.BAD_REQUEST); + expect(clientError.response?.data).toEqual(errorDetails); + } + }); + test('should send POST request with correct headers, body, and URL', async () => { server.use(handlers.postRequest); diff --git a/services/http/src/__tests__/httpClientFetch.tests.ts b/services/http/src/__tests__/httpClientFetch.tests.ts index 2367b88b..5f1436e9 100644 --- a/services/http/src/__tests__/httpClientFetch.tests.ts +++ b/services/http/src/__tests__/httpClientFetch.tests.ts @@ -9,14 +9,16 @@ import { queryParams, getPath, getPathWithQueryParams, + getPathWithError, postPath, putPath, patchPath, deletePath, requestBody, - successResponse + successResponse, errorDetails } from '../__fixtures__/httpClient.fixtures'; import { HttpStatusCode } from '../types/httpStatusCode.types'; +import { HttpClientError } from '../types/httpClient.types'; const server = setupServer(); @@ -61,6 +63,26 @@ describe('services/HttpClientFetch', () => { expect(response.data).toEqual(queryParams); }); + test('should get correct error on GET request', async () => { + server.use(handlers.getRequestWithError); + + const httpClientInstance = new HttpClientFetch({ baseURL: baseUrl }); + + try { + await httpClientInstance + .get() + .url(getPathWithError) + .params(queryParams) + .send(); + } catch (error) { + const clientError = error as HttpClientError; + + expect(clientError.code).toBe('ERR_BAD_REQUEST'); + expect(clientError.response?.status).toBe(HttpStatusCode.BAD_REQUEST); + expect(clientError.response?.data).toEqual(errorDetails); + } + }); + test('should send POST request with correct headers, body, and URL', async () => { server.use(handlers.postRequest); diff --git a/services/http/src/services/httpClientAxios/httpClientAxios.ts b/services/http/src/services/httpClientAxios/httpClientAxios.ts index ff232d55..dcf53a0d 100644 --- a/services/http/src/services/httpClientAxios/httpClientAxios.ts +++ b/services/http/src/services/httpClientAxios/httpClientAxios.ts @@ -1,7 +1,7 @@ import axios, { AxiosInstance, AxiosError } from 'axios'; import { HttpClient, IHttpClientInit, DEFAULT_REQUEST_TIMEOUT } from '../httpClient'; import { IRequestClientOptions } from '../httpRequest'; -import { IHttpClientResponse, THeaders } from '../../types/httpClient.types'; +import { HttpClientError, IHttpClientResponse } from '../../types/httpClient.types'; export class HttpClientAxios extends HttpClient { requestClient; @@ -16,14 +16,27 @@ export class HttpClientAxios extends HttpClient { timeout: timeout ?? DEFAULT_REQUEST_TIMEOUT }); - this.requestClient = (arg: IRequestClientOptions): Promise> => + this.requestClient = (arg: IRequestClientOptions): Promise> => this.axiosInstance({ url: arg.url, method: arg.method, headers: arg.headers, params: arg.params, data: arg.body }) .then(response => ({ data: response.data, status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()) - })); + })) + .catch((error: AxiosError) => { + throw new HttpClientError({ + message: error.message, + code: error.code, + response: error.response ? { + data: error.response.data, + status: error.response.status, + statusText: error.response.statusText, + headers: Object.entries(error.response.headers) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) // TODO: временное решение + } : undefined + }); + }); } setHeader(key: string, value: string): void { diff --git a/services/http/src/services/httpClientFetch/httpClientFetch.ts b/services/http/src/services/httpClientFetch/httpClientFetch.ts index 59c24311..fc2b97ce 100644 --- a/services/http/src/services/httpClientFetch/httpClientFetch.ts +++ b/services/http/src/services/httpClientFetch/httpClientFetch.ts @@ -1,6 +1,6 @@ import { IRequestClientOptions } from '../httpRequest'; import { DEFAULT_REQUEST_TIMEOUT, HttpClient, IHttpClientInit } from '../httpClient'; -import { THeaders, IHttpClientResponse } from '../../types/httpClient.types'; +import { THeaders, IHttpClientResponse, HttpClientError } from '../../types/httpClient.types'; export class HttpClientFetch extends HttpClient { baseURL: string; @@ -14,30 +14,39 @@ export class HttpClientFetch extends HttpClient { this.baseURL = baseURL; this.headers = Object.assign(this.headers, headers); this.timeout = timeout ?? DEFAULT_REQUEST_TIMEOUT; - this.requestClient = async (arg: IRequestClientOptions): Promise> => { + this.requestClient = async (arg: IRequestClientOptions): Promise> => { const url = encodeURI(`${this.baseURL}${arg.url}`) + HttpClient.buildQueryString(arg.params); const headers = Object.assign(this.headers, arg.headers); const response = await fetch(url, { method: arg.method, headers, body: JSON.stringify(arg.body) }); - if (!response.ok) { - throw new Error(`HTTP error! Code: ${response.status}`); - } - const contentType = response.headers.get('Content-Type'); - let data: R; + let data; if (contentType?.includes('application/json')) { data = await response.json(); } else if (contentType?.includes('text/')) { - data = (await response.text()) as unknown as R; + data = (await response.text()); } else { - data = (await response.blob()) as unknown as R; + data = (await response.blob()); + } + + if (!response.ok) { + throw new HttpClientError({ + message: `Request failed with status ${response.status}`, + code: `ERR_${response.statusText.toUpperCase().replace(' ', '_')}`, + response: { + data: data as E, + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + } + }); } return { - data, + data: data as R, status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()), diff --git a/services/http/src/types/httpClient.types.ts b/services/http/src/types/httpClient.types.ts index 9cf4dee3..47db3337 100644 --- a/services/http/src/types/httpClient.types.ts +++ b/services/http/src/types/httpClient.types.ts @@ -15,7 +15,7 @@ export interface IRequestConfig { data?: D; } -export interface IHttpClientResponse { +export interface IHttpClientResponse { data: T; status: HttpStatusCode; statusText: string; @@ -34,8 +34,7 @@ export class HttpClientError extends Error { response?: IHttpClientResponse; // config?: IRequestConfig; }) { - super(args.message); - + super(); this.code = args.code; this.response = args.response; }; From 112c81309195cc20b43162b712a92c0197aefa63 Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Wed, 4 Jun 2025 15:19:22 +0300 Subject: [PATCH 40/49] feat(http-client): fix in headers mapping --- .../http/src/services/httpClientAxios/httpClientAxios.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/http/src/services/httpClientAxios/httpClientAxios.ts b/services/http/src/services/httpClientAxios/httpClientAxios.ts index dcf53a0d..d3c03dce 100644 --- a/services/http/src/services/httpClientAxios/httpClientAxios.ts +++ b/services/http/src/services/httpClientAxios/httpClientAxios.ts @@ -22,7 +22,7 @@ export class HttpClientAxios extends HttpClient { data: response.data, status: response.status, statusText: response.statusText, - headers: Object.fromEntries(response.headers.entries()) + headers: Object.fromEntries(Object.entries(response.headers)) })) .catch((error: AxiosError) => { throw new HttpClientError({ @@ -32,8 +32,7 @@ export class HttpClientAxios extends HttpClient { data: error.response.data, status: error.response.status, statusText: error.response.statusText, - headers: Object.entries(error.response.headers) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) // TODO: временное решение + headers: Object.fromEntries(Object.entries(error.response.headers)) } : undefined }); }); From d168f08098015f5f236c5cceec39b7602f52ebe8 Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Mon, 9 Jun 2025 14:36:30 +0300 Subject: [PATCH 41/49] feat(http-client): timeout and abort signal --- .../src/__fixtures__/httpClient.fixtures.ts | 1 + .../src/__handlers__/httpClient.handlers.ts | 14 ++++-- .../src/__tests__/httpClientAxios.tests.ts | 38 +++++++++++++++- .../src/__tests__/httpClientFetch.tests.ts | 41 ++++++++++++++++-- services/http/src/services/httpClient.ts | 4 +- .../httpClientAxios/httpClientAxios.ts | 23 ++++++++-- .../httpClientFetch/httpClientFetch.ts | 43 ++++++++++++++++--- services/http/src/services/httpRequest.ts | 14 +++++- services/http/src/types/httpClient.types.ts | 2 +- 9 files changed, 158 insertions(+), 22 deletions(-) diff --git a/services/http/src/__fixtures__/httpClient.fixtures.ts b/services/http/src/__fixtures__/httpClient.fixtures.ts index e5a769c3..423bfc5f 100644 --- a/services/http/src/__fixtures__/httpClient.fixtures.ts +++ b/services/http/src/__fixtures__/httpClient.fixtures.ts @@ -10,6 +10,7 @@ export const baseUrl = 'https://test-url.com'; export const getPath = '/get'; export const getPathWithQueryParams = '/get/with-query'; export const getPathWithError = '/get/with-error'; +export const getPathWithTimeout = '/get/with-timeout'; export const postPath = '/post'; export const putPath = '/put'; export const patchPath = '/patch'; diff --git a/services/http/src/__handlers__/httpClient.handlers.ts b/services/http/src/__handlers__/httpClient.handlers.ts index 0e520076..0cb5d8fa 100644 --- a/services/http/src/__handlers__/httpClient.handlers.ts +++ b/services/http/src/__handlers__/httpClient.handlers.ts @@ -1,4 +1,4 @@ -import { http, HttpResponse } from 'msw'; +import { http, HttpResponse, delay } from 'msw'; import { baseUrl, getPath, @@ -8,8 +8,10 @@ import { deletePath, getPathWithQueryParams, getPathWithError, + getPathWithTimeout, queryParams, - errorDetails + errorDetails, + successResponse } from '../__fixtures__/httpClient.fixtures'; export const getRequest = http.get(`${baseUrl}${getPath}`, ({ request }) => { @@ -41,7 +43,13 @@ export const getRequestWithError = http.get(`${baseUrl}${getPathWithError}`, ({ return new HttpResponse(JSON.stringify(errorDetails), { status: 400, headers: { 'content-type': 'application/json' } }); } - return HttpResponse.json({ testParam: testParamValue }); + return HttpResponse.json(successResponse); +}); + +export const getRequestWithTimeout = http.get(`${baseUrl}${getPathWithTimeout}`, async () => { + await delay(3000); + + return HttpResponse.json(successResponse); }); export const postRequest = http.post(`${baseUrl}${postPath}`, async ({ request }) => { diff --git a/services/http/src/__tests__/httpClientAxios.tests.ts b/services/http/src/__tests__/httpClientAxios.tests.ts index d0ce5f9e..ccdfa0d0 100644 --- a/services/http/src/__tests__/httpClientAxios.tests.ts +++ b/services/http/src/__tests__/httpClientAxios.tests.ts @@ -8,6 +8,7 @@ import { queryParams, getPath, getPathWithQueryParams, + getPathWithTimeout, postPath, putPath, patchPath, @@ -63,17 +64,19 @@ describe('services/HttpClientAxios', () => { expect(response.data).toEqual(queryParams); }); - test('should get correct error on GET request', async () => { + test('should get correct 400 error on GET request', async () => { server.use(handlers.getRequestWithError); const httpClientInstance = new HttpClientAxios({ baseURL: baseUrl }); try { - await httpClientInstance + const response = await httpClientInstance .get() .url(getPathWithError) .params(queryParams) .send(); + + expect(response).rejects.toThrowError(); } catch (error) { const clientError = error as HttpClientError; @@ -83,6 +86,37 @@ describe('services/HttpClientAxios', () => { } }); + test('should get the error when GET request timeout exceeded', async () => { + server.use(handlers.getRequestWithTimeout); + + const httpClientInstance = new HttpClientAxios({ baseURL: baseUrl, timeout: 1000 }); + + const response = httpClientInstance + .get() + .url(getPathWithTimeout) + .send(); + + await expect(response).rejects.toThrow(); // Error('Timeout of 1000ms exceeded'); + }); + + test('should get the error on request cancel', async () => { + server.use(handlers.getRequestWithTimeout); + + const httpClientInstance = new HttpClientAxios({ baseURL: baseUrl }); + + const controller = new AbortController(); + + const response = httpClientInstance + .get() + .url(getPathWithTimeout) + .signal(controller.signal) + .send(); + + controller.abort(); + + await expect(response).rejects.toThrowError('The request was cancelled'); + }); + test('should send POST request with correct headers, body, and URL', async () => { server.use(handlers.postRequest); diff --git a/services/http/src/__tests__/httpClientFetch.tests.ts b/services/http/src/__tests__/httpClientFetch.tests.ts index 5f1436e9..0e0e772e 100644 --- a/services/http/src/__tests__/httpClientFetch.tests.ts +++ b/services/http/src/__tests__/httpClientFetch.tests.ts @@ -15,7 +15,9 @@ import { patchPath, deletePath, requestBody, - successResponse, errorDetails + successResponse, + errorDetails, + getPathWithTimeout } from '../__fixtures__/httpClient.fixtures'; import { HttpStatusCode } from '../types/httpStatusCode.types'; import { HttpClientError } from '../types/httpClient.types'; @@ -63,17 +65,19 @@ describe('services/HttpClientFetch', () => { expect(response.data).toEqual(queryParams); }); - test('should get correct error on GET request', async () => { + test('should get correct 400 error on GET request', async () => { server.use(handlers.getRequestWithError); const httpClientInstance = new HttpClientFetch({ baseURL: baseUrl }); try { - await httpClientInstance + const response = await httpClientInstance .get() .url(getPathWithError) .params(queryParams) .send(); + + expect(response).rejects.toThrowError(); } catch (error) { const clientError = error as HttpClientError; @@ -83,6 +87,37 @@ describe('services/HttpClientFetch', () => { } }); + test('should get the error when GET request timeout exceeded', async () => { + server.use(handlers.getRequestWithTimeout); + + const httpClientInstance = new HttpClientFetch({ baseURL: baseUrl, timeout: 1000 }); + + const response = httpClientInstance + .get() + .url(getPathWithTimeout) + .send(); + + await expect(response).rejects.toThrowError('Timeout of 1000ms exceeded'); + }); + + test('should get the error on request cancel', async () => { + server.use(handlers.getRequestWithTimeout); + + const httpClientInstance = new HttpClientFetch({ baseURL: baseUrl }); + + const controller = new AbortController(); + + const response = httpClientInstance + .get() + .url(getPathWithTimeout) + .signal(controller.signal) + .send(); + + controller.abort(); + + await expect(response).rejects.toThrowError('The request was cancelled'); + }); + test('should send POST request with correct headers, body, and URL', async () => { server.use(handlers.postRequest); diff --git a/services/http/src/services/httpClient.ts b/services/http/src/services/httpClient.ts index 20f11729..0f1af635 100644 --- a/services/http/src/services/httpClient.ts +++ b/services/http/src/services/httpClient.ts @@ -1,4 +1,4 @@ -import { HttpRequest, HttpRequestWithBody, IRequestClientOptions } from './httpRequest'; +import { HttpRequest, HttpRequestWithBody, IRequestOptions } from './httpRequest'; import { HttpMethod } from '../types/httpMethod.types'; import { IHttpClientResponse, THeaders, TQueryParams } from '../types/httpClient.types'; @@ -11,7 +11,7 @@ export interface IHttpClientInit { } export abstract class HttpClient { - abstract requestClient: (arg: IRequestClientOptions) => Promise>; + abstract requestClient: (arg: IRequestOptions) => Promise>; abstract setHeader(key: string, value: string): void; diff --git a/services/http/src/services/httpClientAxios/httpClientAxios.ts b/services/http/src/services/httpClientAxios/httpClientAxios.ts index d3c03dce..761b27ee 100644 --- a/services/http/src/services/httpClientAxios/httpClientAxios.ts +++ b/services/http/src/services/httpClientAxios/httpClientAxios.ts @@ -1,6 +1,6 @@ import axios, { AxiosInstance, AxiosError } from 'axios'; import { HttpClient, IHttpClientInit, DEFAULT_REQUEST_TIMEOUT } from '../httpClient'; -import { IRequestClientOptions } from '../httpRequest'; +import { IRequestOptions } from '../httpRequest'; import { HttpClientError, IHttpClientResponse } from '../../types/httpClient.types'; export class HttpClientAxios extends HttpClient { @@ -13,11 +13,19 @@ export class HttpClientAxios extends HttpClient { this.axiosInstance = axios.create({ baseURL, headers, - timeout: timeout ?? DEFAULT_REQUEST_TIMEOUT + timeout: timeout ?? DEFAULT_REQUEST_TIMEOUT, + timeoutErrorMessage: `Timeout of ${timeout ?? DEFAULT_REQUEST_TIMEOUT}ms exceeded` }); - this.requestClient = (arg: IRequestClientOptions): Promise> => - this.axiosInstance({ url: arg.url, method: arg.method, headers: arg.headers, params: arg.params, data: arg.body }) + this.requestClient = (options: IRequestOptions): Promise> => + this.axiosInstance({ + url: options.url, + method: options.method, + headers: options.headers, + params: options.params, + data: options.body, + signal: options.signal + }) .then(response => ({ data: response.data, status: response.status, @@ -25,6 +33,13 @@ export class HttpClientAxios extends HttpClient { headers: Object.fromEntries(Object.entries(response.headers)) })) .catch((error: AxiosError) => { + if (error.code === 'ERR_CANCELED') { + throw new HttpClientError({ + code: error.code, + message: options.signal?.aborted ? 'The request was cancelled' : error.message + }); + } + throw new HttpClientError({ message: error.message, code: error.code, diff --git a/services/http/src/services/httpClientFetch/httpClientFetch.ts b/services/http/src/services/httpClientFetch/httpClientFetch.ts index fc2b97ce..3bc4551d 100644 --- a/services/http/src/services/httpClientFetch/httpClientFetch.ts +++ b/services/http/src/services/httpClientFetch/httpClientFetch.ts @@ -1,7 +1,23 @@ -import { IRequestClientOptions } from '../httpRequest'; +import { IRequestOptions } from '../httpRequest'; import { DEFAULT_REQUEST_TIMEOUT, HttpClient, IHttpClientInit } from '../httpClient'; import { THeaders, IHttpClientResponse, HttpClientError } from '../../types/httpClient.types'; +const combineAbortSignals = (signals: Array): AbortSignal => { + const controller = new AbortController(); + + signals.forEach(signal => { + if (!signal) return; + + if (signal.aborted) { + controller.abort(); + } else { + signal.addEventListener('abort', () => controller.abort()); + } + }); + + return controller.signal; +}; + export class HttpClientFetch extends HttpClient { baseURL: string; headers: THeaders = {}; @@ -14,11 +30,28 @@ export class HttpClientFetch extends HttpClient { this.baseURL = baseURL; this.headers = Object.assign(this.headers, headers); this.timeout = timeout ?? DEFAULT_REQUEST_TIMEOUT; - this.requestClient = async (arg: IRequestClientOptions): Promise> => { - const url = encodeURI(`${this.baseURL}${arg.url}`) + HttpClient.buildQueryString(arg.params); - const headers = Object.assign(this.headers, arg.headers); - const response = await fetch(url, { method: arg.method, headers, body: JSON.stringify(arg.body) }); + this.requestClient = async (options: IRequestOptions): Promise> => { + const url = encodeURI(`${this.baseURL}${options.url}`) + HttpClient.buildQueryString(options.params); + const headers = Object.assign(this.headers, options.headers); + + const timeoutSignal = AbortSignal.timeout(this.timeout); + const combinedSignals = combineAbortSignals([timeoutSignal, options.signal]) + + const response = await fetch(url, { + method: options.method, + headers, + body: JSON.stringify(options.body), + signal: combinedSignals + }).catch(error => { + if (error.name === 'AbortError') { + throw new HttpClientError({ + code: error.code, + message: timeoutSignal.aborted ? `Timeout of ${this.timeout}ms exceeded` : 'The request was cancelled' + }); + } + throw error; // TODO: добавить ловлю остальных ошибок + }); const contentType = response.headers.get('Content-Type'); diff --git a/services/http/src/services/httpRequest.ts b/services/http/src/services/httpRequest.ts index 07b4b95e..84886d65 100644 --- a/services/http/src/services/httpRequest.ts +++ b/services/http/src/services/httpRequest.ts @@ -1,15 +1,16 @@ import { HttpMethod } from '../types/httpMethod.types'; import { IHttpClientResponse, THeaders, TQueryParams } from '../types/httpClient.types'; -export interface IRequestClientOptions { +export interface IRequestOptions { url: string; method: HttpMethod; headers: THeaders; params: TQueryParams; + signal?: AbortSignal; body?: Object; } -export type TRequestClient = (arg: IRequestClientOptions) => Promise>; +export type TRequestClient = (arg: IRequestOptions) => Promise>; export class HttpRequest { protected requestClient: TRequestClient; @@ -17,6 +18,7 @@ export class HttpRequest { protected method: HttpMethod; protected headersValue: THeaders = {}; protected paramsValue: TQueryParams = {}; + protected abortSignal?: AbortSignal; constructor(requestClient: TRequestClient, method: HttpMethod) { this.requestClient = requestClient; @@ -41,12 +43,19 @@ export class HttpRequest { return this; } + signal(abortSignal: AbortSignal): this { + this.abortSignal = abortSignal; + + return this; + } + send(): Promise> { return this.requestClient({ url: this.urlValue, method: this.method, headers: this.headersValue, params: this.paramsValue, + signal: this.abortSignal }); } } @@ -66,6 +75,7 @@ export class HttpRequestWithBody extends HttpRequest { method: this.method, headers: this.headersValue, params: this.paramsValue, + signal: this.abortSignal, body: this.bodyValue }) } diff --git a/services/http/src/types/httpClient.types.ts b/services/http/src/types/httpClient.types.ts index 47db3337..e0ca0fec 100644 --- a/services/http/src/types/httpClient.types.ts +++ b/services/http/src/types/httpClient.types.ts @@ -34,7 +34,7 @@ export class HttpClientError extends Error { response?: IHttpClientResponse; // config?: IRequestConfig; }) { - super(); + super(args.message); this.code = args.code; this.response = args.response; }; From a71959ecd71727463633691ee64ab3e164ab3368 Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Mon, 9 Jun 2025 14:45:44 +0300 Subject: [PATCH 42/49] feat(http-client): add other fetch exceptions handling --- .../http/src/services/httpClientFetch/httpClientFetch.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/http/src/services/httpClientFetch/httpClientFetch.ts b/services/http/src/services/httpClientFetch/httpClientFetch.ts index 3bc4551d..9a3c1918 100644 --- a/services/http/src/services/httpClientFetch/httpClientFetch.ts +++ b/services/http/src/services/httpClientFetch/httpClientFetch.ts @@ -50,7 +50,11 @@ export class HttpClientFetch extends HttpClient { message: timeoutSignal.aborted ? `Timeout of ${this.timeout}ms exceeded` : 'The request was cancelled' }); } - throw error; // TODO: добавить ловлю остальных ошибок + + throw new HttpClientError({ + code: error.code, + message: error.message + }); }); const contentType = response.headers.get('Content-Type'); From 122b0c5ac5125ef9d967c147ea8db159e8db39c1 Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Mon, 9 Jun 2025 15:40:07 +0300 Subject: [PATCH 43/49] feat(http-client): update tests --- .../src/__tests__/httpClientAxios.tests.ts | 29 +++++++++---------- .../src/__tests__/httpClientFetch.tests.ts | 19 +++++------- .../httpClientAxios/httpClientAxios.ts | 11 +++---- 3 files changed, 25 insertions(+), 34 deletions(-) diff --git a/services/http/src/__tests__/httpClientAxios.tests.ts b/services/http/src/__tests__/httpClientAxios.tests.ts index ccdfa0d0..ce4b0cc9 100644 --- a/services/http/src/__tests__/httpClientAxios.tests.ts +++ b/services/http/src/__tests__/httpClientAxios.tests.ts @@ -19,7 +19,6 @@ import { errorDetails } from '../__fixtures__/httpClient.fixtures'; import { HttpStatusCode } from '../types/httpStatusCode.types'; -import { HttpClientError } from '../types/httpClient.types'; const server = setupServer(); @@ -69,21 +68,19 @@ describe('services/HttpClientAxios', () => { const httpClientInstance = new HttpClientAxios({ baseURL: baseUrl }); - try { - const response = await httpClientInstance - .get() - .url(getPathWithError) - .params(queryParams) - .send(); - - expect(response).rejects.toThrowError(); - } catch (error) { - const clientError = error as HttpClientError; - - expect(clientError.code).toBe('ERR_BAD_REQUEST'); - expect(clientError.response?.status).toBe(HttpStatusCode.BAD_REQUEST); - expect(clientError.response?.data).toEqual(errorDetails); - } + const response = httpClientInstance + .get() + .url(getPathWithError) + .params(queryParams) + .send(); + + await expect(response).rejects.toMatchObject({ + code: 'ERR_BAD_REQUEST', + response: { + status: HttpStatusCode.BAD_REQUEST, + data: errorDetails + } + }); }); test('should get the error when GET request timeout exceeded', async () => { diff --git a/services/http/src/__tests__/httpClientFetch.tests.ts b/services/http/src/__tests__/httpClientFetch.tests.ts index 0e0e772e..b81e908b 100644 --- a/services/http/src/__tests__/httpClientFetch.tests.ts +++ b/services/http/src/__tests__/httpClientFetch.tests.ts @@ -20,7 +20,6 @@ import { getPathWithTimeout } from '../__fixtures__/httpClient.fixtures'; import { HttpStatusCode } from '../types/httpStatusCode.types'; -import { HttpClientError } from '../types/httpClient.types'; const server = setupServer(); @@ -70,21 +69,19 @@ describe('services/HttpClientFetch', () => { const httpClientInstance = new HttpClientFetch({ baseURL: baseUrl }); - try { - const response = await httpClientInstance + const response = httpClientInstance .get() .url(getPathWithError) .params(queryParams) .send(); - expect(response).rejects.toThrowError(); - } catch (error) { - const clientError = error as HttpClientError; - - expect(clientError.code).toBe('ERR_BAD_REQUEST'); - expect(clientError.response?.status).toBe(HttpStatusCode.BAD_REQUEST); - expect(clientError.response?.data).toEqual(errorDetails); - } + await expect(response).rejects.toMatchObject({ + code: 'ERR_BAD_REQUEST', + response: { + status: HttpStatusCode.BAD_REQUEST, + data: errorDetails + } + }); }); test('should get the error when GET request timeout exceeded', async () => { diff --git a/services/http/src/services/httpClientAxios/httpClientAxios.ts b/services/http/src/services/httpClientAxios/httpClientAxios.ts index 761b27ee..66f0b74c 100644 --- a/services/http/src/services/httpClientAxios/httpClientAxios.ts +++ b/services/http/src/services/httpClientAxios/httpClientAxios.ts @@ -33,15 +33,12 @@ export class HttpClientAxios extends HttpClient { headers: Object.fromEntries(Object.entries(response.headers)) })) .catch((error: AxiosError) => { - if (error.code === 'ERR_CANCELED') { - throw new HttpClientError({ - code: error.code, - message: options.signal?.aborted ? 'The request was cancelled' : error.message - }); - } + const message = (error.code === 'ERR_CANCELED' && options.signal?.aborted) + ? 'The request was cancelled' + : error.message; throw new HttpClientError({ - message: error.message, + message: message, code: error.code, response: error.response ? { data: error.response.data, From 8999a36772f83ff4138f5f5da037b5445a76a92f Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Tue, 10 Jun 2025 13:30:08 +0300 Subject: [PATCH 44/49] feat(http-client): remove axios instance --- services/http/src/services/httpClient.ts | 18 ++++++++++-- .../httpClientAxios/httpClientAxios.ts | 29 +++++++------------ .../httpClientFetch/httpClientFetch.ts | 19 +++--------- 3 files changed, 29 insertions(+), 37 deletions(-) diff --git a/services/http/src/services/httpClient.ts b/services/http/src/services/httpClient.ts index 0f1af635..6c2f62c6 100644 --- a/services/http/src/services/httpClient.ts +++ b/services/http/src/services/httpClient.ts @@ -5,15 +5,23 @@ import { IHttpClientResponse, THeaders, TQueryParams } from '../types/httpClient export const DEFAULT_REQUEST_TIMEOUT = 60000; export interface IHttpClientInit { - baseURL: string; + baseURL?: string; headers?: THeaders; timeout?: number; } export abstract class HttpClient { - abstract requestClient: (arg: IRequestOptions) => Promise>; + baseURL: string; + headers: THeaders = {}; + timeout: number; - abstract setHeader(key: string, value: string): void; + protected constructor({ baseURL, headers, timeout }: IHttpClientInit) { + this.baseURL = baseURL ?? ''; + this.headers = Object.assign(this.headers, headers); + this.timeout = timeout ?? DEFAULT_REQUEST_TIMEOUT; + } + + abstract requestClient: (arg: IRequestOptions) => Promise>; static buildQueryString(queryParams: TQueryParams) { const params = Object.keys(queryParams); @@ -27,6 +35,10 @@ export abstract class HttpClient { return ''; } + setHeader(key: string, value: string): void { + this.headers[key] = value; + }; + get() { return new HttpRequest(this.requestClient, HttpMethod.GET); } diff --git a/services/http/src/services/httpClientAxios/httpClientAxios.ts b/services/http/src/services/httpClientAxios/httpClientAxios.ts index 66f0b74c..b8e23ae9 100644 --- a/services/http/src/services/httpClientAxios/httpClientAxios.ts +++ b/services/http/src/services/httpClientAxios/httpClientAxios.ts @@ -1,30 +1,25 @@ -import axios, { AxiosInstance, AxiosError } from 'axios'; -import { HttpClient, IHttpClientInit, DEFAULT_REQUEST_TIMEOUT } from '../httpClient'; +import axios, { AxiosError } from 'axios'; +import { HttpClient, IHttpClientInit } from '../httpClient'; import { IRequestOptions } from '../httpRequest'; import { HttpClientError, IHttpClientResponse } from '../../types/httpClient.types'; export class HttpClientAxios extends HttpClient { requestClient; - private axiosInstance: AxiosInstance; - constructor({ baseURL, headers, timeout }: IHttpClientInit) { - super(); - - this.axiosInstance = axios.create({ - baseURL, - headers, - timeout: timeout ?? DEFAULT_REQUEST_TIMEOUT, - timeoutErrorMessage: `Timeout of ${timeout ?? DEFAULT_REQUEST_TIMEOUT}ms exceeded` - }); + constructor(initSettings: IHttpClientInit) { + super(initSettings); this.requestClient = (options: IRequestOptions): Promise> => - this.axiosInstance({ + axios({ + baseURL: this.baseURL, url: options.url, method: options.method, - headers: options.headers, + headers: { ...this.headers, ...options.headers }, params: options.params, data: options.body, - signal: options.signal + signal: options.signal, + timeout: this.timeout, + timeoutErrorMessage: `Timeout of ${this.timeout}ms exceeded` }) .then(response => ({ data: response.data, @@ -49,8 +44,4 @@ export class HttpClientAxios extends HttpClient { }); }); } - - setHeader(key: string, value: string): void { - this.axiosInstance.defaults.headers[key] = value; - }; } diff --git a/services/http/src/services/httpClientFetch/httpClientFetch.ts b/services/http/src/services/httpClientFetch/httpClientFetch.ts index 9a3c1918..e2c5e3da 100644 --- a/services/http/src/services/httpClientFetch/httpClientFetch.ts +++ b/services/http/src/services/httpClientFetch/httpClientFetch.ts @@ -1,6 +1,6 @@ import { IRequestOptions } from '../httpRequest'; -import { DEFAULT_REQUEST_TIMEOUT, HttpClient, IHttpClientInit } from '../httpClient'; -import { THeaders, IHttpClientResponse, HttpClientError } from '../../types/httpClient.types'; +import { HttpClient, IHttpClientInit } from '../httpClient'; +import { IHttpClientResponse, HttpClientError } from '../../types/httpClient.types'; const combineAbortSignals = (signals: Array): AbortSignal => { const controller = new AbortController(); @@ -19,17 +19,10 @@ const combineAbortSignals = (signals: Array): AbortSign }; export class HttpClientFetch extends HttpClient { - baseURL: string; - headers: THeaders = {}; - timeout: number; requestClient; - constructor({ baseURL, headers, timeout }: IHttpClientInit) { - super(); - - this.baseURL = baseURL; - this.headers = Object.assign(this.headers, headers); - this.timeout = timeout ?? DEFAULT_REQUEST_TIMEOUT; + constructor(initSettings: IHttpClientInit) { + super(initSettings); this.requestClient = async (options: IRequestOptions): Promise> => { const url = encodeURI(`${this.baseURL}${options.url}`) + HttpClient.buildQueryString(options.params); @@ -90,8 +83,4 @@ export class HttpClientFetch extends HttpClient { }; }; } - - setHeader(key: string, value: string): void { - this.headers[key] = value; - } } From 7ec89e190ad2f87e06161449ebd8d406cfa8bb7e Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Tue, 10 Jun 2025 14:10:33 +0300 Subject: [PATCH 45/49] feat(http-client): custom timeout for axios client --- .../src/__tests__/httpClientAxios.tests.ts | 2 +- services/http/src/services/httpClient.ts | 4 +-- .../httpClientAxios/httpClientAxios.ts | 27 ++++++++++++------- .../httpClientFetch/httpClientFetch.ts | 21 +++------------ .../src/utilities/httpClient.utilities.ts | 15 +++++++++++ 5 files changed, 39 insertions(+), 30 deletions(-) create mode 100644 services/http/src/utilities/httpClient.utilities.ts diff --git a/services/http/src/__tests__/httpClientAxios.tests.ts b/services/http/src/__tests__/httpClientAxios.tests.ts index ce4b0cc9..2c4de0da 100644 --- a/services/http/src/__tests__/httpClientAxios.tests.ts +++ b/services/http/src/__tests__/httpClientAxios.tests.ts @@ -93,7 +93,7 @@ describe('services/HttpClientAxios', () => { .url(getPathWithTimeout) .send(); - await expect(response).rejects.toThrow(); // Error('Timeout of 1000ms exceeded'); + await expect(response).rejects.toThrowError('Timeout of 1000ms exceeded'); }); test('should get the error on request cancel', async () => { diff --git a/services/http/src/services/httpClient.ts b/services/http/src/services/httpClient.ts index 6c2f62c6..06ca3af1 100644 --- a/services/http/src/services/httpClient.ts +++ b/services/http/src/services/httpClient.ts @@ -12,12 +12,12 @@ export interface IHttpClientInit { export abstract class HttpClient { baseURL: string; - headers: THeaders = {}; + headers: THeaders; timeout: number; protected constructor({ baseURL, headers, timeout }: IHttpClientInit) { this.baseURL = baseURL ?? ''; - this.headers = Object.assign(this.headers, headers); + this.headers = headers ?? {}; this.timeout = timeout ?? DEFAULT_REQUEST_TIMEOUT; } diff --git a/services/http/src/services/httpClientAxios/httpClientAxios.ts b/services/http/src/services/httpClientAxios/httpClientAxios.ts index b8e23ae9..578564f6 100644 --- a/services/http/src/services/httpClientAxios/httpClientAxios.ts +++ b/services/http/src/services/httpClientAxios/httpClientAxios.ts @@ -2,6 +2,7 @@ import axios, { AxiosError } from 'axios'; import { HttpClient, IHttpClientInit } from '../httpClient'; import { IRequestOptions } from '../httpRequest'; import { HttpClientError, IHttpClientResponse } from '../../types/httpClient.types'; +import { combineAbortSignals } from '../../utilities/httpClient.utilities'; export class HttpClientAxios extends HttpClient { requestClient; @@ -9,17 +10,18 @@ export class HttpClientAxios extends HttpClient { constructor(initSettings: IHttpClientInit) { super(initSettings); - this.requestClient = (options: IRequestOptions): Promise> => - axios({ + this.requestClient = (options: IRequestOptions): Promise> => { + const timeoutSignal = AbortSignal.timeout(this.timeout); + const combinedSignals = combineAbortSignals(timeoutSignal, options.signal); + + return axios({ baseURL: this.baseURL, url: options.url, method: options.method, - headers: { ...this.headers, ...options.headers }, + headers: {...this.headers, ...options.headers}, params: options.params, data: options.body, - signal: options.signal, - timeout: this.timeout, - timeoutErrorMessage: `Timeout of ${this.timeout}ms exceeded` + signal: combinedSignals }) .then(response => ({ data: response.data, @@ -28,9 +30,15 @@ export class HttpClientAxios extends HttpClient { headers: Object.fromEntries(Object.entries(response.headers)) })) .catch((error: AxiosError) => { - const message = (error.code === 'ERR_CANCELED' && options.signal?.aborted) - ? 'The request was cancelled' - : error.message; + let message: string; + + if (error.code === 'ERR_CANCELED') { + message = timeoutSignal.aborted + ? `Timeout of ${this.timeout}ms exceeded` + : 'The request was cancelled'; + } else { + message = error.message; + } throw new HttpClientError({ message: message, @@ -43,5 +51,6 @@ export class HttpClientAxios extends HttpClient { } : undefined }); }); + }; } } diff --git a/services/http/src/services/httpClientFetch/httpClientFetch.ts b/services/http/src/services/httpClientFetch/httpClientFetch.ts index e2c5e3da..4dfc553c 100644 --- a/services/http/src/services/httpClientFetch/httpClientFetch.ts +++ b/services/http/src/services/httpClientFetch/httpClientFetch.ts @@ -1,22 +1,7 @@ import { IRequestOptions } from '../httpRequest'; import { HttpClient, IHttpClientInit } from '../httpClient'; import { IHttpClientResponse, HttpClientError } from '../../types/httpClient.types'; - -const combineAbortSignals = (signals: Array): AbortSignal => { - const controller = new AbortController(); - - signals.forEach(signal => { - if (!signal) return; - - if (signal.aborted) { - controller.abort(); - } else { - signal.addEventListener('abort', () => controller.abort()); - } - }); - - return controller.signal; -}; +import { combineAbortSignals } from '../../utilities/httpClient.utilities'; export class HttpClientFetch extends HttpClient { requestClient; @@ -26,10 +11,10 @@ export class HttpClientFetch extends HttpClient { this.requestClient = async (options: IRequestOptions): Promise> => { const url = encodeURI(`${this.baseURL}${options.url}`) + HttpClient.buildQueryString(options.params); - const headers = Object.assign(this.headers, options.headers); + const headers = { ...this.headers, ...options.headers }; const timeoutSignal = AbortSignal.timeout(this.timeout); - const combinedSignals = combineAbortSignals([timeoutSignal, options.signal]) + const combinedSignals = combineAbortSignals(timeoutSignal, options.signal) const response = await fetch(url, { method: options.method, diff --git a/services/http/src/utilities/httpClient.utilities.ts b/services/http/src/utilities/httpClient.utilities.ts new file mode 100644 index 00000000..49076cf5 --- /dev/null +++ b/services/http/src/utilities/httpClient.utilities.ts @@ -0,0 +1,15 @@ +export const combineAbortSignals = (...signals: Array): AbortSignal => { + const controller = new AbortController(); + + signals.forEach(signal => { + if (!signal) return; + + if (signal.aborted) { + controller.abort(); + } else { + signal.addEventListener('abort', () => controller.abort()); + } + }); + + return controller.signal; +}; From 42d7503bace8d6584dc93efff5ff757e4c89d8d2 Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Tue, 10 Jun 2025 16:43:06 +0300 Subject: [PATCH 46/49] feat(http-client): request interceptor --- services/http/src/services/httpClient.ts | 29 ++++++++++++++++++- .../httpClientAxios/httpClientAxios.ts | 24 +++++++++------ .../httpClientFetch/httpClientFetch.ts | 29 +++++++++++++------ services/http/src/types/httpClient.types.ts | 13 +++++---- 4 files changed, 70 insertions(+), 25 deletions(-) diff --git a/services/http/src/services/httpClient.ts b/services/http/src/services/httpClient.ts index 06ca3af1..fd9c4270 100644 --- a/services/http/src/services/httpClient.ts +++ b/services/http/src/services/httpClient.ts @@ -1,6 +1,12 @@ import { HttpRequest, HttpRequestWithBody, IRequestOptions } from './httpRequest'; import { HttpMethod } from '../types/httpMethod.types'; -import { IHttpClientResponse, THeaders, TQueryParams } from '../types/httpClient.types'; +import { + IHttpClientResponse, + HttpClientError, + THeaders, + TQueryParams, + IRequestConfig +} from '../types/httpClient.types'; export const DEFAULT_REQUEST_TIMEOUT = 60000; @@ -10,10 +16,15 @@ export interface IHttpClientInit { timeout?: number; } +type TRequestInterceptor = (request: IRequestConfig) => Promise; +type TResponseInterceptor = (response: IHttpClientResponse, error?: HttpClientError) => Promise; + export abstract class HttpClient { baseURL: string; headers: THeaders; timeout: number; + protected requestInterceptor?: TRequestInterceptor; + protected responseInterceptor?: TResponseInterceptor; protected constructor({ baseURL, headers, timeout }: IHttpClientInit) { this.baseURL = baseURL ?? ''; @@ -58,4 +69,20 @@ export abstract class HttpClient { delete() { return new HttpRequest(this.requestClient, HttpMethod.DELETE); } + + addRequestInterceptor(interceptor: TRequestInterceptor): void { + this.requestInterceptor = interceptor; + } + + addResponseInterceptor(interceptor: TResponseInterceptor): void { + this.responseInterceptor = interceptor; + } + + protected async processRequest(config: IRequestConfig): Promise { + if (this.requestInterceptor) { + return this.requestInterceptor(config); + } + + return config; + } } diff --git a/services/http/src/services/httpClientAxios/httpClientAxios.ts b/services/http/src/services/httpClientAxios/httpClientAxios.ts index 578564f6..0b6f4ac1 100644 --- a/services/http/src/services/httpClientAxios/httpClientAxios.ts +++ b/services/http/src/services/httpClientAxios/httpClientAxios.ts @@ -10,24 +10,29 @@ export class HttpClientAxios extends HttpClient { constructor(initSettings: IHttpClientInit) { super(initSettings); - this.requestClient = (options: IRequestOptions): Promise> => { + this.requestClient = async (options: IRequestOptions): Promise> => { + const processedConfig = await this.processRequest({ + url: options.url, + method: options.method, + baseURL: this.baseURL, + headers: { ...this.headers, ...options.headers }, + params: options.params, + data: options.body + }); + const timeoutSignal = AbortSignal.timeout(this.timeout); const combinedSignals = combineAbortSignals(timeoutSignal, options.signal); return axios({ - baseURL: this.baseURL, - url: options.url, - method: options.method, - headers: {...this.headers, ...options.headers}, - params: options.params, - data: options.body, + ...processedConfig, signal: combinedSignals }) .then(response => ({ data: response.data, status: response.status, statusText: response.statusText, - headers: Object.fromEntries(Object.entries(response.headers)) + headers: Object.fromEntries(Object.entries(response.headers)), + config: processedConfig })) .catch((error: AxiosError) => { let message: string; @@ -48,7 +53,8 @@ export class HttpClientAxios extends HttpClient { status: error.response.status, statusText: error.response.statusText, headers: Object.fromEntries(Object.entries(error.response.headers)) - } : undefined + } : undefined, + config: processedConfig }); }); }; diff --git a/services/http/src/services/httpClientFetch/httpClientFetch.ts b/services/http/src/services/httpClientFetch/httpClientFetch.ts index 4dfc553c..c1ee68b4 100644 --- a/services/http/src/services/httpClientFetch/httpClientFetch.ts +++ b/services/http/src/services/httpClientFetch/httpClientFetch.ts @@ -10,28 +10,37 @@ export class HttpClientFetch extends HttpClient { super(initSettings); this.requestClient = async (options: IRequestOptions): Promise> => { - const url = encodeURI(`${this.baseURL}${options.url}`) + HttpClient.buildQueryString(options.params); - const headers = { ...this.headers, ...options.headers }; + const processedConfig = await this.processRequest({ + url: options.url, + method: options.method, + baseURL: this.baseURL, + headers: { ...this.headers, ...options.headers }, + params: options.params, + data: options.body + }); + const url = encodeURI(`${processedConfig.baseURL}${processedConfig.url}`) + HttpClient.buildQueryString(processedConfig.params ?? {}); const timeoutSignal = AbortSignal.timeout(this.timeout); - const combinedSignals = combineAbortSignals(timeoutSignal, options.signal) + const combinedSignals = combineAbortSignals(timeoutSignal, options.signal); const response = await fetch(url, { - method: options.method, - headers, - body: JSON.stringify(options.body), + method: processedConfig.method, + headers: processedConfig.headers, + body: JSON.stringify(processedConfig.data), signal: combinedSignals }).catch(error => { if (error.name === 'AbortError') { throw new HttpClientError({ code: error.code, - message: timeoutSignal.aborted ? `Timeout of ${this.timeout}ms exceeded` : 'The request was cancelled' + message: timeoutSignal.aborted ? `Timeout of ${this.timeout}ms exceeded` : 'The request was cancelled', + config: processedConfig }); } throw new HttpClientError({ code: error.code, - message: error.message + message: error.message, + config: processedConfig }); }); @@ -56,7 +65,8 @@ export class HttpClientFetch extends HttpClient { status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()), - } + }, + config: processedConfig }); } @@ -65,6 +75,7 @@ export class HttpClientFetch extends HttpClient { status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()), + config: processedConfig }; }; } diff --git a/services/http/src/types/httpClient.types.ts b/services/http/src/types/httpClient.types.ts index e0ca0fec..343f5bfd 100644 --- a/services/http/src/types/httpClient.types.ts +++ b/services/http/src/types/httpClient.types.ts @@ -6,7 +6,7 @@ export type TQueryParamValue = string | number | boolean; export type TQueryParams = Record; -export interface IRequestConfig { +export interface IRequestConfig { url?: string; method?: string; baseURL?: string; @@ -15,27 +15,28 @@ export interface IRequestConfig { data?: D; } -export interface IHttpClientResponse { +export interface IHttpClientResponse { data: T; status: HttpStatusCode; statusText: string; headers: THeaders; - // config?: IRequestConfig; + config?: IRequestConfig; } -export class HttpClientError extends Error { +export class HttpClientError extends Error { code?: string; response?: IHttpClientResponse; - // config?: IRequestConfig + config?: IRequestConfig constructor(args: { message?: string; code?: string; response?: IHttpClientResponse; - // config?: IRequestConfig; + config?: IRequestConfig; }) { super(args.message); this.code = args.code; this.response = args.response; + this.config = args.config; }; } From daaae79a99e5a3c8b754526271e81511c89668ea Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Tue, 10 Jun 2025 18:33:51 +0300 Subject: [PATCH 47/49] feat(http-client): response and error interceptors --- services/http/src/services/httpClient.ts | 26 ++++++++++++++----- .../httpClientAxios/httpClientAxios.ts | 6 ++--- .../httpClientFetch/httpClientFetch.ts | 26 ++++++++++--------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/services/http/src/services/httpClient.ts b/services/http/src/services/httpClient.ts index fd9c4270..fd21f397 100644 --- a/services/http/src/services/httpClient.ts +++ b/services/http/src/services/httpClient.ts @@ -17,7 +17,8 @@ export interface IHttpClientInit { } type TRequestInterceptor = (request: IRequestConfig) => Promise; -type TResponseInterceptor = (response: IHttpClientResponse, error?: HttpClientError) => Promise; +type TResponseInterceptor = (response: IHttpClientResponse) => Promise>; +type TErrorInterceptor = (error: HttpClientError) => Promise; export abstract class HttpClient { baseURL: string; @@ -25,6 +26,7 @@ export abstract class HttpClient { timeout: number; protected requestInterceptor?: TRequestInterceptor; protected responseInterceptor?: TResponseInterceptor; + protected errorInterceptor?: TErrorInterceptor; protected constructor({ baseURL, headers, timeout }: IHttpClientInit) { this.baseURL = baseURL ?? ''; @@ -32,7 +34,7 @@ export abstract class HttpClient { this.timeout = timeout ?? DEFAULT_REQUEST_TIMEOUT; } - abstract requestClient: (arg: IRequestOptions) => Promise>; + protected abstract requestClient: (arg: IRequestOptions) => Promise>; static buildQueryString(queryParams: TQueryParams) { const params = Object.keys(queryParams); @@ -74,15 +76,27 @@ export abstract class HttpClient { this.requestInterceptor = interceptor; } - addResponseInterceptor(interceptor: TResponseInterceptor): void { + addResponseInterceptor(interceptor: TResponseInterceptor): void { this.responseInterceptor = interceptor; } + addErrorInterceptor(interceptor: TErrorInterceptor): void { + this.errorInterceptor = interceptor; + } + protected async processRequest(config: IRequestConfig): Promise { - if (this.requestInterceptor) { - return this.requestInterceptor(config); + return this.requestInterceptor?.(config) ?? config; + } + + protected async processResponse(response: IHttpClientResponse): Promise> { + return this.responseInterceptor?.(response) ?? response; + } + + protected async processError(error: HttpClientError): Promise { + if (this.errorInterceptor) { + return this.errorInterceptor(error); } - return config; + throw error; } } diff --git a/services/http/src/services/httpClientAxios/httpClientAxios.ts b/services/http/src/services/httpClientAxios/httpClientAxios.ts index 0b6f4ac1..63ca7e29 100644 --- a/services/http/src/services/httpClientAxios/httpClientAxios.ts +++ b/services/http/src/services/httpClientAxios/httpClientAxios.ts @@ -27,7 +27,7 @@ export class HttpClientAxios extends HttpClient { ...processedConfig, signal: combinedSignals }) - .then(response => ({ + .then(response => this.processResponse({ data: response.data, status: response.status, statusText: response.statusText, @@ -45,7 +45,7 @@ export class HttpClientAxios extends HttpClient { message = error.message; } - throw new HttpClientError({ + return this.processError(new HttpClientError({ message: message, code: error.code, response: error.response ? { @@ -55,7 +55,7 @@ export class HttpClientAxios extends HttpClient { headers: Object.fromEntries(Object.entries(error.response.headers)) } : undefined, config: processedConfig - }); + })); }); }; } diff --git a/services/http/src/services/httpClientFetch/httpClientFetch.ts b/services/http/src/services/httpClientFetch/httpClientFetch.ts index c1ee68b4..73a2e861 100644 --- a/services/http/src/services/httpClientFetch/httpClientFetch.ts +++ b/services/http/src/services/httpClientFetch/httpClientFetch.ts @@ -29,19 +29,21 @@ export class HttpClientFetch extends HttpClient { body: JSON.stringify(processedConfig.data), signal: combinedSignals }).catch(error => { + let message: string; + if (error.name === 'AbortError') { - throw new HttpClientError({ - code: error.code, - message: timeoutSignal.aborted ? `Timeout of ${this.timeout}ms exceeded` : 'The request was cancelled', - config: processedConfig - }); + message = timeoutSignal.aborted + ? `Timeout of ${this.timeout}ms exceeded` + : 'The request was cancelled'; + } else { + message = error.message; } - throw new HttpClientError({ + return this.processError(new HttpClientError({ code: error.code, - message: error.message, + message: message, config: processedConfig - }); + })); }); const contentType = response.headers.get('Content-Type'); @@ -57,7 +59,7 @@ export class HttpClientFetch extends HttpClient { } if (!response.ok) { - throw new HttpClientError({ + return this.processError(new HttpClientError({ message: `Request failed with status ${response.status}`, code: `ERR_${response.statusText.toUpperCase().replace(' ', '_')}`, response: { @@ -67,16 +69,16 @@ export class HttpClientFetch extends HttpClient { headers: Object.fromEntries(response.headers.entries()), }, config: processedConfig - }); + })); } - return { + return await this.processResponse({ data: data as R, status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()), config: processedConfig - }; + }); }; } } From 1f1fd2b83f48e1a0e2b1700d2a712072f37ea1ec Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Wed, 11 Jun 2025 19:46:18 +0300 Subject: [PATCH 48/49] feat(http-client): update error interceptor --- services/http/src/services/httpClient.ts | 4 ++-- .../http/src/services/httpClientFetch/httpClientFetch.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/services/http/src/services/httpClient.ts b/services/http/src/services/httpClient.ts index fd21f397..c07536f0 100644 --- a/services/http/src/services/httpClient.ts +++ b/services/http/src/services/httpClient.ts @@ -18,7 +18,7 @@ export interface IHttpClientInit { type TRequestInterceptor = (request: IRequestConfig) => Promise; type TResponseInterceptor = (response: IHttpClientResponse) => Promise>; -type TErrorInterceptor = (error: HttpClientError) => Promise; +type TErrorInterceptor = (error: HttpClientError) => Promise>; export abstract class HttpClient { baseURL: string; @@ -92,7 +92,7 @@ export abstract class HttpClient { return this.responseInterceptor?.(response) ?? response; } - protected async processError(error: HttpClientError): Promise { + protected async processError(error: HttpClientError): Promise> { if (this.errorInterceptor) { return this.errorInterceptor(error); } diff --git a/services/http/src/services/httpClientFetch/httpClientFetch.ts b/services/http/src/services/httpClientFetch/httpClientFetch.ts index 73a2e861..e7c4c053 100644 --- a/services/http/src/services/httpClientFetch/httpClientFetch.ts +++ b/services/http/src/services/httpClientFetch/httpClientFetch.ts @@ -39,11 +39,11 @@ export class HttpClientFetch extends HttpClient { message = error.message; } - return this.processError(new HttpClientError({ + throw new HttpClientError({ code: error.code, message: message, config: processedConfig - })); + }); }); const contentType = response.headers.get('Content-Type'); @@ -59,6 +59,8 @@ export class HttpClientFetch extends HttpClient { } if (!response.ok) { + // TODO: через интерцептор проходят только те ошибки, которые генерируются если response.ok === false, + // надо обсудить достаточно ли этого return this.processError(new HttpClientError({ message: `Request failed with status ${response.status}`, code: `ERR_${response.statusText.toUpperCase().replace(' ', '_')}`, From f1ce532cd76fe623df1297ff3e9f09f4bc362600 Mon Sep 17 00:00:00 2001 From: Viktor Smorodin Date: Mon, 16 Jun 2025 17:41:47 +0300 Subject: [PATCH 49/49] feat(http-client): interceptors and their tests --- services/http/README.md | 51 +++++++++++++-- .../src/__fixtures__/httpClient.fixtures.ts | 2 + .../src/__handlers__/httpClient.handlers.ts | 37 +++++++---- .../src/__tests__/httpClientAxios.tests.ts | 61 +++++++++++++++++- .../src/__tests__/httpClientFetch.tests.ts | 63 ++++++++++++++++++- services/http/src/services/httpClient.ts | 8 +-- services/http/src/services/httpRequest.ts | 14 ++--- 7 files changed, 208 insertions(+), 28 deletions(-) diff --git a/services/http/README.md b/services/http/README.md index 7247df3e..9ceac235 100644 --- a/services/http/README.md +++ b/services/http/README.md @@ -16,6 +16,7 @@ There are two classes ready to be used as http clients: **HttpClientAxios** and ```ts import HttpClientAxios from '@byndyusoft-ui/http'; +import { http } from 'msw'; interface IRequestBody { name: string; @@ -25,13 +26,13 @@ interface IResponse { message: string; } -const httpClient = new HttpClientAxios({ baseUr: 'https://base-url.com' }); +const httpClient = new HttpClientAxios({ baseUrl: 'https://base-url.com' }); const postData = async (name: string): Promise => await httpClient .post() .url('/post-some-data') - .headers({ Authorization: 'Bearer token'}) - .body({ name }) + .headers({Authorization: 'Bearer token'}) + .body({name}) .send(); ``` @@ -44,7 +45,7 @@ const postData = async (name: string): Promise => await httpClient message: string; } - const httpClient = new HttpClientFetch({ baseUr: 'https://base-url.com' }); + const httpClient = new HttpClientFetch({ baseUrl: 'https://base-url.com' }); const getData = async (month: string): Promise => await httpClient .get() @@ -53,3 +54,45 @@ const postData = async (name: string): Promise => await httpClient .params({ month }) .send(); ``` + +### You can add request, response and error interceptors + +```ts + const httpClient = new HttpClientFetch({ baseUr: 'https://base-url.com' }); + + httpClient.setRequestInterceptor(requestConfig => { + // interceptor logic + + return modifiedRequestConfig; + }); + + httpClient.setResponseInterceptor(response => { + // interceptor logic + + return modifiedResponse; + }); +``` +#### Example of token refreshing and request repeating when error 401 is received + +```ts + httpClient.setErrorInterceptor(async error => { + if (error.response?.status === HttpStatusCode.UNAUTHORIZED) { + const { data: { token } } = await httpClient + .get<{ token: string }>() + .url('/get-token-path') + .send(); + + httpClient.setHeader('Authorization', `Bearer ${token}`); + + return httpClient.requestClient({ + method: error.config?.method as HttpMethod, + url: error.config?.url, + headers: { ...error.config?.headers, Authorization: `Bearer ${token}` }, + params: error.config?.params, + body: error.config?.data as Object + }); + } + + throw error; + }); +``` diff --git a/services/http/src/__fixtures__/httpClient.fixtures.ts b/services/http/src/__fixtures__/httpClient.fixtures.ts index 423bfc5f..0ab42653 100644 --- a/services/http/src/__fixtures__/httpClient.fixtures.ts +++ b/services/http/src/__fixtures__/httpClient.fixtures.ts @@ -11,6 +11,8 @@ export const getPath = '/get'; export const getPathWithQueryParams = '/get/with-query'; export const getPathWithError = '/get/with-error'; export const getPathWithTimeout = '/get/with-timeout'; +export const getTokenPath = '/get/token'; +export const getDataWithAuthorizationPath = '/get/with-authorization'; export const postPath = '/post'; export const putPath = '/put'; export const patchPath = '/patch'; diff --git a/services/http/src/__handlers__/httpClient.handlers.ts b/services/http/src/__handlers__/httpClient.handlers.ts index 0cb5d8fa..e0485e5a 100644 --- a/services/http/src/__handlers__/httpClient.handlers.ts +++ b/services/http/src/__handlers__/httpClient.handlers.ts @@ -9,14 +9,17 @@ import { getPathWithQueryParams, getPathWithError, getPathWithTimeout, + getTokenPath, + getDataWithAuthorizationPath, queryParams, errorDetails, successResponse } from '../__fixtures__/httpClient.fixtures'; +import { HttpStatusCode } from '../types/httpStatusCode.types'; export const getRequest = http.get(`${baseUrl}${getPath}`, ({ request }) => { if (request.headers.get('Authorization') !== 'Bearer token' || request.headers.get('Header') !== 'Header value') { - return new HttpResponse(null, { status: 400, statusText: 'Wrong headers' }); + return new HttpResponse(null, { status: HttpStatusCode.BAD_REQUEST, statusText: 'Wrong headers' }); } return HttpResponse.json({ success: true }); @@ -28,7 +31,7 @@ export const getRequestWithQuery = http.get(`${baseUrl}${getPathWithQueryParams} const testParamValue = url.searchParams.get('testParam'); if (!testParamValue) { - return new HttpResponse(null, { status: 404 }); + return new HttpResponse(null, { status: HttpStatusCode.NOT_FOUND }); } return HttpResponse.json({ testParam: testParamValue }); @@ -40,7 +43,7 @@ export const getRequestWithError = http.get(`${baseUrl}${getPathWithError}`, ({ const testParamValue = url.searchParams.get('testParam'); if (testParamValue === queryParams['testParam']) { - return new HttpResponse(JSON.stringify(errorDetails), { status: 400, headers: { 'content-type': 'application/json' } }); + return new HttpResponse(JSON.stringify(errorDetails), { status: HttpStatusCode.BAD_REQUEST, headers: { 'content-type': 'application/json' } }); } return HttpResponse.json(successResponse); @@ -52,6 +55,18 @@ export const getRequestWithTimeout = http.get(`${baseUrl}${getPathWithTimeout}`, return HttpResponse.json(successResponse); }); +export const getToken = http.get(`${baseUrl}${getTokenPath}`, () => HttpResponse.json({ token: 'test_token_01' })); + +export const getDataWithAuthorization = http.get(`${baseUrl}${getDataWithAuthorizationPath}`, async ({ request }) => { + await delay(1000); + + if (request.headers.get('Authorization') !== 'Bearer test_token_01') { + return new HttpResponse(null, { status: HttpStatusCode.UNAUTHORIZED, statusText: 'Unauthorized' }); + } + + return HttpResponse.json(successResponse); +}); + export const postRequest = http.post(`${baseUrl}${postPath}`, async ({ request }) => { const requestBody = await request.clone().json(); @@ -59,7 +74,7 @@ export const postRequest = http.post(`${baseUrl}${postPath}`, async ({ request } return HttpResponse.json(requestBody); } - return new HttpResponse(null, { status: 400, statusText: 'No body' }); + return new HttpResponse(null, { status: HttpStatusCode.BAD_REQUEST, statusText: 'No body' }); }); export const putRequest = http.put(`${baseUrl}${putPath}`, async ({ request }) => { @@ -68,7 +83,7 @@ export const putRequest = http.put(`${baseUrl}${putPath}`, async ({ request }) = const testParamValue = url.searchParams.get('testParam') if (!testParamValue) { - return new HttpResponse(null, { status: 404 }) + return new HttpResponse(null, { status: HttpStatusCode.NOT_FOUND }) } const requestBody = await request.clone().json(); @@ -77,7 +92,7 @@ export const putRequest = http.put(`${baseUrl}${putPath}`, async ({ request }) = return HttpResponse.json(requestBody); } - return new HttpResponse(null, { status: 400, statusText: 'No body' }); + return new HttpResponse(null, { status: HttpStatusCode.BAD_REQUEST, statusText: 'No body' }); }); export const patchRequest = http.patch(`${baseUrl}${patchPath}`, async ({ request }) => { @@ -86,7 +101,7 @@ export const patchRequest = http.patch(`${baseUrl}${patchPath}`, async ({ reques const testParamValue = url.searchParams.get('testParam') if (!testParamValue) { - return new HttpResponse(null, { status: 404 }) + return new HttpResponse(null, { status: HttpStatusCode.NOT_FOUND }) } const requestBody = await request.clone().json(); @@ -95,7 +110,7 @@ export const patchRequest = http.patch(`${baseUrl}${patchPath}`, async ({ reques return HttpResponse.json(requestBody); } - return new HttpResponse(null, { status: 400, statusText: 'No body' }); + return new HttpResponse(null, { status: HttpStatusCode.BAD_REQUEST, statusText: 'No body' }); }); export const deleteRequest = http.delete(`${baseUrl}${deletePath}`, async ({ request }) => { @@ -104,12 +119,12 @@ export const deleteRequest = http.delete(`${baseUrl}${deletePath}`, async ({ req const testParamValue = url.searchParams.get('testParam'); if (!testParamValue) { - return new HttpResponse(null, { status: 404 }); + return new HttpResponse(null, { status: HttpStatusCode.NOT_FOUND }); } if (request.headers.get('Authorization') !== 'Bearer token' || request.headers.get('Header') !== 'Header value') { - return new HttpResponse(null, { status: 400, statusText: 'Wrong headers' }); + return new HttpResponse(null, { status: HttpStatusCode.BAD_REQUEST, statusText: 'Wrong headers' }); } - return new HttpResponse(null, { status: 200 }); + return new HttpResponse(null, { status: HttpStatusCode.OK }); }); diff --git a/services/http/src/__tests__/httpClientAxios.tests.ts b/services/http/src/__tests__/httpClientAxios.tests.ts index 2c4de0da..5ed11b6c 100644 --- a/services/http/src/__tests__/httpClientAxios.tests.ts +++ b/services/http/src/__tests__/httpClientAxios.tests.ts @@ -16,9 +16,10 @@ import { requestBody, successResponse, getPathWithError, - errorDetails + errorDetails, getTokenPath, getDataWithAuthorizationPath } from '../__fixtures__/httpClient.fixtures'; import { HttpStatusCode } from '../types/httpStatusCode.types'; +import { HttpMethod } from '../types/httpMethod.types'; const server = setupServer(); @@ -175,4 +176,62 @@ describe('services/HttpClientAxios', () => { expect(response.status).toEqual(HttpStatusCode.OK); }); + + test('should request interceptor to change authorization header before request', async () => { + server.use(handlers.getToken, handlers.getDataWithAuthorization); + + const httpClientInstance = new HttpClientAxios({ baseURL: baseUrl, headers: { Authorization: 'Bearer token' } }); + + httpClientInstance.setRequestInterceptor(async requestConfig => { + if (requestConfig.url !== getTokenPath) { + const { data: { token: newToken } } = await httpClientInstance.get<{ token: string }>().url(getTokenPath).send(); + + requestConfig.headers = { ...requestConfig.headers, Authorization: `Bearer ${newToken}` }; + + httpClientInstance.setHeader('Authorization', `Bearer ${newToken}`); + } + + return requestConfig; + }); + + const response = await httpClientInstance + .get() + .url(getDataWithAuthorizationPath) + .send(); + + expect(response.data).toEqual(successResponse); + expect(httpClientInstance.headers.Authorization).toEqual('Bearer test_token_01'); + }); + + test('should error interceptor to update token if gets 401 error and retry request', async () => { + server.use(handlers.getToken, handlers.getDataWithAuthorization); + + const httpClientInstance = new HttpClientAxios({ baseURL: baseUrl, headers: { Authorization: 'Bearer token' } }); + + httpClientInstance.setErrorInterceptor(async error => { + if (error.response?.status === HttpStatusCode.UNAUTHORIZED && error.config?.url !== getTokenPath) { + const { data: { token: newToken } } = await httpClientInstance.get<{ token: string }>().url(getTokenPath).send(); + + httpClientInstance.setHeader('Authorization', `Bearer ${newToken}`); + + return httpClientInstance.requestClient({ + method: error.config?.method as HttpMethod, + url: error.config?.url, + headers: { ...error.config?.headers, Authorization: `Bearer ${newToken}` }, + params: error.config?.params, + body: error.config?.data as Object + }); + } + + throw error; + }); + + const response = await httpClientInstance + .get() + .url(getDataWithAuthorizationPath) + .send(); + + expect(response.data).toEqual(successResponse); + expect(httpClientInstance.headers.Authorization).toEqual('Bearer test_token_01'); + }); }); diff --git a/services/http/src/__tests__/httpClientFetch.tests.ts b/services/http/src/__tests__/httpClientFetch.tests.ts index b81e908b..0ac8edc8 100644 --- a/services/http/src/__tests__/httpClientFetch.tests.ts +++ b/services/http/src/__tests__/httpClientFetch.tests.ts @@ -17,9 +17,12 @@ import { requestBody, successResponse, errorDetails, - getPathWithTimeout + getPathWithTimeout, + getDataWithAuthorizationPath, + getTokenPath } from '../__fixtures__/httpClient.fixtures'; import { HttpStatusCode } from '../types/httpStatusCode.types'; +import { HttpMethod } from '../types/httpMethod.types'; const server = setupServer(); @@ -176,4 +179,62 @@ describe('services/HttpClientFetch', () => { expect(response.status).toEqual(HttpStatusCode.OK); }); + + test('should request interceptor to change authorization header before request', async () => { + server.use(handlers.getToken, handlers.getDataWithAuthorization); + + const httpClientInstance = new HttpClientFetch({ baseURL: baseUrl, headers: { Authorization: 'Bearer token' } }); + + httpClientInstance.setRequestInterceptor(async requestConfig => { + if (requestConfig.url !== getTokenPath) { + const { data: { token: newToken } } = await httpClientInstance.get<{ token: string }>().url(getTokenPath).send(); + + requestConfig.headers = { ...requestConfig.headers, Authorization: `Bearer ${newToken}` }; + + httpClientInstance.setHeader('Authorization', `Bearer ${newToken}`); + } + + return requestConfig; + }); + + const response = await httpClientInstance + .get() + .url(getDataWithAuthorizationPath) + .send(); + + expect(response.data).toEqual(successResponse); + expect(httpClientInstance.headers.Authorization).toEqual('Bearer test_token_01'); + }); + + test('should error interceptor to update token if gets 401 error and retry request', async () => { + server.use(handlers.getToken, handlers.getDataWithAuthorization); + + const httpClientInstance = new HttpClientFetch({ baseURL: baseUrl, headers: { Authorization: 'Bearer token' } }); + + httpClientInstance.setErrorInterceptor(async error => { + if (error.response?.status === HttpStatusCode.UNAUTHORIZED && error.config?.url !== getTokenPath) { + const { data: { token: newToken } } = await httpClientInstance.get<{ token: string }>().url(getTokenPath).send(); + + httpClientInstance.setHeader('Authorization', `Bearer ${newToken}`); + + return httpClientInstance.requestClient({ + method: error.config?.method as HttpMethod, + url: error.config?.url, + headers: { ...error.config?.headers, Authorization: `Bearer ${newToken}` }, + params: error.config?.params, + body: error.config?.data as Object + }); + } + + throw error; + }); + + const response = await httpClientInstance + .get() + .url(getDataWithAuthorizationPath) + .send(); + + expect(response.data).toEqual(successResponse); + expect(httpClientInstance.headers.Authorization).toEqual('Bearer test_token_01'); + }); }); diff --git a/services/http/src/services/httpClient.ts b/services/http/src/services/httpClient.ts index c07536f0..fce882bc 100644 --- a/services/http/src/services/httpClient.ts +++ b/services/http/src/services/httpClient.ts @@ -34,7 +34,7 @@ export abstract class HttpClient { this.timeout = timeout ?? DEFAULT_REQUEST_TIMEOUT; } - protected abstract requestClient: (arg: IRequestOptions) => Promise>; + abstract requestClient: (arg: IRequestOptions) => Promise>; static buildQueryString(queryParams: TQueryParams) { const params = Object.keys(queryParams); @@ -72,15 +72,15 @@ export abstract class HttpClient { return new HttpRequest(this.requestClient, HttpMethod.DELETE); } - addRequestInterceptor(interceptor: TRequestInterceptor): void { + setRequestInterceptor(interceptor: TRequestInterceptor): void { this.requestInterceptor = interceptor; } - addResponseInterceptor(interceptor: TResponseInterceptor): void { + setResponseInterceptor(interceptor: TResponseInterceptor): void { this.responseInterceptor = interceptor; } - addErrorInterceptor(interceptor: TErrorInterceptor): void { + setErrorInterceptor(interceptor: TErrorInterceptor): void { this.errorInterceptor = interceptor; } diff --git a/services/http/src/services/httpRequest.ts b/services/http/src/services/httpRequest.ts index 84886d65..2b6af910 100644 --- a/services/http/src/services/httpRequest.ts +++ b/services/http/src/services/httpRequest.ts @@ -2,10 +2,10 @@ import { HttpMethod } from '../types/httpMethod.types'; import { IHttpClientResponse, THeaders, TQueryParams } from '../types/httpClient.types'; export interface IRequestOptions { - url: string; method: HttpMethod; - headers: THeaders; - params: TQueryParams; + url?: string; + headers?: THeaders; + params?: TQueryParams; signal?: AbortSignal; body?: Object; } @@ -16,8 +16,8 @@ export class HttpRequest { protected requestClient: TRequestClient; protected urlValue: string = ''; protected method: HttpMethod; - protected headersValue: THeaders = {}; - protected paramsValue: TQueryParams = {}; + protected headersValue?: THeaders; + protected paramsValue?: TQueryParams; protected abortSignal?: AbortSignal; constructor(requestClient: TRequestClient, method: HttpMethod) { @@ -32,13 +32,13 @@ export class HttpRequest { } headers(headers: THeaders): this { - Object.assign(this.headersValue, headers); + this.headersValue = { ...this.headersValue, ...headers }; return this; } params(queryParams: TQueryParams): this { - Object.assign(this.paramsValue, queryParams); + this.paramsValue = { ...this.paramsValue, ...queryParams }; return this; }