Skip to content

Commit

Permalink
ApiClient made more generic and uses qs instead of query-string
Browse files Browse the repository at this point in the history
  • Loading branch information
gius committed Nov 5, 2021
1 parent e112a54 commit aac95f4
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 83 deletions.
20 changes: 10 additions & 10 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,28 @@
"**/node_modules": true,
"**/dist": true
},
"peacock.color": "#190d38",
"typescript.tsdk": "node_modules\\typescript\\lib",
"workbench.colorCustomizations": {
"titleBar.activeBackground": "#190d38",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#190d3899",
"titleBar.inactiveForeground": "#e7e7e799",
"activityBar.activeBackground": "#2b1761",
"activityBar.activeBorder": "#9b4525",
"activityBar.background": "#2b1761",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",
"activityBarBadge.background": "#9b4525",
"activityBarBadge.foreground": "#e7e7e7",
"statusBar.background": "#190d38",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#2b1761",
"titleBar.activeBackground": "#190d38",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#190d3899",
"titleBar.inactiveForeground": "#e7e7e799",
"editorGroup.border": "#2b1761",
"panel.border": "#2b1761",
"sash.hoverBorder": "#2b1761",
"sideBar.border": "#2b1761",
"statusBar.background": "#190d38",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#2b1761",
"statusBarItem.remoteBackground": "#190d38",
"statusBarItem.remoteForeground": "#e7e7e7"
},
"peacock.color": "#190d38",
"typescript.tsdk": "node_modules\\typescript\\lib"
}
}
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# 1.0.0
- Complete refactoring

# 0.16.2

- `ManualPromise` has observable `State` property
Expand Down
2 changes: 1 addition & 1 deletion packages/apiclient/__tests__/restRequestBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe("getQueryString", () => {

const query = { foo: "param1", bar: 123 };
const result = builder.getQueryString(query);
expect(result).toBe("bar=123&foo=param1");
expect(result).toBe("foo=param1&bar=123");
});
});

Expand Down
5 changes: 4 additions & 1 deletion packages/apiclient/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
},
"dependencies": {
"@frui.ts/helpers": "^999.0.0",
"query-string": "^6.13.7"
"qs": "^6.10.1"
},
"devDependencies": {
"@types/qs": "^6.9.7"
}
}
36 changes: 20 additions & 16 deletions packages/apiclient/src/fetchApiConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@ import { bind } from "@frui.ts/helpers";
import FetchError from "./fetchError";
import type { IApiConnector } from "./types";

const jsonContentType = "application/json";
export const jsonContentType = "application/json";
export type Middleware = (response: Response) => Response | PromiseLike<Response>;
export type FetchFunction = (input: RequestInfo, init?: RequestInit) => Promise<Response>;
export type JsonSerializer = (value: any) => string;
export type Serializer = (value: any) => string;

