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

View File

@ -3,22 +3,19 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
module.exports = cloneAllProperties;
/**
* clone the error properties to the data objects
* [err.name, err.message, err.stack] are not enumerable properties
* @param data Object to be altered
* @param err Error Object
*/
function cloneAllProperties(data, err) {
data.name = err.name;
data.message = err.message;
export function cloneAllProperties(data: object, err: Error) {
data['name'] = err.name;
data['message'] = err.message;
for (const p in err) {
if ((p in data)) continue;
if (p in data) continue;
data[p] = err[p];
}
// 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.
// License text available at https://opensource.org/licenses/MIT
'use strict';
const accepts = require('accepts');
const debug = require('debug')('strong-error-handler:http-response');
const sendJson = require('./send-json');
const sendHtml = require('./send-html');
const sendXml = require('./send-xml');
const util = require('util');
import accepts from 'accepts';
import debugFactory from 'debug';
import util from 'util';
import {sendHtml} from './send-html';
import {sendJson} from './send-json';
import {sendXml} from './send-xml';
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
@ -22,11 +22,18 @@ module.exports = negotiateContentProducer;
* @param {Object} options options of strong-error-handler
* @returns {Function} Operation function with signature `fn(res, data)`
*/
function negotiateContentProducer(req, logWarning, options) {
export function negotiateContentProducer(
req,
logWarning,
options: ErrorWriterOptions,
) {
const SUPPORTED_TYPES = [
'application/json', 'json',
'text/html', 'html',
'text/xml', 'xml',
'application/json',
'json',
'text/html',
'html',
'text/xml',
'xml',
];
options = options || {};
@ -38,8 +45,12 @@ function negotiateContentProducer(req, logWarning, options) {
debug('Accepting options.defaultType `%s`', options.defaultType);
defaultType = options.defaultType;
} else {
debug('defaultType: `%s` is not supported, ' +
'falling back to defaultType: `%s`', options.defaultType, defaultType);
debug(
'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 (SUPPORTED_TYPES.indexOf(options.defaultType) > -1) {
debug('Forcing options.defaultType `%s`',
options.defaultType);
debug('Forcing options.defaultType `%s`', options.defaultType);
contentType = options.defaultType;
} else {
debug('contentType: `%s` is not supported, ' +
debug(
'contentType: `%s` is not supported, ' +
'falling back to contentType: `%s`',
options.defaultType, contentType);
options.defaultType,
contentType,
);
}
}
@ -77,14 +90,20 @@ function negotiateContentProducer(req, logWarning, options) {
contentType = query._format;
} else {
// format passed through query but not supported
const msg = util.format('Response _format "%s" is not supported' +
'used "%s" instead"', query._format, defaultType);
const msg = util.format(
'Response _format "%s" is not supported' + 'used "%s" instead"',
query._format,
defaultType,
);
logWarning(msg);
}
}
debug('Content-negotiation: req.headers.accept: `%s` Resolved as: `%s`',
req.headers.accept, contentType);
debug(
'Content-negotiation: req.headers.accept: `%s` Resolved as: `%s`',
req.headers.accept,
contentType,
);
return resolveOperation(contentType);
}

View File

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

View File

@ -3,18 +3,24 @@
// This file is licensed under the MIT License.
// 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, '..'));
const buildResponseData = require('./data-builder');
const debug = require('debug')('strong-error-handler');
const logToConsole = require('./logger');
const negotiateContentProducer = require('./content-negotiation');
const debug = debugFactory('strong-error-handler');
function noop() {
}
function noop() {}
/**
* Create a middleware error handler function.
@ -22,15 +28,20 @@ function noop() {
* @param {Object} options
* @returns {Function}
*/
function createStrongErrorHandler(options) {
options = options || {};
export function createStrongErrorHandler(
options: ErrorHandlerOptions = {},
): StrongErrorHandler {
debug('Initializing with options %j', options);
// Log all errors via console.error (enabled by default)
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);
writeErrorToResponse(err, req, res, options);
};
@ -39,24 +50,27 @@ function createStrongErrorHandler(options) {
/**
* Writes thrown error to response
*
* @param {Error} err
* @param {Express.Request} req
* @param {Express.Response} res
* @param {Object} options
* @param err
* @param req
* @param res
* @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);
options = options || {};
if (res.headersSent) {
debug('Response was already sent, closing the underlying connection');
return req.socket.destroy();
}
// this will alter the err object, to handle when res.statusCode is an error
if (!err.status && !err.statusCode && res.statusCode >= 400)
err.statusCode = res.statusCode;
if (!isHttpError(err) && res.statusCode >= 400)
(err as Partial<HttpError> & Error).statusCode = res.statusCode;
const data = buildResponseData(err, options);
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);
sendResponse(res, data, options);
function warn(msg) {
function warn(msg: string) {
res.header('X-Warning', msg);
debug(msg);
}
}
exports = module.exports = createStrongErrorHandler;
exports.writeErrorToResponse = writeErrorToResponse;
module.exports = createStrongErrorHandler;
module.exports.writeErrorToResponse = writeErrorToResponse;

View File

@ -3,23 +3,25 @@
// This file is licensed under the MIT License.
// 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;
const g = require('strong-globalize')();
module.exports = function logToConsole(req, err) {
export function logToConsole(req: Express.Request, err: Error) {
if (!Array.isArray(err)) {
g.error('Request %s %s failed: %s',
req.method, req.url, err.stack || err);
g.error('Request %s %s failed: %s', req.method, req.url, err.stack || err);
return;
}
const errMsg = g.f('Request %s %s failed with multiple errors:\n',
req.method, req.url);
const errMsg = g.f(
'Request %s %s failed with multiple errors:\n',
req.method,
req.url,
);
const errors = err.map(formatError).join('\n');
console.error(errMsg, errors);
};
}
function formatError(err) {
return format('%s', err.stack || err);

View File

@ -3,10 +3,10 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
const ejs = require('ejs');
const fs = require('fs');
const path = require('path');
import ejs from 'ejs';
import type Express from 'express';
import fs from 'fs';
import path from 'path';
const assetDir = path.resolve(__dirname, '../views');
const compiledTemplates = {
@ -14,9 +14,11 @@ const compiledTemplates = {
default: loadDefaultTemplates(),
};
module.exports = sendHtml;
function sendHtml(res, data, options) {
export function sendHtml(
res: Express.Response,
data: ejs.Data['data'],
options: ejs.Data['options'],
) {
const toRender = {options, data};
// TODO: ability to call non-default template functions from options
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
*
* @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 fileContent = fs.readFileSync(filepath, 'utf8');
return ejs.compile(fileContent, options);
@ -41,7 +43,7 @@ function loadDefaultTemplates() {
return compileTemplate(defaultTemplate);
}
function sendResponse(res, body) {
function sendResponse(res: Express.Response, body: string) {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(body);
}

View File

@ -3,18 +3,20 @@
// This file is licensed under the MIT License.
// 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');
module.exports = function sendJson(res, data, options) {
export function sendJson(res: Express.Response, data, options) {
options = options || {};
// Set `options.rootProperty` to not wrap the data into an `error` object
const err = options.rootProperty === false ? data : {
const err =
options.rootProperty === false
? data
: {
// Use `options.rootProperty`, if not set, default to `error`
[options.rootProperty || 'error']: data,
};
const content = safeStringify(err);
res.setHeader('Content-Type', 'application/json; charset=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.
// License text available at https://opensource.org/licenses/MIT
'use strict';
const cloneAllProperties = require('../lib/clone.js');
const debug = require('debug')('test');
const expect = require('chai').expect;
@ -177,7 +175,7 @@ describe('strong-error-handler', function() {
});
});
const _consoleError = console.error;
const consoleError = console.error;
function redirectConsoleError() {
logs = [];
console.error = function() {
@ -187,7 +185,7 @@ describe('strong-error-handler', function() {
}
function restoreConsoleError() {
console.error = _consoleError;
console.error = consoleError;
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"
]
}