feat: convert to TypeScript

BREAKING CHANGE: Conversion to TypeScript may cause API breakage

Signed-off-by: Rifa Achrinza <25147899+achrinza@users.noreply.github.com>
This commit is contained in:
Rifa Achrinza 2021-07-23 21:43:56 +08:00
parent 0d2220bbfa
commit df0b5ac9ab
No known key found for this signature in database
GPG Key ID: 20BEC73FE57F7CF2
22 changed files with 3739 additions and 221 deletions

4
.eslintignore Normal file
View File

@ -0,0 +1,4 @@
node_modules/
dist/
coverage/
.eslintrc.js

View File

@ -1,3 +0,0 @@
{
"extends": "loopback"
}

28
.eslintrc.js Normal file
View File

@ -0,0 +1,28 @@
module.exports = {
extends: [
'@loopback/eslint-config/eslintrc.js',
// 'plugin:prettier/recommended',
],
plugins: ['prettier'],
overrides: [
{
files: ['**/*.ts'],
rules: {
/*
* The mocha plugin reports the following signature as a violation of
* `mocha/handle-done-callback` (misinterpreting `this` as `done`).
*
* See https://github.com/lo1tuma/eslint-plugin-mocha/issues/270
*
* ```ts
* before(async function setupApplication(this: Mocha.Context) {
* this.timeout(6000);
* // ...
* }
* ```
*/
'mocha/handle-done-callback': 'off',
},
},
],
};

5
.mocharc.json Normal file
View File

@ -0,0 +1,5 @@
{
"exit": true,
"recursive": true,
"require": "source-map-support/register"
}

2
.prettierignore Normal file
View File

@ -0,0 +1,2 @@
dist
*.json

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"bracketSpacing": false,
"singleQuote": true,
"printWidth": 80,
"trailingComma": "all",
"arrowParens": "avoid"
}

33
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,33 @@
{
"editor.rulers": [80],
"editor.tabCompletion": "on",
"editor.tabSize": 2,
"editor.trimAutoWhitespace": true,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
"source.fixAll.eslint": true
},
"files.exclude": {
"**/.DS_Store": true,
"**/.git": true,
"**/.hg": true,
"**/.svn": true,
"**/CVS": true,
"dist": true,
},
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
"typescript.tsdk": "./node_modules/typescript/lib",
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false,
"typescript.preferences.quoteStyle": "single",
"eslint.run": "onSave",
"eslint.nodePath": "./node_modules",
"eslint.validate": [
"javascript",
"typescript"
]
}

65
index.d.ts vendored
View File

@ -1,65 +0,0 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: strong-error-handler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
// Type definitions for strong-error-handler 3.x
// Project: https://github.com/strongloop/strong-error-handler
// Definitions by: Kyusung Shim <https://github.com/shimks>
// TypeScript Version: 3.0
import * as Express from 'express';
export = errorHandlerFactory;
/**
* Creates a middleware function for error-handling
* @param options Options for error handler settings
*/
declare function errorHandlerFactory(
options?: errorHandlerFactory.ErrorHandlerOptions
): errorHandlerFactory.StrongErrorHandler;
declare namespace errorHandlerFactory {
/**
* Writes thrown error to response
* @param err Error to handle
* @param req Incoming request
* @param res Response
* @param options Options for error handler settings
*/
function writeErrorToResponse(
err: Error,
req: Express.Request,
res: Express.Response,
options?: ErrorWriterOptions
): void;
/**
* Error-handling middleware function. Includes server-side logging
*/
type StrongErrorHandler = (
err: Error,
req: Express.Request,
res: Express.Response,
next: (err?: any) => void
) => void;
/**
* Options for writing errors to the response
*/
interface ErrorWriterOptions {
debug?: boolean;
safeFields?: string[];
defaultType?: string;
negotiateContentType?: boolean;
rootProperty?: string | false;
}
/**
* Options for error-handling
*/
interface ErrorHandlerOptions extends ErrorWriterOptions {
log?: boolean;
}
}

