Skip to content
This repository has been archived by the owner on Feb 18, 2021. It is now read-only.

Commit

Permalink
Added GetAllContactMessages feature. Added GetSingleContactMessage fe…
Browse files Browse the repository at this point in the history
…ature. Restructured request interface.
  • Loading branch information
jusexton committed Jul 13, 2020
1 parent 0b1f16b commit ddad085
Show file tree
Hide file tree
Showing 12 changed files with 357 additions and 136 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# 0.3.0

- Contact client is now able to retrieve multiple contact messages
- Contact client is now able to retrieve a specific contact message
- Portfolio client can now be configured with an authentication token
- Restructured request interface to be more fine grained
- Client configs are now wrapped to provide additional functionality off of configurations

# 0.2.0

- Added security client
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@jsextonn/portfolio-api-client",
"version": "0.2.0",
"version": "0.3.0",
"description": "NodeJS client for portfolio API",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
89 changes: 75 additions & 14 deletions src/apis/contact/client.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,51 @@
import axios, { AxiosInstance, AxiosResponse } from "axios";
import { BaseClient, ClientConfig } from "../../common/client";
import { axiosRequestConfig, PortfolioRequest } from "../../common/request";
import {
PortfolioRequest,
RequestWithBody,
RequestWithParameters,
} from "../../common/request";
import { PortfolioResponse } from "../../common/response";
import { ContactMessage, Reason, Sender } from "./models";

export type CreateContactMessageRequest = PortfolioRequest<
CreateContactMessageForm
>;
import {
ContactMessage,
ContactMessageCollection,
Reason,
Sender,
} from "./models";

export interface CreateContactMessageForm {
message: string;
reason: Reason;
sender: Sender;
}

export type CreateContactMessageRequest = RequestWithBody<
CreateContactMessageForm
>;

export interface GetContactMessageRequest extends PortfolioRequest {
id: string;
}

export interface GetContactMessageQueryParameters {
reason?: Reason;
archived?: boolean;
responded?: boolean;
}

export type GetContactMessagesRequest = RequestWithParameters<
GetContactMessageQueryParameters
>;

export type ContactMessageResponse = PortfolioResponse<ContactMessage>;

export type ContactMessagesResponse = PortfolioResponse<
ContactMessageCollection
>;