/** Creates a new RequestInit based on the provided values and with a 'Content-Type: application/json' header. */
export function appendJsonHeader(params?: RequestInit): RequestInit {
export function appendContentTypeHeader(contentType: string, params?: RequestInit): RequestInit {
return {
...params,
headers: {
...(params ?? {}).headers,
"Content-Type": jsonContentType,
"Content-Type": contentType,
},
};
}

/** Creates a new RequestInit based on the provided values and with a 'Content-Type: application/json' header. */
export function appendJsonHeader(params?: RequestInit): RequestInit {
return appendContentTypeHeader(jsonContentType, params);
}

/** Middleware used by FetchApiConnector to handle response status codes other than 2xx as errors */
export async function handleErrorStatusMiddleware(response: Response) {
if (response.status >= 200 && response.status < 300) {
Expand All @@ -42,12 +46,12 @@ export async function handleErrorStatusMiddleware(response: Response) {
*/
export class FetchApiConnector implements IApiConnector {
protected fetchFunction: FetchFunction;
protected jsonSerializer: JsonSerializer;
protected serializer: Serializer;
protected middleware: Middleware;

constructor(configuration?: { fetchFunction?: FetchFunction; jsonSerializer?: JsonSerializer; middleware?: Middleware }) {
constructor(configuration?: { fetchFunction?: FetchFunction; serializer?: Serializer; middleware?: Middleware }) {
this.fetchFunction = configuration?.fetchFunction ?? bind(window.fetch, window);
this.jsonSerializer = configuration?.jsonSerializer ?? JSON.stringify;
this.serializer = configuration?.serializer ?? JSON.stringify;
this.middleware = configuration?.middleware ?? handleErrorStatusMiddleware;
}

Expand All @@ -58,28 +62,28 @@ export class FetchApiConnector implements IApiConnector {
post(url: string, body?: BodyInit, params?: RequestInit): Promise<Response> {
return this.fetchFunction(url, { ...params, method: "POST", body }).then(this.middleware);
}
postJson(url: string, content: any, params?: RequestInit): Promise<Response> {
return this.post(url, this.jsonSerializer(content), appendJsonHeader(params));
postObject(url: string, content: any, params?: RequestInit): Promise<Response> {
return this.post(url, this.serializer(content), appendJsonHeader(params));
}

put(url: string, body?: BodyInit, params?: RequestInit): Promise<Response> {
return this.fetchFunction(url, { ...params, method: "PUT", body }).then(this.middleware);
}
putJson(url: string, content: any, params?: RequestInit): Promise<Response> {
return this.put(url, this.jsonSerializer(content), appendJsonHeader(params));
putObject(url: string, content: any, params?: RequestInit): Promise<Response> {
return this.put(url, this.serializer(content), appendJsonHeader(params));
}

patch(url: string, body?: BodyInit, params?: RequestInit): Promise<Response> {
return this.fetchFunction(url, { ...params, method: "PATCH", body }).then(this.middleware);
}
patchJson(url: string, content: any, params?: RequestInit): Promise<Response> {
return this.patch(url, this.jsonSerializer(content), appendJsonHeader(params));
patchObject(url: string, content: any, params?: RequestInit): Promise<Response> {
return this.patch(url, this.serializer(content), appendJsonHeader(params));
}

delete(url: string, body?: BodyInit, params?: RequestInit): Promise<Response> {
return this.fetchFunction(url, { ...params, method: "DELETE", body }).then(this.middleware);
}
deleteJson(url: string, content: any, params?: RequestInit): Promise<Response> {
return this.delete(url, this.jsonSerializer(content), appendJsonHeader(params));
deleteObject(url: string, content: any, params?: RequestInit): Promise<Response> {
return this.delete(url, this.serializer(content), appendJsonHeader(params));
}
}
50 changes: 29 additions & 21 deletions packages/apiclient/src/restRequestBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { StringifiableRecord, StringifyOptions } from "query-string";
import { stringify } from "query-string";
import type { IStringifyOptions } from "qs";
import { stringify } from "qs";
import type { IApiConnector } from "./types";

const cleanupRegex = /\/+$/g; // removes trailing slash

export type Deserializer<T> = (response: Response) => Promise<T>;

export const ContentTypes = {
json: "application/json,text/json",
};
Expand All @@ -23,12 +25,18 @@ export function appendUrl(base: string, ...segments: any[]) {
return segments.length ? `${basePath}/${segments.join("/")}` : basePath;
}

/** Fluent URL builder that makes the network call with the underlying IApiConnector */
/**
* Fluent URL builder that makes the network call with the underlying IApiConnector
* Check https://github.com/ljharb/qs for query string customizations
*/
export class RestRequestBuilder {
static DefaultQueryStringOptions: StringifyOptions = { skipNull: true };
static DefaultQueryStringOptions: IStringifyOptions = { skipNulls: true };

protected urlValue: string;
queryStringOptions?: StringifyOptions;
queryStringOptions?: IStringifyOptions;

objectContentType: string = ContentTypes.json;
objectDeserializer: Deserializer<unknown> = x => x.json();

get url() {
return this.urlValue;
Expand Down Expand Up @@ -61,50 +69,50 @@ export class RestRequestBuilder {
return this;
}

get<T>(queryParams?: StringifiableRecord): Promise<T> {
get<T>(queryParams?: any): Promise<T> {
const requestUrl = this.appendQuery(this.urlValue, queryParams);
const params = appendAcceptHeader(this.params, ContentTypes.json);
return this.apiConnector.get(requestUrl, params).then(x => x.json() as Promise<T>);
const params = appendAcceptHeader(this.params, this.objectContentType);
return this.apiConnector.get(requestUrl, params).then(this.objectDeserializer as Deserializer<T>);
}

getRaw(queryParams?: StringifiableRecord) {
getRaw(queryParams?: any) {
const requestUrl = this.appendQuery(this.urlValue, queryParams);
return this.apiConnector.get(requestUrl, this.params);
}

post<T>(content: any): Promise<T> {
const params = appendAcceptHeader(this.params, ContentTypes.json);
return this.apiConnector.postJson(this.urlValue, content, params).then(x => x.json() as Promise<T>);
const params = appendAcceptHeader(this.params, this.objectContentType);
return this.apiConnector.postObject(this.urlValue, content, params).then(this.objectDeserializer as Deserializer<T>);
}

postOnly(content: any) {
return this.apiConnector.postJson(this.urlValue, content, this.params);
return this.apiConnector.postObject(this.urlValue, content, this.params);
}

postData(data?: BodyInit) {
return this.apiConnector.post(this.urlValue, data, this.params);
}

put<T>(content: any): Promise<T> {
const params = appendAcceptHeader(this.params, ContentTypes.json);
return this.apiConnector.putJson(this.urlValue, content, params).then(x => x.json() as Promise<T>);
const params = appendAcceptHeader(this.params, this.objectContentType);
return this.apiConnector.putObject(this.urlValue, content, params).then(this.objectDeserializer as Deserializer<T>);
}

putOnly(content: any) {
return this.apiConnector.putJson(this.urlValue, content, this.params);
return this.apiConnector.putObject(this.urlValue, content, this.params);
}

putData(data?: BodyInit) {
return this.apiConnector.put(this.urlValue, data, this.params);
}

patch<T>(content: any): Promise<T> {
const params = appendAcceptHeader(this.params, ContentTypes.json);
return this.apiConnector.patchJson(this.urlValue, content, params).then(x => x.json() as Promise<T>);
const params = appendAcceptHeader(this.params, this.objectContentType);
return this.apiConnector.patchObject(this.urlValue, content, params).then(this.objectDeserializer as Deserializer<T>);
}

patchOnly(content: any) {
return this.apiConnector.patchJson(this.urlValue, content, this.params);
return this.apiConnector.patchObject(this.urlValue, content, this.params);
}

patchData(data?: BodyInit) {
Expand All @@ -113,7 +121,7 @@ export class RestRequestBuilder {

delete(content?: any) {
return content
? this.apiConnector.deleteJson(this.urlValue, content, this.params)
? this.apiConnector.deleteObject(this.urlValue, content, this.params)
: this.apiConnector.delete(this.urlValue, undefined, this.params);
}

Expand All @@ -122,11 +130,11 @@ export class RestRequestBuilder {
return this;
}

getQueryString(query: StringifiableRecord, queryStringOptions?: StringifyOptions) {
getQueryString(query: any, queryStringOptions?: IStringifyOptions) {
return stringify(query, queryStringOptions ?? this.queryStringOptions ?? RestRequestBuilder.DefaultQueryStringOptions);
}

appendQuery(url: string, query?: StringifiableRecord, queryStringOptions?: StringifyOptions) {
appendQuery(url: string, query?: any, queryStringOptions?: IStringifyOptions) {
if (!query) {
return url;
}
Expand Down
8 changes: 4 additions & 4 deletions packages/apiclient/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ export interface IApiConnector {
get(url: string, params?: RequestInit): Promise<Response>;

post(url: string, body?: BodyInit, params?: RequestInit): Promise<Response>;
postJson(url: string, content: any, params?: RequestInit): Promise<Response>;
postObject(url: string, content: any, params?: RequestInit): Promise<Response>;

put(url: string, body?: BodyInit, params?: RequestInit): Promise<Response>;
putJson(url: string, content: any, params?: RequestInit): Promise<Response>;
putObject(url: string, content: any, params?: RequestInit): Promise<Response>;

patch(url: string, body?: BodyInit, params?: RequestInit): Promise<Response>;
patchJson(url: string, content: any, params?: RequestInit): Promise<Response>;
patchObject(url: string, content: any, params?: RequestInit): Promise<Response>;

delete(url: string, body?: BodyInit, params?: RequestInit): Promise<Response>;
deleteJson(url: string, content: any, params?: RequestInit): Promise<Response>;
deleteObject(url: string, content: any, params?: RequestInit): Promise<Response>;
}
80 changes: 58 additions & 22 deletions packages/apiclient/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,62 @@
# yarn lockfile v1


decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=

query-string@^6.13.7:
version "6.13.7"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.7.tgz#af53802ff6ed56f3345f92d40a056f93681026ee"
integrity sha512-CsGs8ZYb39zu0WLkeOhe0NMePqgYdAuCqxOYKDR5LVCytDZYMGx3Bb+xypvQvPHVPijRXB0HZNFllCzHRe4gEA==
"@types/qs@^6.9.7":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==

call-bind@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
dependencies:
function-bind "^1.1.1"
get-intrinsic "^1.0.2"

function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==

get-intrinsic@^1.0.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
dependencies:
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"

has-symbols@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==

has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
dependencies:
function-bind "^1.1.1"

object-inspect@^1.9.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==

qs@^6.10.1:
version "6.10.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==
dependencies:
side-channel "^1.0.4"

side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
dependencies:
decode-uri-component "^0.2.0"
split-on-first "^1.0.0"
strict-uri-encode "^2.0.0"

split-on-first@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==

strict-uri-encode@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
call-bind "^1.0.0"
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
Loading

0 comments on commit aac95f4

Please sign in to comment.