Skip to content

Commit

Permalink
fix(*): added types for node-fetch and adapted config
Browse files Browse the repository at this point in the history
  • Loading branch information
Filipe Torrado committed Mar 28, 2022
1 parent 0a8c5cd commit 6afe755
Show file tree
Hide file tree
Showing 9 changed files with 2,263 additions and 1,670 deletions.
3,708 changes: 2,105 additions & 1,603 deletions package-lock.json

Large diffs are not rendered by default.

17 changes: 12 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
"name": "@sagacify/api-reader",
"version": "2.2.0",
"description": "REST API request handler",
"main": "build/NodeApiReader.js",
"browser": "build/BrowserApiReader.js",
"exports": "./build/NodeApiReader.js",
"browser": "./build/BrowserApiReader.js",
"directories": {
"lib": "build/src",
"test": "build/test"
},
"type": "module",
"engines": {
"node": "^12.20.0 || >=14.13.1"
},
"bugs": {
"url": "https://github.com/Sagacify/autoroute/issues"
},
Expand All @@ -34,17 +38,18 @@
"lint": "eslint --fix ./src ./test",
"test:lint": "eslint -f stylish ./src ./test",
"test:types": "tsc --noEmit --skipLibCheck",
"test:spec": "env NODE_ENV=test mocha .",
"test:spec": "env NODE_ENV=test ts-mocha .",
"test:cover": "nyc npm run test:spec",
"test:watch": "env NODE_ENV=test mocha --watch",
"test": "npm run test:lint && npm run test:types && npm run test:cover"
},
"dependencies": {
"node-fetch": "^3.1.0",
"qs": "^6.10.2"
"node-fetch": "^3.2.3",
"qs": "^6.10.3"
},
"devDependencies": {
"@sagacify/eslint-config": "^1.2.0",
"@tsconfig/node12": "^1.0.9",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"@types/node": "^17.0.0",
Expand All @@ -64,6 +69,7 @@
"nyc": "^15.1.0",
"prettier": "^2.5.1",
"sinon": "^12.0.1",
"ts-mocha": "^9.0.2",
"ts-node": "^10.4.0",
"typescript": "^4.5.4"
},
Expand All @@ -86,6 +92,7 @@
}
},
"mocha": {
"loader": "ts-node/esm",
"require": "ts-node/register",
"spec": [
"test/**/*.ts"
Expand Down
11 changes: 5 additions & 6 deletions src/BrowserApiReader.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
/* global fetch, Headers, btoa */
import { IsoApiReader, ApiReaderOptions } from './IsoApiReader';

module.exports.ApiReader = class ApiReader extends IsoApiReader {
export class ApiReader extends IsoApiReader {
constructor(baseUrl: string, options: ApiReaderOptions) {
super(
{
fetch,
Headers,
btoa
fetch: window.fetch,
Headers: window.Headers,
btoa: window.btoa
},
baseUrl,
options
);
}
};
}
45 changes: 30 additions & 15 deletions src/IsoApiReader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import qs from 'qs';
import {
import * as qs from 'qs';
import type {
Headers as nHeaders,
BodyInit as nBodyInit,
RequestInit as nRequestInit
Expand All @@ -10,7 +10,16 @@ type IsoHeaders = Headers | nHeaders;
type IsoBody = BodyInit | nBodyInit | null;
type IsoRequestInit = RequestInit | nRequestInit;

type IsoFetch = (url: RequestInfo, init?: IsoRequestInit) => Promise<Response>;
// Special types for handlers, JSON is still not stringified
export interface IsoPreRequestInit
extends Omit<nRequestInit | IsoRequestInit, 'body'> {
body?: BodyInit | Record<string, unknown> | null;
}

export type IsoFetch = (
url: RequestInfo,
init?: IsoRequestInit
) => Promise<Response>;

type SimpleRequest = {
url: string;
Expand All @@ -25,7 +34,7 @@ type SimpleResponse = {
ok: boolean;
redirected: boolean;
type: string;
headers: object;
headers: Record<string, unknown>;
body: unknown;
};

Expand All @@ -34,7 +43,7 @@ type Auth = {
password?: string;
};

type PreRequestHandler = (options: IsoRequestInit) => IsoRequestInit;
type PreRequestHandler = (options: IsoPreRequestInit) => IsoPreRequestInit;
type HttpErrorHandler = (req: SimpleRequest, res: SimpleResponse) => void;

export type ApiReaderOptions = {
Expand All @@ -58,7 +67,7 @@ type ReqOptions = {
method?: string;
headers?: Record<string, string>;
query?: Record<string, unknown>;
body?: (BodyInit & nBodyInit) | null;
body?: (BodyInit & nBodyInit) | Record<string, unknown> | null;
auth?: Auth;
json?: boolean;
};
Expand Down Expand Up @@ -140,7 +149,7 @@ export class IsoApiReader {
url.pathname = `${url.pathname}/${path}`.replace(/\/+/, '/');
const reqHeaders = new this.Headers(headers);

let fetchOptions: IsoRequestInit = {
let fetchOptions: IsoPreRequestInit = {
method: method.toUpperCase(),
headers: this.mergeHeaders(this.baseHeaders, reqHeaders)
};
Expand All @@ -161,7 +170,10 @@ export class IsoApiReader {
}

if (this.preRequestHandler) {
fetchOptions = this.preRequestHandler({ ...fetchOptions, body });
fetchOptions = this.preRequestHandler({
...(fetchOptions as IsoRequestInit),
body
});
}

if (body !== undefined) {
Expand All @@ -178,7 +190,10 @@ export class IsoApiReader {
}
}

const fetchRes = await fetch(url.toString(), fetchOptions);
const fetchRes = await fetch(
url.toString(),
fetchOptions as IsoRequestInit
);
const contentType = (fetchRes.headers.get('Content-Type') || '')
.split(';', 1)[0]
.toLocaleLowerCase()
Expand Down Expand Up @@ -227,27 +242,27 @@ export class IsoApiReader {
return resBody;
}

async head(path: string, options: DefniedReqOptions) {
async head(path: string, options: DefniedReqOptions = {}) {
return this.req(path, { ...options, method: 'HEAD' });
}

async get(path: string, options: DefniedReqOptions) {
async get(path: string, options: DefniedReqOptions = {}) {
return this.req(path, { ...options, method: 'GET' });
}

async post(path: string, options: DefniedReqOptions) {
async post(path: string, options: DefniedReqOptions = {}) {
return this.req(path, { ...options, method: 'POST' });
}

async put(path: string, options: DefniedReqOptions) {
async put(path: string, options: DefniedReqOptions = {}) {
return this.req(path, { ...options, method: 'PUT' });
}

async patch(path: string, options: DefniedReqOptions) {
async patch(path: string, options: DefniedReqOptions = {}) {
return this.req(path, { ...options, method: 'PATCH' });
}

async delete(path: string, options: DefniedReqOptions) {
async delete(path: string, options: DefniedReqOptions = {}) {
return this.req(path, { ...options, method: 'DELETE' });
}
}
11 changes: 7 additions & 4 deletions src/NodeApiReader.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { IsoApiReader, ApiReaderOptions } from './IsoApiReader';
import { IsoApiReader, ApiReaderOptions, IsoFetch } from './IsoApiReader.ts';
// Add missing NodeJS classes/functions
import nFetch, { Headers as nHeaders } from 'node-fetch';

import { Headers as nHeaders, RequestInfo, RequestInit, Response } from 'node-fetch';
const nFetch = (url: RequestInfo, init?: RequestInit): Promise<Response> => import('node-fetch').then(({default: fetch}) => fetch(url, init));


const nBtoa = (text: string) => Buffer.from(text).toString('base64');

export class ApiReader extends IsoApiReader {
constructor(baseUrl: string, options: ApiReaderOptions) {
constructor(baseUrl: string, options: ApiReaderOptions = {}) {
super(
{
fetch: nFetch,
fetch: (nFetch as unknown) as IsoFetch,
Headers: nHeaders,
btoa: nBtoa
},
Expand Down
4 changes: 4 additions & 0 deletions src/node-fetch-wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { RequestInfo, RequestInit } from 'node-fetch';
const fetch = (url: RequestInfo, init?: RequestInit) => import('node-fetch').then(({default: fetch}) => fetch(url, init));

export default fetch;
86 changes: 59 additions & 27 deletions test/src/NodeApiReader.ts → test/NodeApiReader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { expect } from 'chai';
import nock from 'nock';
import { ApiReader } from '../../src/NodeApiReader';
import { FetchError, AbortError } from 'node-fetch';
import { IsoPreRequestInit } from '../src/IsoApiReader.ts';
import { ApiReader } from '../src/NodeApiReader.ts';

type HttpError = Error | {
code: string
};
const errorCode = 'code' as keyof HttpError;

describe('ApiReader', () => {
describe('constructor', () => {
Expand All @@ -20,15 +27,19 @@ describe('ApiReader', () => {
nock('http://fake-api').get('/profile').reply(500);

const apiReader = new ApiReader('http://fake-api');
let error;

try {
await apiReader.get('/profile');
} catch (err) {
error = err;
} catch (err: unknown) {
if (err instanceof Error) {
expect(err.message.startsWith('Http Error 500')).to.equal(true);
return;
} else {
throw err;
}
}

expect(error.code).to.equal('HTTP_500');
expect.fail('Should throw 500 error');
});

it('should call preRequestHandler when defined', async () => {
Expand All @@ -41,9 +52,15 @@ describe('ApiReader', () => {
let newFirstName;

const apiReader = new ApiReader('http://fake-api', {
preRequestHandler: (fetchOptions) => {
preRequestHandler: (fetchOptions: IsoPreRequestInit) => {
newFirstName = 'Nicolas';
fetchOptions.body.firstname = newFirstName;
if (
typeof fetchOptions.body === 'object' &&
fetchOptions.body !== null
) {
(fetchOptions.body as Record<string, unknown>).firstname =
newFirstName;
}

return fetchOptions;
}
Expand Down Expand Up @@ -147,15 +164,19 @@ describe('ApiReader', () => {
nock('http://fake-api').head('/profile').reply(500);

const apiReader = new ApiReader('http://fake-api');
let error;

try {
await apiReader.head('/profile');
} catch (err) {
error = err;
} catch (err: unknown) {
if (err instanceof Error) {
expect(err.message.startsWith('Http Error 500')).to.equal(true);
return;
} else {
throw err;
}
}

expect(error.code).to.equal('HTTP_500');
expect.fail('Should throw 500 error');
});

it('should send an head request on the api', async () => {
Expand All @@ -182,21 +203,24 @@ describe('ApiReader', () => {
nock('http://fake-api').post('/profile').reply(500);

const apiReader = new ApiReader('http://fake-api');
let error;

try {
await apiReader.post('/profile');
} catch (err) {
error = err;
} catch (err: unknown) {
if (err instanceof Error) {
expect(err.message.startsWith('Http Error 500')).to.equal(true);
return;
} else {
throw err;
}
}

expect(error.code).to.equal('HTTP_500');
expect.fail('Should throw 500 error');
});

it('should send a post request and json parse the response', async () => {
const payload = { firstname: 'Olivier', company: 'Sagacify' };
nock('http://fake-api', payload)
.post('/profile')
nock('http://fake-api')
.post('/profile', payload)
.reply(201, { id: 1, ...payload });

const apiReader = new ApiReader('http://fake-api');
Expand All @@ -219,15 +243,19 @@ describe('ApiReader', () => {
nock('http://fake-api').put('/profile').reply(500);

const apiReader = new ApiReader('http://fake-api');
let error;

try {
await apiReader.put('/profile');
} catch (err) {
error = err;
} catch (err: unknown) {
if (err instanceof Error) {
expect(err.message.startsWith('Http Error 500')).to.equal(true);
return;
} else {
throw err;
}
}

expect(error.code).to.equal('HTTP_500');
expect.fail('Should throw 500 error');
});

it('should send a put request and json parse the response', async () => {
Expand All @@ -250,20 +278,24 @@ describe('ApiReader', () => {
nock('http://fake-api').patch('/profile').reply(500);

const apiReader = new ApiReader('http://fake-api');
let error;

try {
await apiReader.patch('/profile');
} catch (err) {
error = err;
} catch (err: unknown) {
if (err instanceof Error) {
expect(err.message.startsWith('Http Error 500')).to.equal(true);
return;
} else {
throw err;
}
}

expect(error.code).to.equal('HTTP_500');
expect.fail('Should throw 500 error');
});

it('should send a patch request and json parse the response', async () => {
const payload = { id: 1, firstname: 'José', company: 'Sagacify' };
nock('http://fake-api', payload).patch('/profile').reply(200, payload);
nock('http://fake-api').patch('/profile', payload).reply(200, payload);

const apiReader = new ApiReader('http://fake-api');
const result = await apiReader.patch('/profile', { body: payload });
Expand Down
Loading

0 comments on commit 6afe755

Please sign in to comment.