Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,13 @@
},
"license": "MIT",
"peerDependencies": {
"@hapi/boom": "^9.1.4",
"express": "^5.1.0",
"json-mask": "^0.3.8",
"mongoose": "^5.9.4",
"zod": "^4.2.1"
},
"devDependencies": {
"@hapi/boom": "^9.1.4",
"@istanbuljs/nyc-config-typescript": "^0.1.3",
"@types/boom": "^7.3.5",
"@types/chai": "^4.2.3",
"@types/chai-as-promised": "^7.1.2",
"@types/express": "^5.0.6",
Expand Down
23 changes: 0 additions & 23 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 30 additions & 12 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Boom from '@hapi/boom';
import {
AttachDataReqsSatisfiedOptional,
DoWorkReqsSatisfiedOptional,
Expand Down Expand Up @@ -33,6 +32,23 @@ import {
SanitizeResponseReqsSatisfied,
} from './types';

export class HipError extends Error {
public readonly statusCode: number;
public readonly output: { statusCode: number; payload: { message: string } };

constructor(statusCode: number, message: string) {
super(message);
this.name = 'HipError';
this.statusCode = statusCode;
// output shape for backwards compatibility with adapters that read .output
this.output = { statusCode, payload: { message } };
}

static isHipError(err: unknown): err is HipError {
return err instanceof HipError;
}
}

export function withDefaultImplementations<
TStrategy extends HasAllNotRequireds &
HasAllRequireds &
Expand Down Expand Up @@ -165,8 +181,8 @@ function transformThrowSync<TOrigFn extends (param: any) => any>(
try {
return origFn(origParam);
} catch (exception) {
if (exception instanceof HipRedirectException || Boom.isBoom(exception)) {
// Don't transform redirect exceptions or intentionally constructed boom errors
if (exception instanceof HipRedirectException || HipError.isHipError(exception)) {
// Don't transform redirect exceptions or intentionally constructed HipErrors
throw exception;
} else {
// All other uncaught exceptions transform to whatever is requested
Expand All @@ -183,8 +199,8 @@ function transformThrowPossiblyAsync<
origParam: Parameters<TOrigFn>[0]
): Promise<PromiseResolveOrSync<ReturnType<TOrigFn>>> {
return Promise.resolve(origFn(origParam)).catch(exception => {
if (exception instanceof HipRedirectException || Boom.isBoom(exception)) {
// Don't transform redirect exceptions or intentionally constructed boom errors
if (exception instanceof HipRedirectException || HipError.isHipError(exception)) {
// Don't transform redirect exceptions or intentionally constructed HipErrors
throw exception;
} else {
// All other uncaught exceptions transform to whatever is requested
Expand Down Expand Up @@ -212,7 +228,7 @@ export async function executeHipthrustable<
unsafeQueryParams: TUnsafeQueryParams,
unsafeBody: TUnsafeBody
) {
const badDataThrow = Boom.badData('User input sanitization failure');
const badDataThrow = new HipError(422, 'User input sanitization failure');
const safeInitPreContext = transformThrowSync(
badDataThrow,
requestHandler.initPreContext,
Expand Down Expand Up @@ -241,7 +257,8 @@ export async function executeHipthrustable<
body: safeBody,
};

const forbiddenPreAuthThrow = Boom.forbidden(
const forbiddenPreAuthThrow = new HipError(
403,
'General pre-authorization lacking for this resource'
);

Expand All @@ -265,7 +282,7 @@ export async function executeHipthrustable<
...preAuthorizeContextOut,
};

const notFoundThrow = Boom.notFound('Resource not found');
const notFoundThrow = new HipError(404, 'Resource not found');
const attachedDataContextOnly =
(await transformThrowPossiblyAsync(
notFoundThrow,
Expand All @@ -274,7 +291,8 @@ export async function executeHipthrustable<
)) || {};
const attachedDataContext = { ...preAuthContext, ...attachedDataContextOnly };

const forbiddenFinalAuthThrow = Boom.forbidden(
const forbiddenFinalAuthThrow = new HipError(
403,
'General authorization lacking for this resource'
);

Expand Down Expand Up @@ -313,12 +331,12 @@ export async function executeHipthrustable<
const responseAndStatus = { response: safeResponse, status: status || 200 };
return responseAndStatus;
} catch (exception) {
if (exception instanceof HipRedirectException || Boom.isBoom(exception)) {
// Don't transform redirect exceptions or intentionally constructed boom errors
if (exception instanceof HipRedirectException || HipError.isHipError(exception)) {
// Don't transform redirect exceptions or intentionally constructed HipErrors
throw exception;
} else {
// All other uncaught exceptions transform to generic 500s here
throw Boom.badImplementation('Uncaught exception');
throw new HipError(500, 'Uncaught exception');
}
}
}
Expand Down
23 changes: 12 additions & 11 deletions src/mongoose.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Boom from '@hapi/boom';
import { HipError } from './core';
import {
AttachData,
DoWork,
Expand Down Expand Up @@ -36,11 +36,11 @@ export function htMongooseFactory(mongoose: any) {
// validation passes when it shouldn't - this example is b/c
// mongoose won't initialize with invalid ObjectIds passed to it.
if (!id || !id.toString()) {
throw Boom.badRequest('Missing dependent resource ID');
throw new HipError(400, 'Missing dependent resource ID');
}
const result = await Model.findById(id).exec();
if (!result) {
throw Boom.notFound('Resource not found');
throw new HipError(404, 'Resource not found');
}
return result;
};
Expand All @@ -50,15 +50,15 @@ export function htMongooseFactory(mongoose: any) {
// tslint:disable-next-line:only-arrow-functions
return async function(fieldValue: any) {
if (!fieldValue || !fieldValue.toString()) {
throw Boom.badRequest('Missing dependent resource value');
throw new HipError(400, 'Missing dependent resource value');
}
const result = await Model.findOne({
[fieldName]: {
$eq: fieldValue,
},
}).exec();
if (!result) {
throw Boom.notFound('Resource not found');
throw new HipError(404, 'Resource not found');
}
return result;
};
Expand Down Expand Up @@ -127,7 +127,7 @@ export function htMongooseFactory(mongoose: any) {
const doc = DocFactory(unsafeParams);
const validateErrors = doc.validateSync();
if (validateErrors !== undefined) {
throw Boom.badRequest('Params not valid');
throw new HipError(400, 'Params not valid');
}
// @tswtf: why do I need to force this?!
return doc.toObject({ transform: stripIdTransform }) as TSafeParam;
Expand All @@ -144,7 +144,7 @@ export function htMongooseFactory(mongoose: any) {
const doc = DocFactory(unsafeQueryParams);
const validateErrors = doc.validateSync();
if (validateErrors !== undefined) {
throw Boom.badRequest('Query params not valid');
throw new HipError(400, 'Query params not valid');
}
return doc.toObject({ transform: stripIdTransform }) as TSafeQueryParam;
});
Expand All @@ -163,7 +163,7 @@ export function htMongooseFactory(mongoose: any) {
validateModifiedOnly: true,
});
if (validateErrors !== undefined) {
throw Boom.badRequest('Body not valid');
throw new HipError(400, 'Body not valid');
}
// @tswtf: why do I need to force this?!
return doc.toObject({ transform: stripIdTransform }) as TSafeBody;
Expand All @@ -176,12 +176,13 @@ export function htMongooseFactory(mongoose: any) {
try {
return await context[propertyKeyOfDocument].save();
} catch (err) {
throw Boom.badData(
throw new HipError(
422,
'Unable to save. Please check if data sent was valid.'
);
}
} else {
throw Boom.badRequest('Resource not found');
throw new HipError(400, 'Resource not found');
}
});
}
Expand All @@ -196,7 +197,7 @@ export function htMongooseFactory(mongoose: any) {
context[propertyKeyWithNewData]
);
} else {
throw Boom.badRequest('Resource not found');
throw new HipError(400, 'Resource not found');
}
});
}
Expand Down
12 changes: 6 additions & 6 deletions src/zod.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Boom from '@hapi/boom';
import { z } from 'zod';
import { HipError } from './core';
import {
AttachData,
SanitizeBody,
Expand All @@ -22,7 +22,7 @@ export function htZodFactory() {
return SanitizeParams((unsafeParams: TUnsafeParam) => {
const parseResult = schema.safeParse(unsafeParams);
if (!parseResult.success) {
throw Boom.badRequest('Params not valid', parseResult.error);
throw new HipError(400, 'Params not valid');
}
return stripIdTransform(parseResult.data) as TSafeParam;
});
Expand All @@ -36,7 +36,7 @@ export function htZodFactory() {
return SanitizeQueryParams((unsafeQueryParams: TUnsafeQueryParam) => {
const parseResult = schema.safeParse(unsafeQueryParams);
if (!parseResult.success) {
throw Boom.badRequest('Query params not valid', parseResult.error);
throw new HipError(400, 'Query params not valid');
}
return stripIdTransform(parseResult.data) as TSafeQueryParam;
});
Expand All @@ -54,7 +54,7 @@ export function htZodFactory() {
return SanitizeBody((unsafeBody: TUnsafeBody) => {
const parseResult = effectiveSchema.safeParse(unsafeBody);
if (!parseResult.success) {
throw Boom.badRequest('Body not valid', parseResult.error);
throw new HipError(400, 'Body not valid');
}
return stripIdTransform(parseResult.data) as TSafeBody;
});
Expand All @@ -70,7 +70,7 @@ export function htZodFactory() {
// In production, you might want to log this error but return a safe default
// or throw an internal server error since response validation failing
// indicates a bug in your code, not invalid user input
throw Boom.internal('Response validation failed', parseResult.error);
throw new HipError(500, 'Response validation failed');
}
return parseResult.data as TSafeResponse;
});
Expand All @@ -84,7 +84,7 @@ export function htZodFactory() {
return AttachData((context: TContextIn) => {
const parseResult = schema.safeParse(context[pojoKey]);
if (!parseResult.success) {
throw Boom.badRequest('Data validation failed', parseResult.error);
throw new HipError(400, 'Data validation failed');
}
return {
[newValidatedKey]: parseResult.data,
Expand Down