3502
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,13 +4,13 @@
"license": "MIT", "license": "MIT",
"version": "4.0.0", "version": "4.0.0",
"engines": { "engines": {
"node": ">=10" "node": "10 || 12 || 14 || 16"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/loopbackio/strong-error-handler.git" "url": "https://github.com/loopbackio/strong-error-handler.git"
}, },
"main": "lib/handler.js", "main": "dist/handler.js",
"scripts": { "scripts": {
"lint": "eslint .", "lint": "eslint .",
"test": "mocha", "test": "mocha",
@ -21,19 +21,29 @@
"debug": "^4.1.1", "debug": "^4.1.1",
"ejs": "^3.1.3", "ejs": "^3.1.3",
"fast-safe-stringify": "^2.0.6", "fast-safe-stringify": "^2.0.6",
"http-errors": "^1.7.2",
"http-status": "^1.1.2", "http-status": "^1.1.2",
"js2xmlparser": "^4.0.0", "js2xmlparser": "^4.0.0",
"strong-globalize": "^6.0.1" "strong-globalize": "^6.0.1"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/config-conventional": "^12.1.4", "@commitlint/config-conventional": "^12.1.4",
"@loopback/build": "^7.0.0",
"@loopback/eslint-config": "^11.0.0",
"@types/chai": "^4.2.21",
"@types/debug": "^4.1.6",
"@types/ejs": "^3.0.7",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/http-errors": "^1.8.1",
"@types/mocha": "^8.2.3",
"chai": "^4.1.2", "chai": "^4.1.2",
"eslint": "^7.0.0", "eslint": "^7.0.0",
"eslint-config-loopback": "^13.1.0", "eslint-plugin-prettier": "^3.4.0",
"express": "^4.16.3", "express": "^4.16.3",
"mocha": "^9.0.2", "mocha": "^9.0.2",
"supertest": "^6.1.4" "supertest": "^6.1.4",
"tslib": "^1.14.1",
"typescript": "^4.3.5"
}, },
"browser": { "browser": {
"strong-error-handler": false "strong-error-handler": false

View File

@ -3,22 +3,19 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
'use strict';
module.exports = cloneAllProperties;
/** /**
* clone the error properties to the data objects * clone the error properties to the data objects
* [err.name, err.message, err.stack] are not enumerable properties * [err.name, err.message, err.stack] are not enumerable properties
* @param data Object to be altered * @param data Object to be altered
* @param err Error Object * @param err Error Object
*/ */
function cloneAllProperties(data, err) { export function cloneAllProperties(data: object, err: Error) {
data.name = err.name; data['name'] = err.name;
data.message = err.message; data['message'] = err.message;
for (const p in err) { for (const p in err) {
if ((p in data)) continue; if (p in data) continue;
data[p] = err[p]; data[p] = err[p];
} }
// stack is appended last to ensure order is the same for response // stack is appended last to ensure order is the same for response
data.stack = err.stack; data['stack'] = err.stack;
} }

View File

@ -3,15 +3,15 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
'use strict'; import accepts from 'accepts';
const accepts = require('accepts'); import debugFactory from 'debug';
const debug = require('debug')('strong-error-handler:http-response'); import util from 'util';
const sendJson = require('./send-json'); import {sendHtml} from './send-html';
const sendHtml = require('./send-html'); import {sendJson} from './send-json';
const sendXml = require('./send-xml'); import {sendXml} from './send-xml';
const util = require('util'); import {ErrorWriterOptions} from './types';
module.exports = negotiateContentProducer; const debug = debugFactory('strong-error-handler:http-response');
/** /**
* Handles req.accepts and req.query._format and options.defaultType * Handles req.accepts and req.query._format and options.defaultType
@ -22,11 +22,18 @@ module.exports = negotiateContentProducer;
* @param {Object} options options of strong-error-handler * @param {Object} options options of strong-error-handler
* @returns {Function} Operation function with signature `fn(res, data)` * @returns {Function} Operation function with signature `fn(res, data)`
*/ */
function negotiateContentProducer(req, logWarning, options) { export function negotiateContentProducer(
req,
logWarning,
options: ErrorWriterOptions,
) {
const SUPPORTED_TYPES = [ const SUPPORTED_TYPES = [
'application/json', 'json', 'application/json',
'text/html', 'html', 'json',
'text/xml', 'xml', 'text/html',
'html',
'text/xml',
'xml',
]; ];
options = options || {}; options = options || {};
@ -38,8 +45,12 @@ function negotiateContentProducer(req, logWarning, options) {
debug('Accepting options.defaultType `%s`', options.defaultType); debug('Accepting options.defaultType `%s`', options.defaultType);
defaultType = options.defaultType; defaultType = options.defaultType;
} else { } else {
debug('defaultType: `%s` is not supported, ' + debug(
'falling back to defaultType: `%s`', options.defaultType, defaultType); 'defaultType: `%s` is not supported, ' +
'falling back to defaultType: `%s`',
options.defaultType,
defaultType,
);
} }
} }
@ -58,13 +69,15 @@ function negotiateContentProducer(req, logWarning, options) {
if (options.negotiateContentType === false) { if (options.negotiateContentType === false) {
if (SUPPORTED_TYPES.indexOf(options.defaultType) > -1) { if (SUPPORTED_TYPES.indexOf(options.defaultType) > -1) {
debug('Forcing options.defaultType `%s`', debug('Forcing options.defaultType `%s`', options.defaultType);
options.defaultType);
contentType = options.defaultType; contentType = options.defaultType;
} else { } else {
debug('contentType: `%s` is not supported, ' + debug(
'falling back to contentType: `%s`', 'contentType: `%s` is not supported, ' +
options.defaultType, contentType); 'falling back to contentType: `%s`',
options.defaultType,
contentType,
);
} }
} }
@ -77,14 +90,20 @@ function negotiateContentProducer(req, logWarning, options) {
contentType = query._format; contentType = query._format;
} else { } else {
// format passed through query but not supported // format passed through query but not supported
const msg = util.format('Response _format "%s" is not supported' + const msg = util.format(
'used "%s" instead"', query._format, defaultType); 'Response _format "%s" is not supported' + 'used "%s" instead"',
query._format,
defaultType,
);
logWarning(msg); logWarning(msg);
} }
} }
debug('Content-negotiation: req.headers.accept: `%s` Resolved as: `%s`', debug(
req.headers.accept, contentType); 'Content-negotiation: req.headers.accept: `%s` Resolved as: `%s`',
req.headers.accept,
contentType,
);
return resolveOperation(contentType); return resolveOperation(contentType);
} }

