From 71a925abe1462aab7684ec0604490136a3c4b4a2 Mon Sep 17 00:00:00 2001 From: jlenon7 Date: Sun, 4 Feb 2024 23:39:00 +0000 Subject: [PATCH] feat(client): add new methods --- package-lock.json | 4 +- package.json | 2 +- src/helpers/HttpClient.ts | 154 ++++++++++++++++++---------- src/types/http-client/Request.ts | 24 ++++- tests/unit/HttpClientTest.ts | 169 ++++++++++++++++++++++++++++++- 5 files changed, 290 insertions(+), 63 deletions(-) diff --git a/package-lock.json b/package-lock.json index 94687a2..a2d0ca0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/common", - "version": "4.29.0", + "version": "4.30.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athenna/common", - "version": "4.29.0", + "version": "4.30.0", "license": "MIT", "dependencies": { "@fastify/formbody": "^7.4.0", diff --git a/package.json b/package.json index cb9f88c..363cf8d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/common", - "version": "4.29.0", + "version": "4.30.0", "description": "The Athenna common helpers to use in any Node.js ESM project.", "license": "MIT", "author": "João Lenon ", diff --git a/src/helpers/HttpClient.ts b/src/helpers/HttpClient.ts index 37f759b..31419f4 100644 --- a/src/helpers/HttpClient.ts +++ b/src/helpers/HttpClient.ts @@ -16,13 +16,7 @@ import { Json } from '#src/helpers/Json' import type { Store } from 'keyv' import type { ClientHttp2Session } from 'http2' -import type { - Body, - Query, - Request, - Response, - RetryStrategyCallback -} from '#src/types' +import type { Request, Response, RetryStrategyCallback } from '#src/types' import type { Hooks, @@ -404,25 +398,42 @@ export class HttpClientBuilder { } /** - * Set the request body. + * Set a key value to the request body. */ - public body(body: Body): HttpClientBuilder { - if (Is.Object(body)) { - this.options.json = body + public body(key: any, value?: any): HttpClientBuilder { + if (!this.options.body) { + this.options.body = {} as any + } + + if (Is.Undefined(value)) { + this.options.body = key return this } - this.options.body = body as any + this.options.body[key] = value return this } /** - * Set the request form. + * Set a key value to the request body. + */ + public input(key: string, value: any): HttpClientBuilder { + return this.body(key, value) + } + + /** + * Execute the given closure only when the value is + * truthy. */ - public form(form: any | Record): HttpClientBuilder { - this.options.form = form + public when( + value: T, + closure: (builder?: HttpClientBuilder, value?: T) => any + ): HttpClientBuilder { + if (value) { + closure(this, value) + } return this } @@ -430,11 +441,17 @@ export class HttpClientBuilder { /** * Set a header at the request. */ - public header(key: string, value: string): HttpClientBuilder { + public header(key: any, value?: any): HttpClientBuilder { if (!this.options.headers) { this.options.headers = {} } + if (Is.Undefined(value)) { + this.options.headers = key + + return this + } + this.options.headers[key] = value return this @@ -444,7 +461,7 @@ export class HttpClientBuilder { * Set a header at the request only if is not already * defined. */ - public safeHeader(key: string, value: string): HttpClientBuilder { + public safeHeader(key: string, value: any): HttpClientBuilder { if (!this.options.headers) { this.options.headers = {} } @@ -453,9 +470,7 @@ export class HttpClientBuilder { return this } - this.options.headers[key] = value - - return this + return this.header(key, value) } /** @@ -463,14 +478,14 @@ export class HttpClientBuilder { */ public removeHeader(key: string): HttpClientBuilder { if (!this.options.headers) { - this.options.headers = {} + return this } if (!this.options.headers[key]) { return this } - delete this.options.headers[key] + this.options.headers = Json.omit(this.options.headers, [key]) return this } @@ -595,8 +610,18 @@ export class HttpClientBuilder { * //=> 'key=a&key=b' * ``` */ - public searchParams(value: Query): HttpClientBuilder { - this.options.searchParams = value + public searchParams(key: any, value?: any): HttpClientBuilder { + if (!this.options.searchParams) { + this.options.searchParams = {} + } + + if (Is.Undefined(value)) { + this.options.searchParams = key + + return this + } + + this.options.searchParams[key] = value return this } @@ -604,8 +629,39 @@ export class HttpClientBuilder { /** * Alias for the searchParams method. */ - public queryParams(value: Query): HttpClientBuilder { - return this.searchParams(value) + public query(key: any, value?: any): HttpClientBuilder { + return this.searchParams(key, value) + } + + /** + * Set a query at the request only if is not already + * defined. + */ + public safeQuery(key: string, value: any): HttpClientBuilder { + if (this.options.searchParams[key]) { + return this + } + + return this.searchParams(key, value) + } + + /** + * Remove a query from the request. + */ + public removeQuery(key: string): HttpClientBuilder { + if (!this.options.searchParams) { + return this + } + + if (!this.options.searchParams[key]) { + return this + } + + this.options.searchParams = Json.omit(this.options.searchParams as any, [ + key + ]) + + return this } /** @@ -1066,7 +1122,14 @@ export class HttpClientBuilder { * Execute the request using all the options defined. */ public request(options: Request = {}): Response { - return got({ ...this.options, ...options } as any) + options = { ...this.options, ...options } + + if (options.body && Is.Object(options.body)) { + options.json = Json.copy(options.body) + options.body = undefined + } + + return got(options as any) } /** @@ -1081,34 +1144,27 @@ export class HttpClientBuilder { /** * Make a POST request. */ - public post(url?: string | URL, body?: Body, options: Request = {}) { + public post(url?: string | URL, options: Request = {}) { return this.method('POST') .url(url || options.url || this.options.url) - .body(body || options.body || this.options.body || {}) .request(options) } /** * Make a PUT request. */ - public put(url?: string | URL, body?: Body, options: Request = {}) { + public put(url?: string | URL, options: Request = {}) { return this.method('PUT') .url(url || options.url || this.options.url) - .body(body || options.body || this.options.body || {}) .request(options) } /** * Make a PATCH request. */ - public patch( - url?: string | URL, - body?: Response, - options: Request = {} - ) { + public patch(url?: string | URL, options: Request = {}) { return this.method('PATCH') .url(url || options.url || this.options.url) - .body(body || options.body || this.options.body || {}) .request(options) } @@ -1168,34 +1224,22 @@ export class HttpClient { /** * Make a POST request. */ - public static post( - url?: string | URL, - body?: Body, - options?: Request - ) { - return this._builder.post(url, body, options) + public static post(url?: string | URL, options?: Request) { + return this._builder.post(url, options) } /** * Make a PUT request. */ - public static put( - url?: string | URL, - body?: Body, - options?: Request - ) { - return this._builder.put(url, body, options) + public static put(url?: string | URL, options?: Request) { + return this._builder.put(url, options) } /** * Make a PATCH request. */ - public static patch( - url?: string | URL, - body?: Body, - options?: Request - ) { - return this._builder.patch(url, body, options) + public static patch(url?: string | URL, options?: Request) { + return this._builder.patch(url, options) } /** diff --git a/src/types/http-client/Request.ts b/src/types/http-client/Request.ts index 1780b49..89af0f2 100644 --- a/src/types/http-client/Request.ts +++ b/src/types/http-client/Request.ts @@ -7,11 +7,33 @@ * file that was distributed with this source code. */ +import { Readable } from 'node:stream' import type { Except } from '#src/types' import type { OptionsInit, ResponseType } from 'got' +import type { FormDataLike } from 'form-data-encoder' -export type Request = Except & +export type Request = Except & Partial<{ + get body(): + | string + | Record + | Buffer + | Readable + | Generator + | AsyncGenerator + | FormDataLike + | undefined + set body( + value: + | string + | Record + | Buffer + | Readable + | Generator + | AsyncGenerator + | FormDataLike + | undefined + ) get responseType(): ResponseType | string set responseType(value: ResponseType | string) }> diff --git a/tests/unit/HttpClientTest.ts b/tests/unit/HttpClientTest.ts index 456896a..7318501 100644 --- a/tests/unit/HttpClientTest.ts +++ b/tests/unit/HttpClientTest.ts @@ -143,21 +143,21 @@ export default class HttpClientTest { @Test() public async shouldBeAbleToMakeAPOSTRequestUsingHttpClient({ assert }: Context) { - const userCreated = await HttpClient.post('/users', { name: 'Robson Trasel' }).json() + const userCreated = await HttpClient.post('/users', { body: { name: 'Robson Trasel' } }).json() assert.deepEqual(userCreated, { id: 1, name: 'Robson Trasel' }) } @Test() public async shouldBeAbleToMakeAPUTRequestUsingHttpClient({ assert }: Context) { - const userUpdated = await HttpClient.put('/users/1', { name: 'Robson Trasel Updated' }).json() + const userUpdated = await HttpClient.put('/users/1', { body: { name: 'Robson Trasel Updated' } }).json() assert.deepEqual(userUpdated, { id: 1, name: 'Robson Trasel Updated' }) } @Test() public async shouldBeAbleToMakeAPATCHRequestUsingHttpClient({ assert }: Context) { - const userUpdated = await HttpClient.patch('/users/1', { name: 'Robson Trasel Updated Patch' }).json() + const userUpdated = await HttpClient.patch('/users/1', { body: { name: 'Robson Trasel Updated Patch' } }).json() assert.deepEqual(userUpdated, { id: 1, name: 'Robson Trasel Updated Patch' }) } @@ -275,7 +275,7 @@ export default class HttpClientTest { options.headers['Content-Type'] = 'application/x-www-form-urlencoded' }) - await builder.post('users', '{"payload":"old"}') + await builder.body('{"payload":"old"}').post('users') } @Test() @@ -331,4 +331,165 @@ export default class HttpClientTest { await File.safeRemove(Path.fixtures('streamed.json')) } + + @Test() + public async shouldBeAbleToSetAJsonBodyInARequest({ assert }: Context) { + assert.plan(2) + + const user = await HttpClient.builder() + .setBeforeRequestHook(options => { + assert.deepEqual(options.body, '{"name":"Robson Trasel"}') + }) + .body({ name: 'Robson Trasel' }) + .post('/users') + .json() + + assert.deepEqual(user, { id: 1, name: 'Robson Trasel' }) + } + + @Test() + public async shouldBeAbleToSetAJsonUsingInputMethodInARequest({ assert }: Context) { + assert.plan(2) + + const user = await HttpClient.builder() + .setBeforeRequestHook(options => { + assert.deepEqual(options.body, '{"name":"Robson Trasel"}') + }) + .input('name', 'Robson Trasel') + .post('/users') + .json() + + assert.deepEqual(user, { id: 1, name: 'Robson Trasel' }) + } + + @Test() + public async shouldBeAbleToSetMultipleBodyValuesInRequest({ assert }: Context) { + assert.plan(2) + + const user = await HttpClient.builder() + .setBeforeRequestHook(options => { + assert.deepEqual(options.body, '{"id":1,"name":"Robson Trasel"}') + }) + .body('id', 1) + .body('name', 'Robson Trasel') + .post('/users') + .json() + + assert.deepEqual(user, { id: 1, name: 'Robson Trasel' }) + } + + @Test() + public async shouldBeAbleToOverwriteTheBodySettingOnlyOneValue({ assert }: Context) { + assert.plan(2) + + const user = await HttpClient.builder() + .setBeforeRequestHook(options => { + assert.deepEqual(options.body, '{"name":"Robson Trasel"}') + }) + .body('id', 1) + .body('name', 'Robson Trasel') + .body({ name: 'Robson Trasel' }) + .post('/users') + .json() + + assert.deepEqual(user, { id: 1, name: 'Robson Trasel' }) + } + + @Test() + public async shouldBeAbleToSetAQueryParamInRequest({ assert }: Context) { + assert.plan(2) + + const users = await HttpClient.builder() + .setBeforeRequestHook(options => { + assert.deepEqual( + options.searchParams, + new URLSearchParams([ + ['page', '1'], + ['limit', '10'] + ]) + ) + }) + .query('page', '1') + .query('limit', '10') + .get('/users') + .json() + + assert.deepEqual(users, [ + { id: 1, name: 'Robson Trasel' }, + { id: 2, name: 'Victor Tesoura' } + ]) + } + + @Test() + public async shouldBeAbleToOverwriteTheQueryInRequest({ assert }: Context) { + assert.plan(2) + + const users = await HttpClient.builder() + .setBeforeRequestHook(options => { + assert.deepEqual( + options.searchParams, + new URLSearchParams([ + ['page', '1'], + ['limit', '10'] + ]) + ) + }) + .query('page', '1') + .query('limit', '10') + .query({ page: '1', limit: '10' }) + .get('/users') + .json() + + assert.deepEqual(users, [ + { id: 1, name: 'Robson Trasel' }, + { id: 2, name: 'Victor Tesoura' } + ]) + } + + @Test() + public async shouldBeAbleToExecuteClosureWhenFirstValueIsDefined({ assert }: Context) { + assert.plan(3) + + const users = await HttpClient.builder() + .setBeforeRequestHook(options => { + assert.deepEqual( + options.searchParams, + new URLSearchParams([ + ['page', '1'], + ['limit', '10'] + ]) + ) + }) + .when(true, (builder, value) => { + assert.isTrue(value) + builder.query({ page: '1', limit: '10' }) + }) + .get('/users') + .json() + + assert.deepEqual(users, [ + { id: 1, name: 'Robson Trasel' }, + { id: 2, name: 'Victor Tesoura' } + ]) + } + + @Test() + public async shouldNotExecuteClosureWhenFirstValueIsFalse({ assert }: Context) { + assert.plan(2) + + const users = await HttpClient.builder() + .setBeforeRequestHook(options => { + assert.deepEqual(options.searchParams, new URLSearchParams()) + }) + .when(false, builder => { + builder.query({ page: '1', limit: '10' }) + }) + .get('/users') + .json() + + assert.deepEqual(users, [ + { id: 1, name: 'Robson Trasel' }, + { id: 2, name: 'Victor Tesoura' } + ]) + } }