Skip to content

Commit

Permalink
Merge pull request #5 from muralco/add/router-error-handler
Browse files Browse the repository at this point in the history
Add router based error handler
  • Loading branch information
pzavolinsky authored Nov 26, 2019
2 parents ffd88ed + 007087a commit 9228630
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 14 deletions.
16 changes: 16 additions & 0 deletions features/advanced.feature
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,22 @@ Scenario: get TODOs by id
}
"""

Scenario: delete USER by id
Given a user U with { "username": "${random}", "name": "Ringo ${random}" }
When DELETE /users/purge/${U.username}
Then the response is 200 and the payload includes
"""
{
"username": "${U.username}"
}
"""
And the user U was deleted

Scenario: do not delete a not existing user and respond with a custom error
When DELETE /users/purge/${U.username}
Then the response is 400
And the response payload at error is "invalid_user"

# Edges

Scenario: create a user without username
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "async-app",
"version": "3.0.0",
"version": "4.0.0",
"description": "An express wrapper for handling async middlewares, order middlewares, schema validator, and other stuff",
"main": "index.js",
"types": "index.d.ts",
Expand Down
15 changes: 14 additions & 1 deletion src/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Converter,
Decorator,
Entities,
ErrorHandlerFn,
isAsyncMiddleware,
isMiddleware,
isNumber,
Expand All @@ -16,6 +17,11 @@ import {

const DEFAULT_STATUS_CODE = 200;

interface AsyncOptions<TEntities extends Entities, TSchema> {
compileSchema?: CompileSchema<TSchema>;
errorHandler?: ErrorHandlerFn<TEntities>;
}

type Options = {
statusCode: number;
validateSchema?: ValidateSchema;
Expand All @@ -35,6 +41,7 @@ const copyDecorators = <TSrc extends Decorator, TDest extends Decorator>(
const mapMiddleware = <TEntities extends Entities>(
middleware: Middleware<TEntities>,
options?: Options,
errorHandler?: ErrorHandlerFn<TEntities>,
): CommonMiddleware<TEntities> => {
const fn: CommonMiddleware<TEntities> = async (req, res, next) => {
try {
Expand Down Expand Up @@ -87,6 +94,10 @@ const mapMiddleware = <TEntities extends Entities>(

const { statusCode, error, extra } = err;

// User defined error handler for all middlewares in app
if (errorHandler) {
return errorHandler(err, req, res, next);
}
return res.status(statusCode || 500).send({ error, ...extra });
}
};
Expand All @@ -97,7 +108,7 @@ const mapMiddleware = <TEntities extends Entities>(
};

export default <TEntities extends Entities, TSchema>(
compileSchema?: CompileSchema<TSchema>,
{ errorHandler, compileSchema }: AsyncOptions<TEntities, TSchema> = {},
): Converter<TEntities, TSchema> => (args, context) => {
const statusCode = args.find(isNumber) || DEFAULT_STATUS_CODE;

Expand All @@ -119,6 +130,8 @@ export default <TEntities extends Entities, TSchema>(
m === lastMiddleware
? { statusCode, validateSchema }
: undefined,
// Passes a custom error handler
errorHandler,
)
: m,
);
Expand Down
10 changes: 7 additions & 3 deletions src/create-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,13 @@ export const createApp = <TEntities extends Entities = Entities, TSchema = {}>(
));
}

const async = opts && opts.compileSchemaFn && opts.validateResponseSchema
? asyncConverter<TEntities, TSchema>(opts.compileSchemaFn)
: asyncConverter<TEntities, TSchema>();
const async = asyncConverter<TEntities, TSchema>({
compileSchema:
opts && opts.compileSchemaFn && opts.validateResponseSchema
? opts.compileSchemaFn
: undefined,
errorHandler: opts && opts.errorHandlerFn ? opts.errorHandlerFn : undefined,
});

METHODS.forEach(m =>
app[m] = patchMethod<TEntities, TSchema>(
Expand Down
3 changes: 3 additions & 0 deletions src/examples/advanced/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import createApp, { Req } from './async-app';
import can from './can';
import { addTodo, addUser, getTodosForUser } from './db';
import load from './load';
import purgeUser from './purgeUser';

const app = createApp();
app.use(bodyParser.json());
Expand Down Expand Up @@ -58,6 +59,8 @@ app.get(
(req: Req<'user'>) => req.user,
);

app.use('/users/purge', purgeUser);

// --- TODOs ---------------------------------------------------------------- //
app.post(
'/todos/:username',
Expand Down
25 changes: 16 additions & 9 deletions src/examples/advanced/async-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ToDo, User } from './db';

// In your case this is `from 'async-app'`
import createApp, { Entities, Req as Request } from '../..';
import { ErrorHandlerFn } from '../../types';

// This type represents all the custom `req` keys that we could have, in this
// example those are `res.user` and `req.todo`.
Expand All @@ -24,15 +25,21 @@ export interface ExampleEntities extends Entities {

// This type helps us write middlewares that explicitly declare which custom
// keys from `req` they are going to use (e.g. `(req: Req<'user'>) => req.user`)
export type Req<T extends keyof ExampleEntities = '_'> =
Request<ExampleEntities, T>;
export type Req<T extends keyof ExampleEntities = '_'> = Request<
ExampleEntities,
T
>;

export { ErrorHandlerFn } from '../..';

// This function replaces `express()` as in `const app = express()`;
export default () => createApp<ExampleEntities, Type>({
// In here you specify additional `async-app` options...
export default (errorHandlerFn?: ErrorHandlerFn<ExampleEntities>) =>
createApp<ExampleEntities, Type>({
// In here you specify additional `async-app` options...

// The following line enables schema validation using `mural-schema`. Note
// that you can easily change your schema validation module by specifing add
// different `compileSchemaFn`.
compileSchemaFn: schema => parseSchema(schema),
});
// The following line enables schema validation using `mural-schema`. Note
// that you can easily change your schema validation module by specifing add
// different `compileSchemaFn`.
compileSchemaFn: schema => parseSchema(schema),
errorHandlerFn,
});
9 changes: 9 additions & 0 deletions src/examples/advanced/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,12 @@ export const getTodo = async (id: number) =>

export const getTodosForUser = async (username: string) =>
Object.values(DB.todos).filter(t => t.owner === username);

export const removeUser = async (username: string) => {
const exists = username in DB.users;
const userToDelete = exists ? new Object(DB.users[username]) : {};
if (exists) {
delete DB.users[username];
}
return userToDelete;
};
23 changes: 23 additions & 0 deletions src/examples/advanced/purgeUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import createApp, { ErrorHandlerFn, ExampleEntities, Req } from './async-app';
import { removeUser } from './db';
import load from './load';

const handleLoaderErrors: ErrorHandlerFn<ExampleEntities> =
(err, _, res, __) => {
const { statusCode, error, extra } = err;
if (err.error === 'USER') {
return res.status(400).send({ error: 'invalid_user', ...extra });
}
return res.status(statusCode || 500).send({ error, ...extra });
};

const app = createApp(handleLoaderErrors);

app.delete(
'/:username',
'Deletes a user',
load.user.fromParams(),
(req: Req<'user'>) => removeUser(req.user.username),
);

export default app;
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Express, NextFunction, Request, Response } from 'express';
import { CustomError } from './error';

// === General ============================================================== //
export interface Context {
Expand Down Expand Up @@ -92,6 +93,13 @@ export type GenerateSchemaErrorFn = (
source: string,
) => any;

export type ErrorHandlerFn<TEntities extends Entities> = (
errors: CustomError,
req: Req<TEntities, keyof TEntities>,
res: Response,
next: NextFunction,
) => any;

// === Arguments ============================================================ //
export type MiddlewareArg<TEntities extends Entities> =
| CommonMiddleware<TEntities>
Expand Down Expand Up @@ -144,6 +152,7 @@ export interface Opts<
converters?: Converter<TEntities, TSchema>[];
compileSchemaFn?: CompileSchema<TSchema>;
generateSchemaErrorFn?: GenerateSchemaErrorFn;
errorHandlerFn?: ErrorHandlerFn<TEntities>;
validateResponseSchema?: boolean;
}

Expand Down

0 comments on commit 9228630

Please sign in to comment.