diff --git a/README.md b/README.md index 18be6d87..7b5a7bea 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,12 @@ Our JavaScript client implements a common interface which is implemented at [`@i ### Configuration & Usage +- [Install](#install) +- [Instantiate](#instantiate) and [Use](#use) client +- [Catch Errors](#catch-errors) +- [Configure Agent](#configure-agent) +- [Proxy HTTP Requests](#proxy-requests) + #### Install ```bash @@ -78,26 +84,49 @@ try { } ``` +#### Configure HTTP Agent + +`core-node` uses [got](https://github.com/sindresorhus/got) as its underlying HTTP client. The Ideal Postcodes API client can also be optionally configured with a [got](https://github.com/sindresorhus/got) options object which is fed to [got](https://github.com/sindresorhus/got) on every request. + +Be aware this options object will overwrite any existing [got](https://github.com/sindresorhus/got) HTTP request parameters. + +```javascript +const client = new Client({ api_key: "iddqd" }, { + cache: new Map, // Instantiate a cache: https://github.com/sindresorhus/got#cache-1 + hooks: { // Hook into HTTP responses: https://github.com/sindresorhus/got#hooksafterresponse + afterResponse: response => { + log(response); + return response; + } + }, +}); +``` + +#### Proxy HTTP Requests + +You can [proxy requests](https://github.com/sindresorhus/got#proxies) by configuring the underlying [got](https://github.com/sindresorhus/got) HTTP client. + +```javascript +const tunnel = require("tunnel"); + +const client = new Client(config, { + agent: tunnel.httpOverHttp({ + proxy: { + host: "localhost" + } + }) +}); +``` + --- ### Quickstart The client exposes a number of simple methods to get at the most common tasks when interacting with the API. Below is a (incomplete) list of commonly used methods. -- [Links](#links) -- [Other JavaScript Clients](#other-javascript-clients) -- [Documentation](#documentation) - - [Configuration & Usage](#configuration--usage) - - [Install](#install) - - [Instantiate](#instantiate) - - [Use](#use) - - [Catch Errors](#catch-errors) - - [Quickstart](#quickstart) - - [Lookup a Postcode](#lookup-a-postcode) - - [Search for an Address](#search-for-an-address) - - [Search for an Address by UDPRN](#search-for-an-address-by-udprn) -- [Test](#test) -- [Licence](#licence) +- [Lookup a Postcode](#lookup-a-postcode) +- [Search for an Address](#search-for-an-address) +- [Search for an Address by UDPRN](#search-for-an-address-by-udprn) For a complete list of client methods, including low level resource methods, please see the [core-interface documentation](https://core-interface.ideal-postcodes.dev/#documentation) diff --git a/lib/agent.ts b/lib/agent.ts index 8e9bfa3f..7bfc6aef 100644 --- a/lib/agent.ts +++ b/lib/agent.ts @@ -1,4 +1,4 @@ -import got, { GotInstance, Response } from "got"; +import got, { GotInstance, Response, GotJSONOptions } from "got"; import { Agent as IAgent, HttpRequest, @@ -15,6 +15,14 @@ interface StringMap { [key: string]: string; } +/** + * GotConfig + * + * An optional configuration object which is passed to the underlying got http + * client + */ +export type GotConfig = Partial; + // Converts a Got header object to one that can be used by the client export const toHeader = (gotHeaders: GotHeaders): StringMap => { return Object.keys(gotHeaders).reduce( @@ -55,9 +63,11 @@ const handleError = (error: Error): Promise => { export class Agent implements IAgent { public got: GotInstance; + public gotConfig: GotConfig; - constructor() { + constructor(gotConfig: GotConfig = {}) { this.got = got; + this.gotConfig = gotConfig; } private requestWithBody(httpRequest: HttpRequest): Promise { @@ -70,6 +80,7 @@ export class Agent implements IAgent { throwHttpErrors: false, body, timeout, + ...this.gotConfig, }) .then(response => toHttpResponse(httpRequest, response)) .catch(handleError); @@ -84,6 +95,7 @@ export class Agent implements IAgent { timeout, throwHttpErrors: false, json: true, + ...this.gotConfig, }) .then(response => toHttpResponse(httpRequest, response)) .catch(handleError); diff --git a/lib/client.ts b/lib/client.ts index cb022717..90dfe629 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -7,7 +7,7 @@ import { TIMEOUT, STRICT_AUTHORISATION, } from "@ideal-postcodes/core-interface"; -import { Agent } from "./agent"; +import { Agent, GotConfig } from "./agent"; const userAgent = `IdealPostcodes ideal-postcodes/core-node`; @@ -16,8 +16,16 @@ interface Config extends Partial { } export class Client extends CoreInterface { - constructor(config: Config) { - const agent = new Agent(); + /** + * Client constructor extends CoreInterface by also accepting an optional got + * configuration object as the second argument. + * + * got is the underlying HTTP client that powers core-node. Be careful when + * configuring gotConfig so as not to manually override critical request + * attributes like method, query, header, etc. + */ + constructor(config: Config, gotConfig: GotConfig = {}) { + const agent = new Agent(gotConfig); const header = { "User-Agent": userAgent }; const tls = config.tls === undefined ? TLS : config.tls; const baseUrl = config.baseUrl === undefined ? API_URL : config.baseUrl; diff --git a/test/agent.ts b/test/agent.ts index 379315c9..ed82dfc2 100644 --- a/test/agent.ts +++ b/test/agent.ts @@ -13,6 +13,19 @@ describe("Agent", () => { agent = new Agent(); }); + describe("Agent class", () => { + it("allows for optional got configuration", () => { + const a = new Agent(); + assert.deepEqual(a.gotConfig, {}); + }); + + it("assigns GOT config", () => { + const retry = 2; + const a = new Agent({ retry }); + assert.deepEqual({ retry }, a.gotConfig); + }); + }); + describe("toHeader", () => { it("coerces a Got header object into an object of strings", () => { const gotHeader = { @@ -129,6 +142,81 @@ describe("Agent", () => { timeout: 1000, } as any); }); + + describe("GOT Configuration", () => { + const timeout = 2000; + const retry = 2; + + beforeEach(() => { + agent = new Agent({ timeout, retry }); + }); + + it("overrides HTTP configuration for GET requests", async () => { + const method: HttpVerb = "GET"; + const query = { foo: "bar" }; + const header = { baz: "quux" }; + const url = "http://www.foo.com/"; + const SUCCESS = 200; + + const response: unknown = { + statusCode: SUCCESS, + headers: header, + body: Buffer.from("{}"), + url, + }; + + const stub = sinon + .stub(agent, "got") + .resolves(response as Response); + + await agent.http({ method, timeout: 1000, url, header, query }); + + sinon.assert.calledOnce(stub); + sinon.assert.calledWithExactly(stub, url, { + method, + headers: header, + throwHttpErrors: false, + query, + json: true, + timeout, + retry, + } as any); + }); + + it("overrides HTTP configuration for POST requests", async () => { + const method: HttpVerb = "POST"; + const query = { foo: "bar" }; + const header = { baz: "quux" }; + const url = "http://www.foo.com/"; + const SUCCESS = 200; + const body = { foo: "bar" }; + + const response: unknown = { + statusCode: SUCCESS, + headers: header, + body: Buffer.from("{}"), + url, + }; + + const stub = sinon + .stub(agent, "got") + .resolves(response as Response); + + await agent.http({ body, method, timeout: 1000, url, header, query }); + + sinon.assert.calledOnce(stub); + sinon.assert.calledWithExactly(stub, url, { + method, + throwHttpErrors: false, + json: true, + body, + headers: header, + query, + retry, + timeout, + } as any); + }); + }); }); describe("Error handling", () => { diff --git a/test/client.ts b/test/client.ts index 39924170..60c0b4ac 100644 --- a/test/client.ts +++ b/test/client.ts @@ -25,6 +25,7 @@ describe("Client", () => { assert.equal(client.strictAuthorisation, STRICT_AUTHORISATION); assert.equal(client.timeout, TIMEOUT); }); + it("allows default config values to be overwritten", () => { const options = { tls: false, @@ -44,9 +45,17 @@ describe("Client", () => { options.strictAuthorisation ); assert.equal(customClient.timeout, options.timeout); + assert.deepEqual((customClient.agent as any).gotConfig, {}); }); + it("assigns user agent header", () => { assert.match(client.header["User-Agent"], /Core-Node/i); }); + + it("assigns got config", () => { + const retry = 2; + const customClient = new Client({ api_key }, { retry }); + assert.deepEqual((customClient.agent as any).gotConfig, { retry }); + }); }); });