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

Commit ddad085

Browse files
committed
Added GetAllContactMessages feature. Added GetSingleContactMessage feature. Restructured request interface.
1 parent 0b1f16b commit ddad085

File tree

12 files changed

+357
-136
lines changed

12 files changed

+357
-136
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# 0.3.0
2+
3+
- Contact client is now able to retrieve multiple contact messages
4+
- Contact client is now able to retrieve a specific contact message
5+
- Portfolio client can now be configured with an authentication token
6+
- Restructured request interface to be more fine grained
7+
- Client configs are now wrapped to provide additional functionality off of configurations
8+
19
# 0.2.0
210

311
- Added security client

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@jsextonn/portfolio-api-client",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "NodeJS client for portfolio API",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",

src/apis/contact/client.ts

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,51 @@
11
import axios, { AxiosInstance, AxiosResponse } from "axios";
22
import { BaseClient, ClientConfig } from "../../common/client";
3-
import { axiosRequestConfig, PortfolioRequest } from "../../common/request";
3+
import {
4+
PortfolioRequest,
5+
RequestWithBody,
6+
RequestWithParameters,
7+
} from "../../common/request";
48
import { PortfolioResponse } from "../../common/response";
5-
import { ContactMessage, Reason, Sender } from "./models";
6-
7-
export type CreateContactMessageRequest = PortfolioRequest<
8-
CreateContactMessageForm
9-
>;
9+
import {
10+
ContactMessage,
11+
ContactMessageCollection,
12+
Reason,
13+
Sender,
14+
} from "./models";
1015

1116
export interface CreateContactMessageForm {
1217
message: string;
1318
reason: Reason;
1419
sender: Sender;
1520
}
1621

22+
export type CreateContactMessageRequest = RequestWithBody<
23+
CreateContactMessageForm
24+
>;
25+
26+
export interface GetContactMessageRequest extends PortfolioRequest {
27+
id: string;
28+
}
29+
30+
export interface GetContactMessageQueryParameters {
31+
reason?: Reason;
32+
archived?: boolean;
33+
responded?: boolean;
34+
}
35+
36+
export type GetContactMessagesRequest = RequestWithParameters<
37+
GetContactMessageQueryParameters
38+
>;
39+
1740
export type ContactMessageResponse = PortfolioResponse<ContactMessage>;
1841

