Skip to content

Commit

Permalink
feat: adding middlewares
Browse files Browse the repository at this point in the history
  • Loading branch information
PaquitoSoft committed Mar 5, 2022
1 parent 7da610a commit 84e2e1a
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 20 deletions.
11 changes: 10 additions & 1 deletion src/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
*/
import getData from "./get-data";
import InMemoryCache from "./in-memory-cache";
import * as MiddlewareManager from "./middleware-manager";
import { sendData } from "./send-data";
import { CacheManager, CacheManagerSetterOptions, HttpError } from "./shared-types";
import type { CacheManager, CacheManagerSetterOptions, HttpError } from "./shared-types";

export type {
CacheManager,
Expand Down Expand Up @@ -88,3 +89,11 @@ export function del<T>(url: string, options?: RequestOptions & { body?: object |
fetchOptions: options?.fetchOptions
});
}

export function addMiddleware(type: 'before' | 'after', middleware: MiddlewareManager.Middleware ): void {
MiddlewareManager.addMiddleware(type, middleware);
}

export function removeMiddleware(middleware: MiddlewareManager.Middleware ) {
return MiddlewareManager.removeMiddleware(middleware);
}
42 changes: 32 additions & 10 deletions src/get-data.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { runAfterMiddlewares, runBeforeMiddlewares } from "./middleware-manager";
import parseResponse from "./response-parser";
import { CacheManager } from "./shared-types";

Expand All @@ -6,7 +7,7 @@ interface HttpError extends Error {
response?: Response;
}

