diff --git a/package-lock.json b/package-lock.json index 324841f..beac9ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -374,7 +374,6 @@ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" @@ -2026,7 +2025,6 @@ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -2706,6 +2704,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz", "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -2833,6 +2832,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -3086,8 +3086,7 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/core-js-compat": { "version": "3.46.0", @@ -3543,7 +3542,6 @@ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -3940,7 +3938,6 @@ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "json5": "lib/cli.js" }, @@ -4281,6 +4278,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4484,6 +4482,7 @@ "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -4870,7 +4869,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.20.6", @@ -4898,6 +4898,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/ClientBuilder.js b/src/ClientBuilder.js index ae30f31..51ff894 100644 --- a/src/ClientBuilder.js +++ b/src/ClientBuilder.js @@ -7,6 +7,7 @@ const SharedCredentials = require("./SharedCredentials"); const CustomHeaderSender = require("./CustomHeaderSender"); const StatusCodeSender = require("./StatusCodeSender"); const LicenseSender = require("./LicenseSender"); +const CustomQuerySender = require("@/src/CustomQuerySender"); const BadCredentialsError = require("./Errors").BadCredentialsError; const RetrySender = require("./RetrySender"); const Sleeper = require("./util/Sleeper.ts"); @@ -39,7 +40,7 @@ const INTERNATIONAL_POSTAL_CODE_API_URL = "https://international-postal-code.api */ class ClientBuilder { constructor(signer) { - if (noCredentialsProvided()) throw new BadCredentialsError(); + if (!credentialsProvided()) throw new BadCredentialsError(); this.signer = signer; this.httpSender = undefined; @@ -50,15 +51,16 @@ class ClientBuilder { this.customHeaders = {}; this.debug = undefined; this.licenses = []; + this.customQueries = new Map(); - function noCredentialsProvided() { - return (!signer) instanceof StaticCredentials || (!signer) instanceof SharedCredentials; + function credentialsProvided() { + return signer instanceof StaticCredentials || signer instanceof SharedCredentials; } } /** * @param retries The maximum number of times to retry sending the request to the API. (Default is 5) - * @return Returns this to accommodate method chaining. + * @return ClientBuilder this to accommodate method chaining. */ withMaxRetries(retries) { this.maxRetries = retries; @@ -68,7 +70,7 @@ class ClientBuilder { /** * @param timeout The maximum time (in milliseconds) to wait for a connection, and also to wait for
* the response to be read. (Default is 10000) - * @return Returns this to accommodate method chaining. + * @return ClientBuilder this to accommodate method chaining. */ withMaxTimeout(timeout) { this.maxTimeout = timeout; @@ -77,7 +79,7 @@ class ClientBuilder { /** * @param sender Default is a series of nested senders. See buildSender(). - * @return Returns this to accommodate method chaining. + * @return ClientBuilder this to accommodate method chaining. */ withSender(sender) { this.httpSender = sender; @@ -87,7 +89,7 @@ class ClientBuilder { /** * This may be useful when using a local installation of the Smarty APIs. * @param url Defaults to the URL for the API corresponding to the Client object being built. - * @return Returns this to accommodate method chaining. + * @return ClientBuilder this to accommodate method chaining. */ withBaseUrl(url) { this.baseUrl = url; @@ -101,7 +103,7 @@ class ClientBuilder { * @param protocol The protocol on the proxy server to which you wish to connect. If the proxy server uses HTTPS, then you must set the protocol to 'https'. * @param username The username to login to the proxy. * @param password The password to login to the proxy. - * @return Returns this to accommodate method chaining. + * @return ClientBuilder this to accommodate method chaining. */ withProxy(host, port, protocol, username, password) { this.proxy = { @@ -123,35 +125,68 @@ class ClientBuilder { /** * Use this to add any additional headers you need. * @param customHeaders A String to Object Map of header name/value pairs. - * @return Returns this to accommodate method chaining. + * @return ClientBuilder this to accommodate method chaining. */ withCustomHeaders(customHeaders) { this.customHeaders = customHeaders; - return this; } /** * Enables debug mode, which will print information about the HTTP request and response to console.log - * @return Returns this to accommodate method chaining. + * @return ClientBuilder this to accommodate method chaining. */ withDebug() { this.debug = true; - return this; } /** * Allows the caller to specify the subscription license (aka "track") they wish to use. * @param licenses A String Array of licenses. - * @returns Returns this to accommodate method chaining. + * @returns ClientBuilder this to accommodate method chaining. */ withLicenses(licenses) { this.licenses = licenses; + return this; + } + + /** + * Allows the caller to specify key and value pair that is added to the request + * @param {string} key - The query parameter key + * @param {string} value - The query parameter value + * @return ClientBuilder this to accommodate method chaining. + */ + withCustomQuery(key, value) { + this.customQueries.set(key, value); + return this; + } + /** + * Allows the caller to specify key and value pair and appends the value associated with the key, seperated by a comma. + * @param {string} key - The query parameter key + * @param {string} value - The query parameter value + * @return ClientBuilder this to accommodate method chaining. + */ + withCustomCommaSeperatedQuery(key, value) { + let values = this.customQueries.get(key); + if (values === "") { + values = value; + } else { + values += "," + value; + } + this.customQueries.set(key, values); return this; } + /** + * Adds to the request query to use the component analysis feature. + * @return ClientBuilder this to accommodate method chaining. + */ + withFeatureComponentAnalysis() { + return this.withCustomCommaSeperatedQuery("features", "component-analysis"); + } + buildSender() { if (this.httpSender) return this.httpSender; @@ -166,8 +201,9 @@ class ClientBuilder { const customHeaderSender = new CustomHeaderSender(agentSender, this.customHeaders); const baseUrlSender = new BaseUrlSender(customHeaderSender, this.baseUrl); const licenseSender = new LicenseSender(baseUrlSender, this.licenses); + const customQuerySender = new CustomQuerySender(licenseSender, this.customQueries); - return licenseSender; + return customQuerySender; } buildClient(baseUrl, Client) { diff --git a/src/CustomQuerySender.ts b/src/CustomQuerySender.ts new file mode 100644 index 0000000..c587aa4 --- /dev/null +++ b/src/CustomQuerySender.ts @@ -0,0 +1,20 @@ +import { Request, Response, Sender } from "./types"; + +export default class CustomQuerySender { + private queries: Map; + private sender: Sender; + + constructor(innerSender: Sender, queries: Map) { + this.queries = queries; + this.sender = innerSender; + } + + send(request: Request): Promise { + this.queries.forEach((value, key) => { + const existingValue = request.parameters[key]; + request.parameters[key] = existingValue ? `${existingValue},${value}` : value; + }); + + return this.sender.send(request); + } +} diff --git a/tests/test_CustomQuerySender.ts b/tests/test_CustomQuerySender.ts new file mode 100644 index 0000000..72b1ef6 --- /dev/null +++ b/tests/test_CustomQuerySender.ts @@ -0,0 +1,33 @@ +import { expect } from "chai"; +import CustomQuerySender from "../src/CustomQuerySender"; +import Request from "../src/Request.js"; +import Response from "../src/Response.js"; +import { Sender } from "@/src/types"; + +describe("A custom query sender", function () { + it("adds custom query parameters to the request.", function () { + class MockSender implements Sender { + request?: Request; + + send = (request: Request): Promise => { + this.request = request; + return Promise.resolve(new Response(200, {})); + }; + } + + const mockSender = new MockSender(); + const customQueries = new Map([ + ["a", "1"], + ["b", "2"], + ]); + const customQuerySender = new CustomQuerySender(mockSender, customQueries); + const request = new Request(); + + customQuerySender.send(request); + + expect("a" in mockSender.request!.parameters).to.equal(true); + expect((mockSender.request!.parameters as Record)["a"]).to.equal("1"); + expect("b" in mockSender.request!.parameters).to.equal(true); + expect((mockSender.request!.parameters as Record)["b"]).to.equal("2"); + }); +});