42+
export type ContactMessagesResponse = PortfolioResponse<
43+
ContactMessageCollection
44+
>;
45+
46+
/**
47+
* Client used to interface with contact API
48+
*/
1949
export class ContactClient extends BaseClient {
2050
constructor(config: ClientConfig, axiosInstance: AxiosInstance = axios) {
2151
super(config, axiosInstance);
@@ -29,13 +59,44 @@ export class ContactClient extends BaseClient {
2959
createMessage(
3060
request: CreateContactMessageRequest
3161
): Promise<AxiosResponse<ContactMessageResponse>> {
32-
const url = `${this.config.host}/contact/mail`;
33-
const config = axiosRequestConfig(request);
34-
35-
return this.axiosInstance.post<ContactMessageResponse>(
36-
url,
37-
request.body,
38-
config
39-
);
62+
const config = this.config.merge(request as PortfolioRequest);
63+
const url = `${config.host}/contact/mail`;
64+
65+
return this.axiosInstance.post<ContactMessageResponse>(url, request.body, {
66+
headers: config.headers,
67+
});
68+
}
69+
70+
/**
71+
* Retrieves a specific contact message by id
72+
*
73+
* @param request
74+
*/
75+
findMessage(
76+
request: GetContactMessageRequest
77+
): Promise<AxiosResponse<ContactMessageResponse>> {
78+
const config = this.config.merge(request as PortfolioRequest);
79+
const url = `${config.host}/contact/mail/${request.id}`;
80+
81+
return this.axiosInstance.get<ContactMessageResponse>(url, {
82+
headers: config.headers,
83+
});
84+
}
85+
86+
/**
87+
* Retrieves multiple contact messages
88+
*
89+
* @param request
90+
*/
91+
findMessages(
92+
request?: GetContactMessagesRequest
93+
): Promise<AxiosResponse<ContactMessagesResponse>> {
94+
const config = this.config.merge(request as PortfolioRequest);
95+
const url = `${config.host}/contact/mail`;
96+
97+
return this.axiosInstance.get<ContactMessagesResponse>(url, {
98+
params: request !== undefined ? request.queryParameters : {},
99+
headers: config.headers,
100+
});
40101
}
41102
}

src/apis/contact/models.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ export interface ContactMessage extends Entity {
1313
lastUpdated: Date;
1414
}
1515

16+
export interface ContactMessageCollection {
17+
count: number;
18+
contactMessages: ContactMessage[];
19+
}
20+
1621
export enum Reason {
1722
// TODO: In future API releases, case will not matter
1823
Business = "business",

src/apis/index.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ import { ClientConfig } from "../common/client";
22
import { ContactClient } from "./contact/client";
33
import { SecurityClient } from "./security/client";
44

5-
const defaultConfig: ClientConfig = {
6-
host: "https://api.justinsexton.net",
7-
};
8-
95
export class PortfolioClient {
106
readonly config: ClientConfig;
117
readonly contact: ContactClient;
@@ -18,13 +14,25 @@ export class PortfolioClient {
1814
}
1915
}
2016

17+
const defaultConfig = { host: "https://api.justinsexton.net" };
18+
19+
const mergeConfigs = (
20+
configOne: ClientConfig,
21+
configTwo: ClientConfig
22+
): ClientConfig => {
23+
return {
24+
...configTwo,
25+
...configOne,
26+
};
27+
};
28+
2129
/**
2230
* Factory method used for configuring and building a new contact client.
2331
*/
24-
export const portfolio = (
25-
config: ClientConfig = defaultConfig
26-
): PortfolioClient => {
27-
return new PortfolioClient(config);
32+
export const portfolio = (config?: ClientConfig): PortfolioClient => {
33+
const resolvedConfig =
34+
config !== undefined ? mergeConfigs(config, defaultConfig) : defaultConfig;
35+
return new PortfolioClient(resolvedConfig);
2836
};
2937

3038
export * from "./contact/client";

src/apis/security/client.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import axios, { AxiosInstance, AxiosResponse } from "axios";
22
import { BaseClient, ClientConfig, PortfolioResponse } from "../../common";
3-
import { axiosRequestConfig, PortfolioRequest } from "../../common/request";
3+
import { PortfolioRequest, RequestWithBody } from "../../common/request";
44
import { TokenBody } from "./models";
55

6-
export type LoginRequest = PortfolioRequest<LoginForm>;
6+
export type LoginRequest = RequestWithBody<LoginForm>;
77

88
export interface LoginForm {
99
username: string;
@@ -12,7 +12,7 @@ export interface LoginForm {
1212

1313
export type LoginResponse = PortfolioResponse<TokenBody>;
1414

15-
export type UpdatePasswordRequest = PortfolioRequest<UpdatePasswordForm>;
15+
export type UpdatePasswordRequest = RequestWithBody<UpdatePasswordForm>;
1616

1717
export interface UpdatePasswordForm {
1818
username: string;
@@ -26,18 +26,22 @@ export class SecurityClient extends BaseClient {
2626
}
2727

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

32-
return this.axiosInstance.post<LoginResponse>(url, request.body, config);
32+
return this.axiosInstance.post<LoginResponse>(url, request.body, {
33+
headers: config.headers,
34+
});
3335
}
3436

3537
confirmAccount(
3638
request: UpdatePasswordRequest
3739
): Promise<AxiosResponse<LoginResponse>> {
38-
const url = `${this.config.host}/security/confirm-account`;
39-
const config = axiosRequestConfig(request);
40+
const config = this.config.merge(request as PortfolioRequest);
41+
const url = `${config.host}/security/confirm-account`;
4042

41-
return this.axiosInstance.post<LoginResponse>(url, request.body, config);
43+
return this.axiosInstance.post<LoginResponse>(url, request.body, {
44+
headers: config.headers,
45+
});
4246
}
4347
}

src/common/client.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,55 @@
11
import axios, { AxiosInstance } from "axios";
2+
import { PortfolioRequest } from "./request";
3+
4+
export type Headers = { [headerName: string]: any };
25

36
export interface ClientConfig {
47
host: string;
58
jwt?: string;
9+
version?: string;
10+
}
11+
12+
export class ClientConfigWrapper {
13+
private readonly config: ClientConfig;
14+
15+
constructor(config: ClientConfig) {
16+
this.config = config;
17+
}
18+
19+
merge = (config: ClientConfig | PortfolioRequest): ClientConfigWrapper => {
20+
const mergedConfig: ClientConfig = {
21+
...this.config,
22+
...config,
23+
};
24+
return new ClientConfigWrapper(mergedConfig);
25+
};
26+
27+
get headers(): Headers {
28+
return {
29+
Authorization: `Bearer ${this.config.jwt}`,
30+
"X-PORTFOLIO-VERSION": this.config.version,
31+
};
32+
}
33+
34+
get host(): string {
35+
return this.config.host;
36+
}
37+
38+
get jwt(): string | undefined {
39+
return this.config.jwt;
40+
}
41+
42+
get version(): string | undefined {
43+
return this.config.version;
44+
}
645
}
746

847
export abstract class BaseClient {
9-
readonly config: ClientConfig;
48+
readonly config: ClientConfigWrapper;
1049
protected readonly axiosInstance: AxiosInstance;
1150

1251
constructor(config: ClientConfig, axiosInstance: AxiosInstance = axios) {
13-
this.config = config;
52+
this.config = new ClientConfigWrapper(config);
1453
this.axiosInstance = axiosInstance;
1554
}
1655
}

src/common/request.ts

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,21 @@
1-
import { AxiosRequestConfig } from "axios";
2-
3-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4-
export interface PortfolioRequest<T = any> {
5-
version?: string;
6-
body?: T;
1+
/**
2+
* Represents a portfolio request.
3+
* JWT and Version properties can be used to override initial portfolio client configuration,
4+
* however currently this functionality does not exist
5+
*/
6+
export interface PortfolioRequest {
77
jwt?: string;
8+
version?: string;
89
}
910

10-
export const axiosRequestConfig = <T>(
11-
request: PortfolioRequest<T>
12-
): AxiosRequestConfig => {
13-
const headers: { [k: string]: string } = {};
14-
15-
if (request.jwt) {
16-
headers["Authorization"] = `Bearer ${request.jwt}`;
17-
}
11+
export interface RequestWithParameters<T> extends PortfolioRequest {
12+
queryParameters?: T;
13+
}
1814

19-
if (request.version) {
20-
headers["X-PORTFOLIO-VERSION"] = request.version;
21-
}
15+
export interface RequestWithBody<T> extends PortfolioRequest {
16+
body?: T;
17+
}
2218

23-
return {
24-
headers: {
25-
...headers,
26-
},
27-
};
28-
};
19+
export interface CompletePortfolioRequest<B, Q>
20+
extends RequestWithBody<B>,
21+
RequestWithParameters<Q> {}

test/client.test.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { portfolio, PortfolioClient } from "../src";
1+
import {
2+
ClientConfig,
3+
ClientConfigWrapper,
4+
portfolio,
5+
PortfolioClient,
6+
} from "../src";
27

38
describe("portfolio client factory function", () => {
49
it("should return portfolio client", () => {
@@ -20,7 +25,7 @@ describe("portfolio client factory function", () => {
2025
});
2126

2227
describe("portfolio client", () => {
23-
const config = {
28+
const config: ClientConfig = {
2429
host: "123",
2530
};
2631

@@ -42,3 +47,27 @@ describe("portfolio client", () => {
4247
expect(client.config).toBe(config);
4348
});
4449
});
50+
51+
describe("client config wrapper", () => {
52+
const config = {
53+
host: "host",
54+
jwt: "123.abc.jwt",
55+
version: "v1.0",
56+
};
57+
58+
let configWrapper: ClientConfigWrapper;
59+
60+
beforeAll(() => {
61+
configWrapper = new ClientConfigWrapper(config);
62+
});
63+
64+
it("should correctly attach jwt as bearer token header", () => {
65+
const headers = configWrapper.headers;
66+
expect(headers["Authorization"]).toEqual(`Bearer ${config.jwt}`);
67+
});
68+
69+
it("should correctly attach version to custom header", () => {
70+
const headers = configWrapper.headers;
71+
expect(headers["X-PORTFOLIO-VERSION"]).toEqual(config.version);
72+
});
73+
});

0 commit comments

Comments
 (0)