Skip to content

Commit

Permalink
Merge pull request #1 from gpor0/main
Browse files Browse the repository at this point in the history
Main
  • Loading branch information
gpor0 authored Aug 12, 2024
2 parents 8a561b0 + 0519b7b commit 0baf959
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 71 deletions.
1 change: 0 additions & 1 deletion jest.config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"preset": "ts-jest",
"testEnvironment": "node",
"testTimeout": 1000,
"setupFiles": []
}
95 changes: 93 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gpor0/frontend-rest-client",
"version": "0.0.1-rc.5",
"version": "0.0.1-rc.6",
"description": "Lightweight Frontend Rest Api Client\n",
"homepage": "https://github.com/gpor0/frontend-rest-client",
"license": "MIT",
Expand Down Expand Up @@ -38,6 +38,7 @@
"jest": "^29.7.0",
"prettier": "^3.3.3",
"ts-jest": "^29.2.4",
"typescript": "^5.5.4"
"typescript": "^5.5.4",
"node-fetch": "^3.3.2"
}
}
126 changes: 91 additions & 35 deletions src/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,77 +1,133 @@
import { RequestCredentials, RequestMode } from "undici-types/fetch";
import { ClientRequest, ClientResponse, FetchFrontendClientOptions, FrontendClient } from "../interfaces";
import { prepareQuery } from "../utils/queryUtils";
import { isDefined, isFunction } from "../utils/validators";
import { isDefined, isFunction, isObject } from "../utils/validators";

/**
* Initializes FetchFrontendClient instance
*
* @param config - configuration
* @public
*/
export function initFetchFrontendClient(config: { baseUrl: string; options?: FetchFrontendClientOptions }): FrontendClient {
return new FetchFrontendClient(config);
}

/**
* Content-Type header string
*
* @public
*/
export const contentTypeHeader = "Content-Type";

/**
* Accept header string
*
* @public
*/
export const acceptHeader = "Accept";