View File

@ -3,14 +3,11 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
'use strict'; import httpStatus from 'http-status';
import {cloneAllProperties} from './clone';
import {ErrorWriterOptions} from './types';
const cloneAllProperties = require('../lib/clone.js'); export function buildResponseData(err: Error, options: ErrorWriterOptions) {
const httpStatus = require('http-status');
module.exports = buildResponseData;
function buildResponseData(err, options) {
// Debugging mode is disabled by default. When turned on (in dev), // Debugging mode is disabled by default. When turned on (in dev),
// all error properties (including) stack traces are sent in the response // all error properties (including) stack traces are sent in the response
const isDebugMode = options.debug; const isDebugMode = options.debug;
@ -50,16 +47,15 @@ function serializeArrayOfErrors(errors, options) {
const details = errors.map(e => buildResponseData(e, options)); const details = errors.map(e => buildResponseData(e, options));
return { return {
statusCode: 500, statusCode: 500,
message: 'Failed with multiple errors, ' + message:
'see `details` for more information.', 'Failed with multiple errors, ' + 'see `details` for more information.',
details: details, details: details,
}; };
} }
function fillStatusCode(data, err) { function fillStatusCode(data, err) {
data.statusCode = err.statusCode || err.status; data.statusCode = err.statusCode || err.status;
if (!data.statusCode || data.statusCode < 400) if (!data.statusCode || data.statusCode < 400) data.statusCode = 500;
data.statusCode = 500;
} }
function fillDebugData(data, err) { function fillDebugData(data, err) {
@ -82,7 +78,7 @@ function fillSafeFields(data, err, safeFields) {
safeFields = [safeFields]; safeFields = [safeFields];
} }
safeFields.forEach(function(field) { safeFields.forEach(function (field) {
if (err[field] !== undefined) { if (err[field] !== undefined) {
data[field] = err[field]; data[field] = err[field];
} }

View File

@ -3,18 +3,24 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
'use strict'; import debugFactory from 'debug';
import type Express from 'express';
import {HttpError, isHttpError} from 'http-errors';
import path from 'path';
import SG from 'strong-globalize';
import {negotiateContentProducer} from './content-negotiation';
import {buildResponseData} from './data-builder';
import {logToConsole} from './logger';
import {
ErrorHandlerOptions,
ErrorWriterOptions,
StrongErrorHandler,
} from './types';
const path = require('path');
const SG = require('strong-globalize');
SG.SetRootDir(path.resolve(__dirname, '..')); SG.SetRootDir(path.resolve(__dirname, '..'));
const buildResponseData = require('./data-builder'); const debug = debugFactory('strong-error-handler');
const debug = require('debug')('strong-error-handler');
const logToConsole = require('./logger');
const negotiateContentProducer = require('./content-negotiation');
function noop() { function noop() {}
}
/** /**
* Create a middleware error handler function. * Create a middleware error handler function.
@ -22,15 +28,20 @@ function noop() {
* @param {Object} options * @param {Object} options
* @returns {Function} * @returns {Function}
*/ */
function createStrongErrorHandler(options) { export function createStrongErrorHandler(
options = options || {}; options: ErrorHandlerOptions = {},
): StrongErrorHandler {
debug('Initializing with options %j', options); debug('Initializing with options %j', options);
// Log all errors via console.error (enabled by default) // Log all errors via console.error (enabled by default)
const logError = options.log !== false ? logToConsole : noop; const logError = options.log !== false ? logToConsole : noop;
return function strongErrorHandler(err, req, res, next) { return function strongErrorHandler(
err: Error,
req: Express.Request,
res: Express.Response,
next: (err: unknown) => void,
) {
logError(req, err); logError(req, err);
writeErrorToResponse(err, req, res, options); writeErrorToResponse(err, req, res, options);
}; };
@ -39,24 +50,27 @@ function createStrongErrorHandler(options) {
/** /**
* Writes thrown error to response * Writes thrown error to response
* *
* @param {Error} err * @param err
* @param {Express.Request} req * @param req
* @param {Express.Response} res * @param res
* @param {Object} options * @param options
*/ */
function writeErrorToResponse(err, req, res, options) { function writeErrorToResponse(
err: Error | HttpError,
req: Express.Request,
res: Express.Response,
options: ErrorWriterOptions = {},
) {
debug('Handling %s', err.stack || err); debug('Handling %s', err.stack || err);
options = options || {};
if (res.headersSent) { if (res.headersSent) {
debug('Response was already sent, closing the underlying connection'); debug('Response was already sent, closing the underlying connection');
return req.socket.destroy(); return req.socket.destroy();
} }
// this will alter the err object, to handle when res.statusCode is an error // this will alter the err object, to handle when res.statusCode is an error
if (!err.status && !err.statusCode && res.statusCode >= 400) if (!isHttpError(err) && res.statusCode >= 400)
err.statusCode = res.statusCode; (err as Partial<HttpError> & Error).statusCode = res.statusCode;
const data = buildResponseData(err, options); const data = buildResponseData(err, options);
debug('Response status %s data %j', data.statusCode, data); debug('Response status %s data %j', data.statusCode, data);
@ -67,11 +81,11 @@ function writeErrorToResponse(err, req, res, options) {
const sendResponse = negotiateContentProducer(req, warn, options); const sendResponse = negotiateContentProducer(req, warn, options);
sendResponse(res, data, options); sendResponse(res, data, options);
function warn(msg) { function warn(msg: string) {
res.header('X-Warning', msg); res.header('X-Warning', msg);
debug(msg); debug(msg);
} }
} }
exports = module.exports = createStrongErrorHandler; module.exports = createStrongErrorHandler;
exports.writeErrorToResponse = writeErrorToResponse; module.exports.writeErrorToResponse = writeErrorToResponse;

View File

@ -3,23 +3,25 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
'use strict'; import type Express from 'express';
import SG from 'strong-globalize';
import {format} from 'util';
const g = new SG();
const format = require('util').format; export function logToConsole(req: Express.Request, err: Error) {
const g = require('strong-globalize')();
module.exports = function logToConsole(req, err) {
if (!Array.isArray(err)) { if (!Array.isArray(err)) {
g.error('Request %s %s failed: %s', g.error('Request %s %s failed: %s', req.method, req.url, err.stack || err);
req.method, req.url, err.stack || err);
return; return;
} }
const errMsg = g.f('Request %s %s failed with multiple errors:\n', const errMsg = g.f(
req.method, req.url); 'Request %s %s failed with multiple errors:\n',
req.method,
req.url,
);
const errors = err.map(formatError).join('\n'); const errors = err.map(formatError).join('\n');
console.error(errMsg, errors); console.error(errMsg, errors);
}; }
function formatError(err) { function formatError(err) {
return format('%s', err.stack || err); return format('%s', err.stack || err);

View File

@ -3,10 +3,10 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
'use strict'; import ejs from 'ejs';
const ejs = require('ejs'); import type Express from 'express';
const fs = require('fs'); import fs from 'fs';
const path = require('path'); import path from 'path';
const assetDir = path.resolve(__dirname, '../views'); const assetDir = path.resolve(__dirname, '../views');
const compiledTemplates = { const compiledTemplates = {
@ -14,9 +14,11 @@ const compiledTemplates = {
default: loadDefaultTemplates(), default: loadDefaultTemplates(),
}; };
module.exports = sendHtml; export function sendHtml(
res: Express.Response,
function sendHtml(res, data, options) { data: ejs.Data['data'],
options: ejs.Data['options'],
) {
const toRender = {options, data}; const toRender = {options, data};
// TODO: ability to call non-default template functions from options // TODO: ability to call non-default template functions from options
const body = compiledTemplates.default(toRender); const body = compiledTemplates.default(toRender);
@ -27,9 +29,9 @@ function sendHtml(res, data, options) {
* Compile and cache the file with the `filename` key in options * Compile and cache the file with the `filename` key in options
* *
* @param filepath (description) * @param filepath (description)
* @returns {Function} render function with signature fn(data); * @returns Render function with signature fn(data);
*/ */
function compileTemplate(filepath) { function compileTemplate(filepath: string): ejs.TemplateFunction {
const options = {cache: true, filename: filepath}; const options = {cache: true, filename: filepath};
const fileContent = fs.readFileSync(filepath, 'utf8'); const fileContent = fs.readFileSync(filepath, 'utf8');
return ejs.compile(fileContent, options); return ejs.compile(fileContent, options);
@ -41,7 +43,7 @@ function loadDefaultTemplates() {
return compileTemplate(defaultTemplate); return compileTemplate(defaultTemplate);
} }
function sendResponse(res, body) { function sendResponse(res: Express.Response, body: string) {
res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(body); res.end(body);
} }

View File

@ -3,18 +3,20 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
'use strict'; import type Express from 'express';
import safeStringify from 'fast-safe-stringify';
const safeStringify = require('fast-safe-stringify'); export function sendJson(res: Express.Response, data, options) {
module.exports = function sendJson(res, data, options) {
options = options || {}; options = options || {};
// Set `options.rootProperty` to not wrap the data into an `error` object // Set `options.rootProperty` to not wrap the data into an `error` object
const err = options.rootProperty === false ? data : { const err =
// Use `options.rootProperty`, if not set, default to `error` options.rootProperty === false
[options.rootProperty || 'error']: data, ? data
}; : {
// Use `options.rootProperty`, if not set, default to `error`
[options.rootProperty || 'error']: data,
};
const content = safeStringify(err); const content = safeStringify(err);
res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(content, 'utf-8'); res.end(content, 'utf-8');
}; }

29
src/types.ts Normal file
View File

@ -0,0 +1,29 @@
import type Express from 'express';
/**
* Options for writing errors to the response
*/
export interface ErrorWriterOptions {
debug?: boolean;
safeFields?: string[];
defaultType?: string;
negotiateContentType?: boolean;
rootProperty?: string | false;
}
/**
* Options for error-handling
*/
export interface ErrorHandlerOptions extends ErrorWriterOptions {
log?: boolean;
}
/**
* Error-handling middleware function. Includes server-side logging
*/
export type StrongErrorHandler = (
err: Error,
req: Express.Request,
res: Express.Response,
next: (err?: unknown) => void,
) => void;

View File

@ -3,8 +3,6 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
'use strict';
const cloneAllProperties = require('../lib/clone.js'); const cloneAllProperties = require('../lib/clone.js');
const debug = require('debug')('test'); const debug = require('debug')('test');
const expect = require('chai').expect; const expect = require('chai').expect;
@ -177,7 +175,7 @@ describe('strong-error-handler', function() {
}); });
}); });
const _consoleError = console.error; const consoleError = console.error;
function redirectConsoleError() { function redirectConsoleError() {
logs = []; logs = [];
console.error = function() { console.error = function() {
@ -187,7 +185,7 @@ describe('strong-error-handler', function() {
} }
function restoreConsoleError() { function restoreConsoleError() {
console.error = _consoleError; console.error = consoleError;
logs = []; logs = [];
} }
}); });

21
tsconfig.build.json Normal file
View File

@ -0,0 +1,21 @@
{
"__comments__": [
"tsconfig.build.json is used by eslint to constrain files to be checked",
"tsconfig.json is used by tsc"
],
"compilerOptions": {
"skipLibCheck": true,
"noEmit": true
},
"include": [
"**/.eslintrc.js",
"**/*.js",
"**/*.ts"
],
"exclude": [
"**/node_modules/**",
"**/dist/**",
"**/*.d.ts",
"coverage"
]
}

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "@loopback/build/config/tsconfig.common.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": [
"src"
]
}