type GetDataRequestOptions = {
export type GetDataRequestOptions = {
fetchOptions?: RequestInit;
ttl?: number; // seconds
cache: CacheManager;
Expand All @@ -19,17 +20,33 @@ async function getData<T>(
cache
}: GetDataRequestOptions
): Promise<T> {
const result = cache.get(url) as T;

if (result) {
return Promise.resolve(result);
}

const requestOptions: RequestInit = {
...fetchOptions,
headers: fetchOptions?.headers || {}
};
const response: Response = await fetch(url, requestOptions);

// const requestParams = beforeMiddlewares.reduce<RequestParams>((params, middleware) => {
// const result = middleware(url, {
// fetchOptions: params.fetchOptions,
// ttl,
// cache
// });
// return {
// url: result.url ?? params.url,
// ttl,
// fetchOptions: result.fetchOptions ?? params.fetchOptions
// } as RequestParams;
// }, { url: url, ttl, fetchOptions: requestOptions });
const requestParams = runBeforeMiddlewares(url, { fetchOptions: requestOptions, ttl, cache });

const result = cache.get(requestParams.url) as T;

if (result) {
return Promise.resolve(result);
}

const response: Response = await fetch(requestParams.url, requestParams.fetchOptions);

if (!response.ok) {
const error: HttpError = new Error(response.statusText);
Expand All @@ -40,11 +57,16 @@ async function getData<T>(

const data = await parseResponse<T>(response);

if (ttl) {
cache.set(url, data, { ttl });
if (requestParams.ttl) {
cache.set(requestParams.url, data, { ttl: requestParams.ttl });
}

return data;
// const output: T = afterMiddlewares.reduce<T>((prevResult, middleware) => {
// return middleware<T>(data);
// }, data);
const output = runAfterMiddlewares<T>(data);

return output;
}

export default getData;
78 changes: 78 additions & 0 deletions src/middleware-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { CacheManager } from "./shared-types";

type RequestMiddlewareOptions = {
fetchOptions: RequestInit;
ttl?: number; // seconds
cache: CacheManager;
};

type BeforeMiddlewaresResult = {
url: string,
fetchOptions: RequestInit,
ttl?: number
};

export type BeforeMiddlewareResult = {
url?: string;
ttl?: number;
fetchOptions?: RequestInit;
};

export type BeforeMiddleware = (url: string, requestOptions: RequestMiddlewareOptions) =>
BeforeMiddlewareResult | undefined | null;

export type AfterMiddleware = <T>(serverData: T) => T;

export type Middleware = BeforeMiddleware | AfterMiddleware;

const middlewares = {
before: [] as BeforeMiddleware[],
after: [] as AfterMiddleware[]
};

export function addMiddleware(type: "before" | "after", middleware: Middleware): void {
if (type === 'before') {
middlewares.before.push(middleware as BeforeMiddleware);
}

if (type === 'after') {
middlewares.after.push(middleware as AfterMiddleware);
}
}

export function removeMiddleware(middleware: Middleware): Middleware | undefined {
let index = middlewares.before.indexOf(middleware as BeforeMiddleware);
if (index !== -1) {
return middlewares.before.splice(index, 1)[0];
}

index = middlewares.after.indexOf(middleware as AfterMiddleware);
if (index !== -1) {
return middlewares.after.splice(index, 1)[0];
}
}

export function runBeforeMiddlewares(
url: string,
{ fetchOptions, ttl, cache }: RequestMiddlewareOptions
): BeforeMiddlewaresResult {
return middlewares.before.reduce<BeforeMiddlewaresResult>((params, middleware) => {
const result = middleware(
params.url, {
fetchOptions: params.fetchOptions,
ttl: params.ttl,
cache
}
);

return {
url: result?.url ?? params.url,
fetchOptions: result?.fetchOptions ?? params.fetchOptions,
ttl: result?.ttl ?? params.ttl
} as BeforeMiddlewaresResult;
}, { url, fetchOptions, ttl });
}

export function runAfterMiddlewares<T>(serverData: T): T {
return middlewares.after.reduce((data, middleware) => middleware(data), serverData);
}
114 changes: 114 additions & 0 deletions test/middleware.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { get, post, addMiddleware, removeMiddleware } from '../src/fetcher';
import { AfterMiddleware, BeforeMiddleware, BeforeMiddlewareResult } from '../src/middleware-manager';

const BASE_URL = 'https://localhost';

describe("Fetcher", () => {

describe('Middlewares', ()=> {

it('Should add "before" middlewares', async () => {
const initialId = '15';
const updatedId = '23';
const mockChangeUrlMiddleware = jest.fn() as jest.MockedFunction<BeforeMiddleware>;
mockChangeUrlMiddleware.mockImplementation((url, _requestOptions) => ({
url: url.replace(initialId, updatedId)
}));
const mockAddHeaderMiddleware = jest.fn() as jest.MockedFunction<BeforeMiddleware>;
mockAddHeaderMiddleware.mockImplementation((_url, requestOptions) => ({
fetchOptions: {
...requestOptions.fetchOptions,
headers: {
...requestOptions.fetchOptions.headers,
'X-Custom-Header': 'custom-value'
}
}
}));
addMiddleware('before', mockChangeUrlMiddleware);
addMiddleware('before', mockAddHeaderMiddleware);

const user = await get(`${BASE_URL}/api/user/${initialId}`);

const [url1] = mockChangeUrlMiddleware.mock.calls[0];
const [url2] = mockAddHeaderMiddleware.mock.calls[0];
expect(url1).toContain(initialId);
expect(url2).toContain(updatedId);

expect(user).toHaveProperty('id', Number(updatedId));
expect(user).toHaveProperty('meta', 'custom-value');
});

it('Should add "after" middlewares', async () => {
const avgPoints = 50;
const avgRebounds = 10;
const mockAddMetaAvgPointsMiddleware = jest.fn() as jest.MockedFunction<AfterMiddleware>;
mockAddMetaAvgPointsMiddleware.mockImplementation((serverData: any) => ({
...serverData,
meta: { avgPoints }
}));
const mockAddMetaAvgReboundsMiddleware = jest.fn() as jest.MockedFunction<AfterMiddleware>;
mockAddMetaAvgReboundsMiddleware.mockImplementation((serverData: any) => ({
...serverData,
meta: {
...serverData.meta,
avgRebounds
}
}));

addMiddleware('after', mockAddMetaAvgPointsMiddleware);
addMiddleware('after', mockAddMetaAvgReboundsMiddleware);

const user = await get(`${BASE_URL}/api/user/15`);

expect(user).toHaveProperty('meta.avgPoints', avgPoints);
expect(user).toHaveProperty('meta.avgRebounds', avgRebounds);
});

it('Should add both "before" and "after" middlewares', async () => {
const initialId = '15';
const updatedId = '23';
const avgPoints = 50;
const mockChangeUrlMiddleware = jest.fn() as jest.MockedFunction<BeforeMiddleware>;
mockChangeUrlMiddleware.mockImplementation((url, _requestOptions) => ({
url: url.replace(initialId, updatedId)
}));
const mockAddMetaAvgPointsMiddleware = jest.fn() as jest.MockedFunction<AfterMiddleware>;
mockAddMetaAvgPointsMiddleware.mockImplementation((serverData: any) => ({
...serverData,
meta: { avgPoints }
}));

addMiddleware('before', mockChangeUrlMiddleware);
addMiddleware('after', mockAddMetaAvgPointsMiddleware);

const user = await get(`${BASE_URL}/api/user/${initialId}`);

expect(user).toHaveProperty('id', Number(updatedId));
expect(user).toHaveProperty('meta.avgPoints', avgPoints);
});

it('Should remove middlewares', async () => {
const mockBeforeMiddleware = jest.fn();
const mockAfterMiddleware = jest.fn((serverData) => serverData);

addMiddleware('before', mockBeforeMiddleware);
addMiddleware('after', mockAfterMiddleware);

await get(`${BASE_URL}/api/user/15`);
expect(mockBeforeMiddleware).toHaveBeenCalled();
expect(mockAfterMiddleware).toHaveBeenCalled();

removeMiddleware(mockBeforeMiddleware);
await get(`${BASE_URL}/api/user/15`);
expect(mockBeforeMiddleware).toHaveBeenCalledTimes(1);
expect(mockAfterMiddleware).toHaveBeenCalledTimes(2);

removeMiddleware(mockAfterMiddleware);
await get(`${BASE_URL}/api/user/15`);
expect(mockBeforeMiddleware).toHaveBeenCalledTimes(1);
expect(mockAfterMiddleware).toHaveBeenCalledTimes(2);
});

});
});

10 changes: 3 additions & 7 deletions test/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { rest } from 'msw';

type RequestBody = {
id?: string;
name: string;
email: string;
};

const BASE_URL = 'https://localhost';

export const handlers = [
Expand Down Expand Up @@ -75,12 +69,14 @@ export const handlers = [
}

if (req.params.userId === '23') {
const customHeader = req.headers.get('X-Custom-Header');
return res(
ctx.status(200),
ctx.json({
id: 23,
name: 'Michael',
email: '[email protected]'
email: '[email protected]',
meta: customHeader
})
);
}
Expand Down
4 changes: 2 additions & 2 deletions test/set-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ describe("Fetcher", () => {
const cacheManager = getMockedCacheManager();
setCacheManager(cacheManager);

const freshUser = await get(`${BASE_URL}/api/user/15`, { ttl: 10 });
await get(`${BASE_URL}/api/user/15`, { ttl: 10 });
expect(cacheManager.get).toHaveBeenCalledTimes(1);
expect(cacheManager.set).toHaveBeenCalledTimes(1);

const cachedUser = await get(`${BASE_URL}/api/user/15`);
await get(`${BASE_URL}/api/user/15`);
expect(cacheManager.get).toHaveBeenCalledTimes(2);
expect(cacheManager.set).toHaveBeenCalledTimes(1);
});
Expand Down

0 comments on commit 84e2e1a

Please sign in to comment.