UNPKG

next-plumber

Version:

For hapi based projects, connect routes & controller

490 lines (386 loc) 13.9 kB
/** * Next Plumber ! * * Controllers & Services & Models * */ "use strict"; const _ = require("lodash"); const Path = require("path"); const Fs = require("fs"); const Colors = require("colors/safe"); const Callsite = require("callsite"); const BlueprintRoutes = require("./blueprint_routes"); const Payload = require("./payload"); Colors.setTheme({ info: "green", error: "red", warn: "yellow" }); module.exports = { pipeRoutes, pipeHooks, pipeModels, pipeServices }; /** * Pipe Service directory into mechanic.service * object for further usage. * Each file exports a single function * @returns {{}} * @private */ function pipeServices(serviceDirectory) { try { const supposedServiceDirectory = Fs.statSync(serviceDirectory); if (!supposedServiceDirectory.isDirectory()) { throw new Error("Given path is not a directory"); } } catch (e) { console.log(Colors.error(e)); console.log(Colors.error("Service folder not found in the given location: " + serviceDirectory)); return {}; } const serviceFsItems = _readDirectoryRecursively(serviceDirectory, _filterJavascriptFile); const services = {}; for (let serviceFsItem of serviceFsItems) { if (!serviceFsItem.path) { continue; } // Nested services! const relativePath = Path.relative(serviceDirectory, serviceFsItem.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(serviceFsItem.name)] = require(serviceFsItem.path); break; } lastServices[splittedPath[i]] = lastServices[splittedPath[i]] || {}; lastServices = lastServices[splittedPath[i]]; } } return services; } /** * * @params {string} [searchDirectory] * Recursively search given folder for "models" folder, * and imports related content * * @return {{}} */ function pipeModels(searchDirectory) { const stack = Callsite(); const requester = stack[1].getFileName(); const calledDirectory = Path.dirname(requester); if (!searchDirectory) { searchDirectory = Path.join(calledDirectory, "plugins"); } const models = {}; return _pipeModels(searchDirectory, searchDirectory, models); } /** * * @param mainDirectory * @param searchDirectory * @param models * @returns {{}} * @private */ function _pipeModels(mainDirectory, searchDirectory, models) { const items = Fs.readdirSync(searchDirectory); let isParentModelFolder = searchDirectory.split(Path.sep).includes("models") || searchDirectory.split(Path.sep).includes("model"); for (let item of items) { const itemPath = Path.join(searchDirectory, item); const stat = Fs.statSync(itemPath); if (stat.isFile() && isParentModelFolder) { const model = require(itemPath); if (!model["modelName"]) { throw new Error(`There is a file which is not model in "models" folder: ${itemPath}`); } models[_getModelName(item)] = model; } if (stat.isDirectory()) { _pipeModels(mainDirectory, itemPath, models); } } return models; } /** * Pipe hooks directory into mechanic.hooks * object for further usage. * Each file exports a single object {availableAfter, method(request, reply), assign} * @params hookFile * @returns {[]} */ function pipeHooks(hookFile) { let hooks = []; try { const stat = Fs.statSync(hookFile); if (!stat.isFile()) { throw new Error("Given path is not a file"); } hooks = require(hookFile); } catch (e) { console.log(Colors.error(e)); console.log(Colors.error("Hooks file not found in the given location: " + hookFile)); } if (hooks.length == 0) { console.log(Colors.error("Upps. There are not any HOOKS, I guess you're busy with something else :( kib bb")); } return hooks; } /** * @param server * @param hooks * @param {string} routeFile * @param {string} [controllerDirectory] */ function pipeRoutes(server, hooks, routeFile, controllerDirectory) { const plumber = {}; const stack = Callsite(); const requester = stack[1].getFileName(); const calledDirectory = Path.dirname(requester); if (!controllerDirectory) { controllerDirectory = Path.join(calledDirectory, "controller"); } routeFile = Path.join(calledDirectory, routeFile); try { const supposedRouteFile = Fs.statSync(routeFile); if (!supposedRouteFile.isFile()) { throw new Error("Given path is not a file"); } plumber.routeConfigs = require(routeFile); } catch (e) { console.log(Colors.error(e)); console.log(Colors.error("Route file not found in the given location: " + routeFile)); return; } if (!plumber.routeConfigs || plumber.routeConfigs.length == 0) { console.log(Colors.info("are u sure bro? there is not any routes here.")); } try { const supposedControllers = Fs.statSync(controllerDirectory); if (!supposedControllers.isDirectory()) { throw new Error("Given path is not a directory"); } plumber.controllerDirectory = controllerDirectory; } catch (e) { console.log(Colors.error(e)); console.log(Colors.error("Controller directory not found in the given location: " + controllerDirectory)); return; } _pipeControllers(server, hooks, plumber); } /** * * @param server * @param hooks * @param plumber * @private */ function _pipeControllers(server, hooks, plumber) { // Recursively read controllerDirectory const controllerFiles = _readDirectoryRecursively(plumber.controllerDirectory, _filterControllerFile); if (!controllerFiles || controllerFiles.length == 0) { console.log(Colors.info("are u sure bro? there is not any controller file here.")); } plumber.controllers = {}; plumber.routes = {}; for (let controllerFile of controllerFiles) { plumber.controllers[_trimExtFromFileName(controllerFile.name)] = require(controllerFile.path); } plumber = _getRoutes(plumber); _pipeRoutes(server, hooks, plumber); } /** * @param server * @param hooks * @param plumber * @private */ function _pipeRoutes(server, hooks, plumber) { const labels = _.reduce(server.connections, (labels, connection) => { if (connection.settings && connection.settings.labels && connection.settings.labels.length > 0) { return labels.concat(connection.settings.labels); } return labels; }, []); const routeKeys = Object.keys(plumber.routes); for (let routeKey of routeKeys) { if (hooks && hooks.length > 0) { // hooks exist, but is it really belongs to this route? plumber.routes[routeKey].config.pre = []; for (let i = hooks.length - 1; i >= 0; i--) { const hook = hooks[i]; if (hook["labels"] && _.intersection(hook["labels"], labels).length == 0) { continue; } if (hook["availableAfter"] && plumber.routes[routeKey].path.indexOf(hook["availableAfter"]) == 0) { plumber.routes[routeKey].config.pre.unshift({ method: hook.method, assign: hook.assign }); } } } server.route({ path: plumber.routes[routeKey].path, method: plumber.routes[routeKey].method, config: plumber.routes[routeKey].config }); } } /** * @param plumber * @return plumber * @private */ function _getRoutes(plumber) { for (let routeConfig of plumber.routeConfigs) { if (plumber.routes[routeConfig.path] && plumber.routes[routeConfig.path]["method"] === routeConfig.method) { throw new Error(`URL path ( ${routeConfig.path}|${plumber.routes[routeConfig.path]["method"]} defined more than one in route file`); } const action = routeConfig.action || routeConfig.config; if (!action) { throw new Error(`Invalid route ${routeConfig}`); } if (action.indexOf("@") != -1) { // Single route const controllerName = action.split("@")[0].trim(); const controllerAction = action.split("@")[1].trim(); const relatedController = plumber.controllers[controllerName]; if (!relatedController) { throw new Error(`Controller : ( ${controllerName} ) not found`); } const relatedAction = relatedController[controllerAction]; if (!relatedAction) { throw new Error(`Controller action -> ${controllerAction} : ( ${controllerAction}"@"${controllerName} ) defined in routes not found in ${controllerName}`); } // Controller and Action found added! plumber.routes[routeConfig.path + "@" + (routeConfig.method || "get")] = { method: routeConfig.method || "get", path: routeConfig.path, config: typeof relatedAction == "function" ? {handler: relatedAction} : relatedAction }; } else { const controllerName = action.trim(); // Blueprint route const relatedController = plumber.controllers[controllerName]; if (!relatedController) { throw new Error(`Controller : ( ${controllerName} ) not found`); } const blueprintRoutes = Object.keys(BlueprintRoutes); const except = routeConfig.except; for (let blueprintRoute of blueprintRoutes) { if (except && except.indexOf(blueprintRoute) != -1) { // Except these! yey! continue; } if (!relatedController[blueprintRoute] && (blueprintRoute == "sorting" || blueprintRoute == "sorted")) { // Sorting and Sorted may not be implemented. continue; } if (!relatedController[blueprintRoute]) { console.log(Colors.error(`Controller: ( ${controllerName} ) defined as a blueprint in routes but "${blueprintRoute}" not found in controller file`)); continue; } const parameterName = controllerName.substr(0, controllerName.indexOf("_controller")).toLocaleLowerCase(); const path = routeConfig.path + BlueprintRoutes[blueprintRoute].path.replace("{id}", "{" + parameterName + "id}"); const method = BlueprintRoutes[blueprintRoute].method; 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! plumber.routes[path + "@" + method] = { method: method, path: path, config: relatedController[blueprintRoute] }; } } } return plumber; } /** * *.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] !== "_"; } /** * Anything ends with *.js * @param fsItem * @return {boolean} */ function _filterJavascriptFile(fsItem) { return /(.*)\.js$/.test(fsItem.name) && !fsItem.stat.isDirectory() && fsItem.name[0] !== "_"; } /** * Get model name from filename * @param fileName * @return {string} modelName */ function _getModelName(fileName) { const filename = _trimExtFromFileName(fileName).toLocaleLowerCase(); return _.reduce(filename.split("_"), (str, fragment) => { return str + _.capitalize(fragment); }, ""); } /** * 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); for (let item of items) { 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; }