mechanic-plumber
Version:
For hapi based projects, connect routes & controller
495 lines (394 loc) • 14.9 kB
JavaScript
/**
*
* Created by uur on 25/06/14.
*/
;
const Fs = require("fs");
const Path = require("path");
const _ = require("lodash");
const Callsite = require("callsite");
const Clc = require("cli-color");
const Payload = require("./payload");
const BlueprintRoutes = require("./blueprint_routes");
module.exports = {
pipe: _pipe,
pipeModels: _pipeModels,
pipeServices: _pipeServices,
pipeHooks: _pipeHooks
};
/**
* Pipe hooks directory into mechanic.hooks
* object for further usage.
* Each file exports a single object {availableAfter, fn(request, reply), assign}
* @returns {[]}
* @private
*/
function _pipeHooks(hookFilePath) {
hookFilePath = hookFilePath || "hooks/hooks.js";
const stack = Callsite();
const requester = stack[1].getFileName();
const mainHookFilePath = Path.join(Path.dirname(requester), hookFilePath);
let hooks = [];
try {
const stat = Fs.statSync(mainHookFilePath);
if (stat.isFile()) {
hooks = require(mainHookFilePath);
}
}
catch (e) {
console.log(Clc.red.bold(e));
}
if (hooks.length == 0) {
console.log(Clc.red.bold("Upps. There are not any HOOKS, I guess you're busy with something else :( kib bb"));
}
return hooks;
}
/**
* Pipe Service directory into mechanic.service
* object for further usage.
* Each file exports a single function
* @returns {{}}
* @private
*/
function _pipeServices(serviceDirectory) {
serviceDirectory = serviceDirectory || "services";
const services = {};
const stack = Callsite();
const requester = stack[1].getFileName();
const mainServiceFolder = Path.join(Path.dirname(requester), serviceDirectory);
try {
const stat = Fs.statSync(mainServiceFolder);
if (!stat.isDirectory()) {
return services;
}
}
catch (e) {
console.log(Clc.red.bold("Upps. There are not any SERVICES, I guess you're busy with something else :( kib bb"));
return {};
}
const serviceItems = _readDirectoryRecursively(mainServiceFolder, _filterJavascriptFile);
for (let serviceItem of serviceItems) {
if (!serviceItem.path) {
continue;
}
// Nested services!
const relativePath = Path.relative(mainServiceFolder, serviceItem.path);
let splittedPath = relativePath.split(Path.sep);
let lastServices = services;
for (let i = 0; i < splittedPath.length; i++) {
if (i == splittedPath.length - 1) {
// !file name
lastServices[_trimExtFromFileName(serviceItem.name)] = require(serviceItem.path);
break;
}
lastServices[splittedPath[i]] = lastServices[splittedPath[i]] || {};
lastServices = lastServices[splittedPath[i]];
}
}
return services;
}
/**
*
* @returns {{}}
* @private
*/
function _pipeModels() {
const models = {};
const stack = Callsite();
const requester = stack[1].getFileName();
const mainPluginFolder = Path.join(Path.dirname(requester), "plugins");
const pluginFolders = Fs.readdirSync(mainPluginFolder);
pluginFolders.forEach(function (pluginFolder) {
pluginFolder = Path.join(mainPluginFolder, pluginFolder);
if (Fs.existsSync(pluginFolder)) {
const stat = Fs.statSync(pluginFolder);
if (stat.isDirectory()) {
const modelFolder = Path.join(pluginFolder, "model");
if (Fs.existsSync(modelFolder)) {
const stat2 = Fs.statSync(modelFolder);
if (stat2.isDirectory()) {
_.extend(models, _pipeModelsInFolder(modelFolder));
}
}
}
}
});
return models;
}
/**
* Promise for returning all model files.
* @param {string} folder
* @return {object}
*/
function _pipeModelsInFolder(folder) {
const pluginModels = {};
const modelFsItems = _readDirectoryRecursively(folder, _filterJavascriptFile);
modelFsItems.forEach(function (modelFsItem) {
const model = require(modelFsItem.path);
if (!model.modelName) {
throw new Error(`There is a file which is not model in "models" folder: ${modelFsItem.path}`);
}
pluginModels[_getModelName(modelFsItem.name)] = require(modelFsItem.path);
});
return pluginModels;
}
/**
* Get model name from filename
* @param fileName
* @return {string} modelName
*/
function _getModelName(fileName) {
let filename = _trimExtFromFileName(fileName).toLocaleLowerCase();
const frags = filename.split("_");
for (let i = 0; i < frags.length; i++) {
frags[i] = frags[i].charAt(0).toUpperCase() + frags[i].slice(1);
}
filename = frags.join("");
return filename.charAt(0).toLocaleUpperCase() + filename.slice(1);
}
/**
* Anything ends with *.js
* @param fsItem
* @return {boolean}
*/
function _filterJavascriptFile(fsItem) {
return /(.*)\.js$/.test(fsItem.name) && !fsItem.stat.isDirectory() && fsItem.name[0] !== "_";
}
function _pipe(plugin, preHooks) {
if (!plugin) {
console.log(Clc.red.bold("Pipe's first and only argument must be plugin itself"));
process.exit(1);
}
if (!preHooks || preHooks.length == 0) {
console.log(Clc.red.bold("Are you sure ? You do not pass any pre hook to this plugin."));
}
const stack = Callsite();
const requester = stack[1].getFileName();
const pluginOptions = {
plugin: plugin,
name: _trimExtFromFileName(Path.basename(requester)),
mainPath: Path.dirname(requester),
mainFile: Path.basename(requester),
preHooks: preHooks,
routes: [],
hapiRoutes: [],
controllers: {},
controllerDirectory: null
};
// Plugin Routes
const pluginRouteFile = Path.join(pluginOptions.mainPath, _trimExtFromFileName(pluginOptions.mainFile) + "_routes.js");
try {
const supposedRouteFile = Fs.statSync(pluginRouteFile);
if (supposedRouteFile.isFile()) {
pluginOptions.routes = require(pluginRouteFile);
}
}
catch (e) {
console.log(e);
}
// Plugin ControllerPath
const pluginControllerDirectory = Path.join(pluginOptions.mainPath, "controller");
try {
const supposedControllers = Fs.statSync(pluginControllerDirectory);
if (supposedControllers.isDirectory()) {
pluginOptions.controllerDirectory = pluginControllerDirectory;
}
}
catch (e) {
console.log(e);
}
_pipeControllers(pluginOptions);
}
/**
* Pipe Controller files.
* @private
*/
function _pipeControllers(pluginOptions) {
if (!pluginOptions.controllerDirectory) {
return;
}
// Recursively read controllerDirectory
const fsItems = _readDirectoryRecursively(pluginOptions.controllerDirectory, _filterControllerFile);
fsItems.forEach((fsItem) => {
pluginOptions.controllers[_trimExtFromFileName(fsItem.name)] = require(fsItem.path);
});
_pipeRoutesJsIntoRoutes(pluginOptions);
_pipeAllIntoHapi(pluginOptions);
}
/**
* Pipe routes defined in routes.js into hapiRoutes
* @param pluginOptions
* @private
*/
function _pipeRoutesJsIntoRoutes(pluginOptions) {
if (!pluginOptions.routes || pluginOptions.routes.length == 0) {
return;
}
let controllerName;
let controllerAction;
let relatedController;
let relatedAction;
pluginOptions.routes.forEach((route) => {
if (pluginOptions.hapiRoutes[route.path] && pluginOptions.hapiRoutes[route.path]["method"] === route.method) {
throw new Error(`URL path ( ${route.path}|${pluginOptions.hapiRoutes[route.path]["method"]} defined more than one in routes.js`);
}
const config = route.config;
if ((typeof config == "string" || config instanceof String) && config.indexOf("@") > 0) {
// config just contain handler.
controllerName = config.split("@")[0].trim();
controllerAction = config.split("@")[1].trim();
relatedController = pluginOptions.controllers[controllerName];
if (!relatedController) {
throw new Error("Controller : ( " + controllerName + ".js ) not found");
}
relatedAction = relatedController[controllerAction];
//// Plug & Play with /
if (typeof relatedAction == "function") {
relatedAction = {
handler: relatedController[controllerAction]
};
}
if (!relatedAction) {
throw new Error("Controller action -> " + controllerAction + " : ( " + controllerAction + "@" + controllerName + " ) defined in routes.js not found in " + controllerName);
}
// Controller and Action found added!
pluginOptions.hapiRoutes[route.path + "@" + route.method] = {
method: route.method,
path: route.path || "GET",
config: relatedAction
};
}
else { // default blueprint if not contain `@`
controllerName = config.trim();
relatedController = pluginOptions.controllers[controllerName];
if (!relatedController) {
throw new Error("Controller : ( " + controllerName + ".js ) not found defined in routes.js");
}
Object.keys(BlueprintRoutes).forEach((blueprintRoute) => {
const idParameter = controllerName.substr(0, controllerName.indexOf("_controller")).toLocaleLowerCase();
const path = route.path + BlueprintRoutes[blueprintRoute].path.replace("{id}", "{" + idParameter + "id}");
const method = BlueprintRoutes[blueprintRoute].method;
if (route.except && route.except.indexOf(blueprintRoute) >= 0) {
;//do nothing
}
else if (!relatedController[blueprintRoute] && ((blueprintRoute === "sorting") || (blueprintRoute === "sorted") || (blueprintRoute === "delete-info") || (blueprintRoute === "update-info"))) {
;//do nothing
}
else if (!relatedController[blueprintRoute]) {
console.log("Controller : ( " + controllerName + ".js ) defined as a blueprint in routes.js but " + blueprintRoute + " not found in controller file");
}
else {
//// Plug & Play with /
if (typeof relatedController[blueprintRoute] == "function") {
relatedController[blueprintRoute] = {
handler: relatedController[blueprintRoute]
};
}
if (!relatedController[blueprintRoute].payload && Payload(blueprintRoute)) {
relatedController[blueprintRoute].payload = Payload(blueprintRoute);
}
if (!relatedController[blueprintRoute].payload && Payload(blueprintRoute)) {
relatedController[blueprintRoute].payload = Payload(blueprintRoute);
}
// Controller and Action found added!
pluginOptions.hapiRoutes[path + "@" + method] = {
method: method,
path: path,
config: relatedController[blueprintRoute]
};
}
});
}
});
}
/**
* *.js -> *
* @param fileName
* @return {string}
* @private
*/
function _trimExtFromFileName(fileName) {
return fileName && fileName.split(".")[0];
}
/**
* Anything ends with *Controller.js under "controller" folder.
* @param fsItem
* @return {boolean}
* @private
*/
function _filterControllerFile(fsItem) {
return /(.*)_controller.js$/.test(fsItem.name) && !fsItem.stat.isDirectory() && fsItem.name[0] !== "_";
}
/**
* Pipe all routes into plugin
* @param pluginOptions
* @private
*/
function _pipeAllIntoHapi(pluginOptions) {
const routeKeys = Object.keys(pluginOptions.hapiRoutes);
const labels = _.reduce(pluginOptions.plugin.connections, (labels, connection) => {
if (connection.settings && connection.settings.labels && connection.settings.labels.length > 0) {
return labels.concat(connection.settings.labels);
}
return labels;
}, []);
routeKeys.forEach((routeKey) => {
if (pluginOptions.preHooks) {
pluginOptions.hapiRoutes[routeKey].config.pre = [];
for (let i = pluginOptions.preHooks.length - 1; i >= 0; i--) {
const pre = pluginOptions.preHooks[i];
if (pre.plugins && pre.plugins.indexOf(pluginOptions.name) == -1) {
continue;
}
if (pre.labels && _.intersection(pre.labels, labels).length == 0) {
continue;
}
if (pre["availableAfter"] && pluginOptions.hapiRoutes[routeKey].path.indexOf(pre["availableAfter"]) == 0) {
if (!pluginOptions.hapiRoutes[routeKey].config.pre) {
pluginOptions.hapiRoutes[routeKey].config.pre = [];
}
pluginOptions.hapiRoutes[routeKey].config.pre.unshift({
method: pre.method,
assign: pre.assign
});
}
}
}
pluginOptions.plugin.route({
path: pluginOptions.hapiRoutes[routeKey].path,
method: pluginOptions.hapiRoutes[routeKey].method ? pluginOptions.hapiRoutes[routeKey].method : "GET",
config: pluginOptions.hapiRoutes[routeKey].config
});
});
}
/**
* Read files in a directory recursively
* Filter function used filter necessary components
*
* @param {string} directoryPath
* @param {function} filterFunc
* @returns {object} FsItems
* @private
*/
function _readDirectoryRecursively(directoryPath, filterFunc) {
let container = [];
const items = Fs.readdirSync(directoryPath);
items.forEach((item) => {
const path = Path.join(directoryPath, item);
const stat = Fs.statSync(path);
if (stat.isDirectory()) {
container = container.concat(_readDirectoryRecursively(path, filterFunc));
}
else {
const fsItem = {
path: path,
stat: stat,
name: item
};
if (filterFunc(fsItem)) {
container.push(fsItem);
}
}
});
return container;
}