/**
* Main implementation of the Frontend client
*
* @public
*/
export class FetchFrontendClient implements FrontendClient {
class FetchFrontendClient implements FrontendClient {
constructor(
private baseUrl: string,
private options?: FetchFrontendClientOptions,
private config: {
baseUrl: string;
options?: FetchFrontendClientOptions;
},
) {}

public async request<RequestBody, Response>(request: ClientRequest<RequestBody>): Promise<ClientResponse<Response>> {
return this.call(request);
public async call<RequestBody, Response>(request: ClientRequest<RequestBody>): Promise<Response> {
const response = await this.request<RequestBody, Response>(request);
return response.payload;
}

public async call<RequestBody, Response>(request?: ClientRequest<RequestBody>): Promise<ClientResponse<Response>> {
public async request<RequestBody, Response>(request: ClientRequest<RequestBody>): Promise<ClientResponse<Response>> {
const { data, query, headers, method, url } = request ?? {};

const { _headers, _credentials, _mode, _bodyParser, _preRequestFn, _responder } = this.options ?? {};
const { _headers: optHeaders, _credentials, _mode, _requestInterceptor, _responseInterceptor } = this.config.options ?? {};

const options: RequestInit = {
let fetchRequest = {
method,
headers: { ...(_headers ?? {}), ...(headers ?? {}) },
} as RequestInit;
headers: { ...(optHeaders ?? {}), ...(headers ?? {}) },
} as RequestInit & { headers: Record<string, string | string[]> };

if (_credentials) {
if (isRequestCredential(_credentials)) {
options.credentials = _credentials;
fetchRequest.credentials = _credentials;
} else {
console.warn(`"${_credentials as string}" is not a proper credential value. It should be one of: ${JSON.stringify(reqCredentials)}`);
console.warn(`"${String(_credentials)}" is not a proper credential value. It should be one of: ${JSON.stringify(reqCredentials)}`);
}
}

if (_mode) {
if (isRequestMode(_mode)) {
options.mode = _mode;
fetchRequest.mode = _mode;
} else {
console.warn(`"${_mode as string}" is not a proper mode value. It should be one of: ${JSON.stringify(modes)}`);
console.warn(`"${String(_mode)}" is not a proper mode value. It should be one of: ${JSON.stringify(modes)}`);
}
}

if (data !== undefined) {
const body = (typeof data === "function" ? data() : data) as unknown as BodyInit;
if (_bodyParser) {
if (isFunction(_bodyParser)) {
options.body = _bodyParser(body);
} else {
console.warn(`"bodyParser ${_bodyParser as string}" is not a function`);
}
}
const requestPayload = (typeof data === "function" && isDefined(data) ? data() : data) as unknown as BodyInit;

if (!fetchRequest.headers[contentTypeHeader] && isObject(requestPayload)) {
fetchRequest.headers[contentTypeHeader] = "application/json";
}

if (fetchRequest.headers[contentTypeHeader] === "application/json" && isObject(requestPayload)) {
fetchRequest.body = JSON.stringify(requestPayload);
} else {
fetchRequest.body = requestPayload;
}

if (!fetchRequest.headers[acceptHeader]) {
fetchRequest.headers[acceptHeader] = "application/json";
}

if (_preRequestFn) {
if (isFunction(_preRequestFn)) {
await _preRequestFn();
const targetUrl = `${this.config.baseUrl}${url}${prepareQuery(query)}`;
if (_requestInterceptor) {
if (isFunction(_requestInterceptor)) {
const reqInterceptorResult = await _requestInterceptor({ request: fetchRequest });
const proceed = reqInterceptorResult.proceed;

if (proceed === false) {
console.error(`Request ${targetUrl} was rejected due to the interceptor`);
throw new Error(`Request was rejected`);
}
if (reqInterceptorResult.request !== undefined) {
fetchRequest = reqInterceptorResult.request;
}
} else {
console.warn(`"preRequest ${_preRequestFn as string}" is not a function`);
console.warn(`"requestInterceptor ${String(_requestInterceptor)}" is not a function`);
}
}

const targetUrl = `${this.baseUrl}${url}${prepareQuery(query)}`;
const fetchResponse = await fetch(targetUrl, options);
const fetchResponse = await fetch(targetUrl, fetchRequest);

const respondedResponse = isDefined(_responder) && isFunction(_responder) ? await _responder(fetchResponse) : fetchResponse;
const httpStatus = fetchResponse.status;
const responseHeaders = fetchResponse.headers as unknown as { [key: string]: string | string[] };

let interceptedResponse: Response;
if (isDefined(_responseInterceptor)) {
if (isFunction(_responseInterceptor)) {
interceptedResponse = (await _responseInterceptor(fetchResponse)) as unknown as Response;
} else {
interceptedResponse = (await fetchResponse.blob()) as unknown as Response;
console.warn(`"responseInterceptor ${String(_responseInterceptor)}" is not a function`);
}
} else if (String(responseHeaders[contentTypeHeader])?.toLowerCase() === "application/json") {
interceptedResponse = (await fetchResponse.json()) as unknown as Response;
} else {
interceptedResponse = (await fetchResponse.blob()) as unknown as Response;
}

const response: ClientResponse<Response> = {
httpStatus: respondedResponse.status,
payload: respondedResponse.body as unknown as Response,
responseHeaders: respondedResponse.headers as unknown as { [key: string]: string | string[] },
const response = {
httpStatus,
payload: interceptedResponse,
responseHeaders,
};

return response;
Expand Down
14 changes: 12 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
export { FetchFrontendClient } from "./client/client";
export { FrontendClient, FetchClientData, HttpMethod, FetchFrontendClientOptions, ClientRequest, ClientResponse, QueryParamValueType } from "./interfaces";
export { initFetchFrontendClient } from "./client/client";
export {
FrontendClient,
FetchClientData,
HttpMethod,
FetchFrontendClientOptions,
ClientRequest,
ClientResponse,
QueryParamValueType,
RequestInterceptorResult,
AuthProvider,
} from "./interfaces";
Loading

0 comments on commit 0baf959

Please sign in to comment.