/**
* Client used to interface with contact API
*/
export class ContactClient extends BaseClient {
constructor(config: ClientConfig, axiosInstance: AxiosInstance = axios) {
super(config, axiosInstance);
Expand All @@ -29,13 +59,44 @@ export class ContactClient extends BaseClient {
createMessage(
request: CreateContactMessageRequest
): Promise<AxiosResponse<ContactMessageResponse>> {
const url = `${this.config.host}/contact/mail`;
const config = axiosRequestConfig(request);

return this.axiosInstance.post<ContactMessageResponse>(
url,
request.body,
config
);
const config = this.config.merge(request as PortfolioRequest);
const url = `${config.host}/contact/mail`;

return this.axiosInstance.post<ContactMessageResponse>(url, request.body, {
headers: config.headers,
});
}

/**
* Retrieves a specific contact message by id
*
* @param request
*/
findMessage(
request: GetContactMessageRequest
): Promise<AxiosResponse<ContactMessageResponse>> {
const config = this.config.merge(request as PortfolioRequest);
const url = `${config.host}/contact/mail/${request.id}`;

return this.axiosInstance.get<ContactMessageResponse>(url, {
headers: config.headers,
});
}

/**
* Retrieves multiple contact messages
*
* @param request
*/
findMessages(
request?: GetContactMessagesRequest
): Promise<AxiosResponse<ContactMessagesResponse>> {
const config = this.config.merge(request as PortfolioRequest);
const url = `${config.host}/contact/mail`;

return this.axiosInstance.get<ContactMessagesResponse>(url, {
params: request !== undefined ? request.queryParameters : {},
headers: config.headers,
});
}
}
5 changes: 5 additions & 0 deletions src/apis/contact/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export interface ContactMessage extends Entity {
lastUpdated: Date;
}

export interface ContactMessageCollection {
count: number;
contactMessages: ContactMessage[];
}

export enum Reason {
// TODO: In future API releases, case will not matter
Business = "business",
Expand Down
24 changes: 16 additions & 8 deletions src/apis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ import { ClientConfig } from "../common/client";
import { ContactClient } from "./contact/client";
import { SecurityClient } from "./security/client";

const defaultConfig: ClientConfig = {
host: "https://api.justinsexton.net",
};

export class PortfolioClient {
readonly config: ClientConfig;
readonly contact: ContactClient;
Expand All @@ -18,13 +14,25 @@ export class PortfolioClient {
}
}

const defaultConfig = { host: "https://api.justinsexton.net" };

const mergeConfigs = (
configOne: ClientConfig,
configTwo: ClientConfig
): ClientConfig => {
return {
...configTwo,
...configOne,
};
};

/**
* Factory method used for configuring and building a new contact client.
*/
export const portfolio = (
config: ClientConfig = defaultConfig
): PortfolioClient => {
return new PortfolioClient(config);
export const portfolio = (config?: ClientConfig): PortfolioClient => {
const resolvedConfig =
config !== undefined ? mergeConfigs(config, defaultConfig) : defaultConfig;
return new PortfolioClient(resolvedConfig);
};

export * from "./contact/client";
Expand Down
22 changes: 13 additions & 9 deletions src/apis/security/client.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import axios, { AxiosInstance, AxiosResponse } from "axios";
import { BaseClient, ClientConfig, PortfolioResponse } from "../../common";
import { axiosRequestConfig, PortfolioRequest } from "../../common/request";
import { PortfolioRequest, RequestWithBody } from "../../common/request";
import { TokenBody } from "./models";

export type LoginRequest = PortfolioRequest<LoginForm>;
export type LoginRequest = RequestWithBody<LoginForm>;

export interface LoginForm {
username: string;
Expand All @@ -12,7 +12,7 @@ export interface LoginForm {

export type LoginResponse = PortfolioResponse<TokenBody>;

export type UpdatePasswordRequest = PortfolioRequest<UpdatePasswordForm>;
export type UpdatePasswordRequest = RequestWithBody<UpdatePasswordForm>;

export interface UpdatePasswordForm {
username: string;
Expand All @@ -26,18 +26,22 @@ export class SecurityClient extends BaseClient {
}

login(request: LoginRequest): Promise<AxiosResponse<LoginResponse>> {
const url = `${this.config.host}/security/login`;
const config = axiosRequestConfig(request);
const config = this.config.merge(request as PortfolioRequest);
const url = `${config.host}/security/login`;

return this.axiosInstance.post<LoginResponse>(url, request.body, config);
return this.axiosInstance.post<LoginResponse>(url, request.body, {
headers: config.headers,
});
}

confirmAccount(
request: UpdatePasswordRequest
): Promise<AxiosResponse<LoginResponse>> {
const url = `${this.config.host}/security/confirm-account`;
const config = axiosRequestConfig(request);
const config = this.config.merge(request as PortfolioRequest);
const url = `${config.host}/security/confirm-account`;

return this.axiosInstance.post<LoginResponse>(url, request.body, config);
return this.axiosInstance.post<LoginResponse>(url, request.body, {
headers: config.headers,
});
}
}
43 changes: 41 additions & 2 deletions src/common/client.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,55 @@
import axios, { AxiosInstance } from "axios";
import { PortfolioRequest } from "./request";

export type Headers = { [headerName: string]: any };

export interface ClientConfig {
host: string;
jwt?: string;
version?: string;
}

export class ClientConfigWrapper {
private readonly config: ClientConfig;

constructor(config: ClientConfig) {
this.config = config;
}

merge = (config: ClientConfig | PortfolioRequest): ClientConfigWrapper => {
const mergedConfig: ClientConfig = {
...this.config,
...config,
};
return new ClientConfigWrapper(mergedConfig);
};

get headers(): Headers {
return {
Authorization: `Bearer ${this.config.jwt}`,
"X-PORTFOLIO-VERSION": this.config.version,
};
}

get host(): string {
return this.config.host;
}

get jwt(): string | undefined {
return this.config.jwt;
}

get version(): string | undefined {
return this.config.version;
}
}

export abstract class BaseClient {
readonly config: ClientConfig;
readonly config: ClientConfigWrapper;
protected readonly axiosInstance: AxiosInstance;

constructor(config: ClientConfig, axiosInstance: AxiosInstance = axios) {
this.config = config;
this.config = new ClientConfigWrapper(config);
this.axiosInstance = axiosInstance;
}
}
39 changes: 16 additions & 23 deletions src/common/request.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
import { AxiosRequestConfig } from "axios";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface PortfolioRequest<T = any> {
version?: string;
body?: T;
/**
* Represents a portfolio request.
* JWT and Version properties can be used to override initial portfolio client configuration,
* however currently this functionality does not exist
*/
export interface PortfolioRequest {
jwt?: string;
version?: string;
}

export const axiosRequestConfig = <T>(
request: PortfolioRequest<T>
): AxiosRequestConfig => {
const headers: { [k: string]: string } = {};

if (request.jwt) {
headers["Authorization"] = `Bearer ${request.jwt}`;
}
export interface RequestWithParameters<T> extends PortfolioRequest {
queryParameters?: T;
}

if (request.version) {
headers["X-PORTFOLIO-VERSION"] = request.version;
}
export interface RequestWithBody<T> extends PortfolioRequest {
body?: T;
}

return {
headers: {
...headers,
},
};
};
export interface CompletePortfolioRequest<B, Q>
extends RequestWithBody<B>,
RequestWithParameters<Q> {}
33 changes: 31 additions & 2 deletions test/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { portfolio, PortfolioClient } from "../src";
import {
ClientConfig,
ClientConfigWrapper,
portfolio,
PortfolioClient,
} from "../src";

describe("portfolio client factory function", () => {
it("should return portfolio client", () => {
Expand All @@ -20,7 +25,7 @@ describe("portfolio client factory function", () => {
});

describe("portfolio client", () => {
const config = {
const config: ClientConfig = {
host: "123",
};

Expand All @@ -42,3 +47,27 @@ describe("portfolio client", () => {
expect(client.config).toBe(config);
});
});

describe("client config wrapper", () => {
const config = {
host: "host",
jwt: "123.abc.jwt",
version: "v1.0",
};

let configWrapper: ClientConfigWrapper;

beforeAll(() => {
configWrapper = new ClientConfigWrapper(config);
});

it("should correctly attach jwt as bearer token header", () => {
const headers = configWrapper.headers;
expect(headers["Authorization"]).toEqual(`Bearer ${config.jwt}`);
});

it("should correctly attach version to custom header", () => {
const headers = configWrapper.headers;
expect(headers["X-PORTFOLIO-VERSION"]).toEqual(config.version);
});
});
Loading

0 comments on commit ddad085

Please sign in to comment.