before-hook
Version:
A modern pre-hook JS library that's just easy, built with love and style.
515 lines (429 loc) • 13.7 kB
JavaScript
const SYMBOL_ERR_TYPE = Symbol("SYMBOL_BEFOREHOOK_ERR_TYPE");
const SYMBOL_SHORT_CIRCUIT_TYPE = Symbol(
"SYMBOL_BEFOREHOOK_SHORT_CIRCUIT_TYPE"
);
const SYMBOL_MIDDLEWARE_ON_CATCH = Symbol(
"SYMBOL_BEFOREHOOK_MIDDLEWARE_ON_CATCH"
);
const SYMBOL_MIDDLEWARE_ID = Symbol("SYMBOL_BEFOREHOOK_MIDDLEWARE_ID");
const isError = e => {
return e && e.stack && e.message;
};
const objectAssignIfExists = (...args) => {
const def = { ...args[1] };
const overrideIfExist = { ...args[2] };
Object.keys(def).forEach(k => {
if (overrideIfExist[k]) {
def[k] = overrideIfExist[k];
}
});
return { ...args[0], ...def };
};
const MiddlewareHelpersInit = () => {
const pvtLogger = {
/* eslint-disable-next-line no-console */
log: (...args) => args.forEach(l => console.log(l.message || l)),
/* eslint-disable-next-line no-console */
logError: (...args) => args.forEach(l => console.error(l.message || l))
};
const reply = obj => {
// TODO: this is redundunt to another declaration of reply
/* eslint-disable-next-line no-param-reassign */
const customError = Error(JSON.stringify(obj));
customError[SYMBOL_ERR_TYPE] = true;
customError[SYMBOL_SHORT_CIRCUIT_TYPE] = "reply";
throw customError;
};
return () => ({
reply,
getLogger: () => pvtLogger
});
};
/* deprecated const setState = (objs, oldState, state) => {
const mutatedOldState = oldState;
const newState = state;
Object.keys(objs).forEach(key => {
newState[key] = objs[key];
mutatedOldState[key] = objs[key];
});
return mutatedOldState;
};
const setContext = setState; */
/* deprecated const simpleClone = objectToClone =>
/* JSON.parse(JSON.stringify( * / objectToClone; /* )) * /
const clone = simpleClone; */
const BaseMiddlewareHandlerInit = handler => {
const dispatchFn = async (...options) => {
const [instanceMethods, ...args] = options;
const { getStateTree } = instanceMethods;
try {
await handler(
{
getParams: () => args,
reply: obj => {
/* eslint-disable-next-line no-param-reassign */
const shortCircuitErrorObject = Error(JSON.stringify(obj));
shortCircuitErrorObject[SYMBOL_ERR_TYPE] = true;
shortCircuitErrorObject[SYMBOL_SHORT_CIRCUIT_TYPE] = "reply";
throw shortCircuitErrorObject;
},
next: () => {
const shortCircuitErrorObject = Error("next is called.");
shortCircuitErrorObject[SYMBOL_SHORT_CIRCUIT_TYPE] = "next";
},
getHelpers: MiddlewareHelpersInit()
},
{ ...getStateTree() } // TODO: Non-built-in methods
);
} catch (error) {
/* ignore, next() is called */
if (error[SYMBOL_SHORT_CIRCUIT_TYPE] === "next") {
return args;
}
throw error;
}
return args;
};
return dispatchFn;
};
const BaseMiddleware = ({ handler, configure } = {}) => {
if (!(typeof handler === "function")) {
throw Error(`Custom middlewares must define a "handler"`);
}
let pre = async () => {};
pre = BaseMiddlewareHandlerInit(handler);
pre[SYMBOL_MIDDLEWARE_ID] = true;
if (configure && configure.augmentMethods) {
const { augmentMethods = {} } = configure;
const configurableMethods = ["onCatch"];
configurableMethods.forEach(fnName => {
const newMethod = augmentMethods[fnName];
if (typeof newMethod === "function") {
if (fnName === "onCatch") {
pre[SYMBOL_MIDDLEWARE_ON_CATCH] = (oldMethod, e, params) => {
return newMethod(() => oldMethod(e), {
prevRawMethod: oldMethod,
arg: e,
...params
});
};
}
}
});
}
return pre;
};
const BodyParserMiddleware = () => {
return BaseMiddleware({
handler: async ({ getParams }) => {
const [event] = getParams();
if (Object.keys({ ...event.body }).length) {
event.body = JSON.parse(event.body);
}
}
});
};
const CreateInstance = options => {
let settings = {
DEBUG: false,
stopOnCatch: true,
register: []
};
settings = objectAssignIfExists({}, settings, options);
let pvtDispatched = false;
const stackedHooks = [];
let isHandlerFed = false;
let handlerLength = -1;
let handler = async () => {};
let FODispatch = async () => {};
/*
*
* CONFIGURABLES - START
*
*/
let pvtLogger = {
/* eslint-disable-next-line no-console */
log: (...args) => args.forEach(l => console.log(l.message || l)),
/* eslint-disable-next-line no-console */
logError: (...args) => args.forEach(l => console.error(l.message || l)),
logWarning: (...args) =>
/* eslint-disable-next-line no-console */
args.forEach(l => console.error(`WARNING: ${l.message || l}`))
};
/* logs are off by default */
if (settings.DEBUG !== true) {
pvtLogger.log = () => {};
pvtLogger.logError = () => {};
pvtLogger.logWarning = () => {};
}
/* start plugins state tree */
const stateTree = {};
const setStateTree = (treeKey, obj) => {
if (typeof stateTree[treeKey] === 'undefined') {
/* eslint-disable-next-line prefer-destructuring */
stateTree[treeKey] = obj;
return;
}
throw Error(`tree key "${treeKey}" already in use`);
};
const getStateTree = () => stateTree;
/* end plugins state tree */
let onReturnObject = args => args;
const onReply = (...args) => onReturnObject(...args);
let onCatch = (...args) => {
const [e] = args;
if (!isError(e)) {
pvtLogger.logError(
`Short circuit handler onCatch is expecting an Error but got ${e.toString &&
e.toString()}`
);
// TODO: expose "reply" instead and not prevMethod...
if (typeof e === "object" && e.statusCode >= 400) {
return onReturnObject(e);
}
}
try {
if (e[SYMBOL_ERR_TYPE] === true) {
// if (e[SYMBOL_SHORT_CIRCUIT_TYPE] === "reply") {
// return onReply(JSON.parse(e.message));
// }
return onReturnObject(JSON.parse(e.message));
}
pvtLogger.logError(e);
return onReturnObject({
statusCode: 500,
body: `${e.message}`
});
} catch (parseError) {
pvtLogger.logError(parseError);
return onReturnObject({
statusCode: 500,
body: `${parseError.message} - ${e && e.message ? e.message : ""}`
});
}
};
let handlerCallWrapper = (...args) => {
return handler(...args);
};
/**
*
* CONFIGURABLES - END
*
* */
const configure = ({ augmentMethods = {} } = {}) => {
const configurableMethods = [
"onCatch",
"onReturnObject",
"handlerCallWrapper",
"pvtLogger"
];
configurableMethods.forEach(fnName => {
const newMethod = augmentMethods[fnName];
if (typeof newMethod === "function") {
if (fnName === "onReturnObject") {
const oldMethod = onReturnObject;
onReturnObject = (arg1, params) => {
return newMethod(() => oldMethod(arg1), {
prevRawMethod: oldMethod,
arg: arg1,
...params
});
};
} else if (fnName === "onCatch") {
const oldMethod = onCatch;
onCatch = (arg1, params) => {
return newMethod(() => oldMethod(arg1), {
prevRawMethod: oldMethod,
arg: arg1,
...params
});
};
} else if (fnName === "handlerCallWrapper") {
const oldMethod = handlerCallWrapper;
handlerCallWrapper = e => {
return newMethod(() => oldMethod(e), {
prevRawMethod: oldMethod,
arg: e
});
};
} else if (fnName === "pvtLogger") {
const oldMethod = pvtLogger;
pvtLogger = e => {
return newMethod(() => oldMethod(e), {
prevRawMethod: oldMethod,
arg: e
});
};
}
}
});
};
/* init configurables */
if (options && options.configure) {
configure(options.configure);
}
/**
*
* CORE - START
*
* */
/* Function Object Init "Before Hook" */
const FOInitBeforeHook = (...args) => {
if (isHandlerFed === true) {
handlerLength = handler.length;
}
if (typeof args[0] === "undefined") {
pvtLogger.logWarning(`"undefined" is probably not expected here.`);
}
if (isHandlerFed === false && Array.isArray(args[0])) {
if (args[0].every(fn => typeof fn === "function")) {
const FOArray = args[0].map(item => CreateInstance(options)(item));
FOArray.use = (...useArgs) => {
const FOArrayInner = FOArray.map(instance =>
instance.use(...useArgs)
);
FOArrayInner.use = FOArray.use;
return FOArrayInner;
};
return FOArray;
}
throw Error(
"before-hook can only be used for functions. One of the items in array is not."
);
} else if (typeof args[0] !== "function") {
if (handlerLength > -1 && handlerLength !== args.length) {
pvtLogger.logWarning(
`Dispatching with ${
args.length
} args while the original handler has ${handlerLength}.`
);
}
if (Array.isArray(args[0])) {
/* eslint-disable-next-line no-console */
console.error(`[DEPRECATED] -
This action will execute your hooked function given the array argument.
If you intended to hook an array of functions instead.
Please use beforeHook.getNew.`);
}
return FODispatch(...args);
}
if (isHandlerFed === true && args[0][SYMBOL_MIDDLEWARE_ID] !== true) {
/* then we assume this scenario calls for a new instance */
return CreateInstance(options)(args[0]);
}
/* eslint-disable-next-line */
handler = args[0];
isHandlerFed = true;
return FODispatch;
};
FOInitBeforeHook.getNew = (arrayOfFunctions) => {
if (!Array.isArray(arrayOfFunctions)) {
throw Error(`Expecting an array argument but type "${typeof arrayOfFunctions}" was passed.`);
}
if (arrayOfFunctions.every(fn => typeof fn === "function")) {
const FOArray = arrayOfFunctions.map(item => CreateInstance(options)(item));
FOArray.use = (...useArgs) => {
const FOArrayInner = FOArray.map(instance =>
instance.use(...useArgs)
);
FOArrayInner.use = FOArray.use;
return FOArrayInner;
};
return FOArray;
}
throw Error(
"before-hook can only be used for functions. One of the items in array is not."
);
}
FOInitBeforeHook.use = (...args) => {
if (!args || !args[0]) {
throw Error(
`.use expects an instance from BaseMiddleware. (Got type "${typeof args[0]}")`
);
}
if (isHandlerFed === false) {
throw Error("A handler needs to be fed first before calling .use");
}
if (args.length > 1) {
pvtLogger.logWarning(
`Ignoring 2nd argument. "use" method was called with more than 1 argument.`
);
}
if (pvtDispatched === true) {
throw Error(
"Using middlewares again after handler's invocation is not allowed."
);
}
const middleware = args[0];
if (middleware[SYMBOL_MIDDLEWARE_ID] === true) {
stackedHooks.push(middleware);
} else {
throw Error(
"Unknown middleware. Middlewares must extend BaseMiddleware."
);
}
return FOInitBeforeHook;
};
FOInitBeforeHook.setLogger = newLogger => {
pvtLogger = newLogger;
};
FOInitBeforeHook.getLogger = () => pvtLogger;
FODispatch = async (...args) => {
let hookBeforeCatching = {};
pvtDispatched = true;
if (Array.isArray(settings.register) && settings.register.length) {
// TODO: support loading async plugins
settings.register.forEach(pluginFn => {
pluginFn({
getParams: () => args,
addTree: setStateTree
});
});
}
try {
/* eslint-disable-next-line no-restricted-syntax */
for (const hook of stackedHooks) {
hookBeforeCatching = hook;
/* eslint-disable-next-line no-await-in-loop */
await hook({ getStateTree }, ...args);
// const extensions = await hook(...args);
}
} catch (middlewaresThrow) {
if (middlewaresThrow[SYMBOL_SHORT_CIRCUIT_TYPE] === "reply") {
if (settings.stopOnCatch === true) {
return onReply(JSON.parse(middlewaresThrow.message));
}
}
const catchHandlerToUse =
typeof hookBeforeCatching[SYMBOL_MIDDLEWARE_ON_CATCH] === "function"
? (err, params) =>
hookBeforeCatching[SYMBOL_MIDDLEWARE_ON_CATCH](
e => onCatch(e, params),
err,
params
)
: (err, params) => onCatch(err, params);
if (settings.stopOnCatch === true) {
return catchHandlerToUse(middlewaresThrow, {
getParams: () => args
});
}
catchHandlerToUse(middlewaresThrow, {
getParams: () => args
});
}
return handlerCallWrapper(...args);
};
/* copy properties of FOInitBeforeHook to FODispatch - so we can chain .use and etc */
Object.keys(FOInitBeforeHook).forEach(method => {
FODispatch[method] = FOInitBeforeHook[method];
});
/**
*
* CORE - END
*
* */
return FOInitBeforeHook;
};
export { BaseMiddleware, BodyParserMiddleware, CreateInstance };
export default CreateInstance;