UNPKG

alchemymvc

Version:
1,356 lines (1,106 loc) 28.8 kB
var Url = alchemy.use('url'), prefixes = alchemy.shared('Routing.prefixes'), allroutes = alchemy.shared('Routing.routes'), allsections = alchemy.shared('Routing.sections'), libpath = alchemy.use('path'), router_helper, name_count = 0, Router; /** * The Router Singleton * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.2.5 */ var RouterClass = Function.inherits('Alchemy.Base', function Router(name, mount, parent) { // Call the Informer constructor Router.super.call(this); // The name of the router this.name = name; // The mount point of this router this.mount = mount; // The parent this.parent = parent || null; // Sub sections this.subSections = {}; // Socket types this.socketTypes = {}; // Socket routes this.socketRoutes = {}; // Linkup routes this.linkupRoutes = {}; // Breadcrumb information this.breadcrumb_info = {}; // Custom options this.options = {}; // The header bypass this.bypass = null; // Routes inside this router this.routes = new Deck(); // Optional permissions to check for this.permissions = null; this.generateSectionIdentifier(); }); /** * Default route settings * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 0.3.0 */ RouterClass.setProperty('default_route_settings', { method : 'get', weight : 10 }); /** * Add a required permission * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.2.5 * @version 1.2.5 * * @param {string|string[]} permission */ RouterClass.setMethod(function requirePermission(permission) { if (!permission) { return; } if (!this.permissions) { this.permissions = []; } this.permissions.include(permission); }); /** * Check the permission * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.2.5 * @version 1.2.5 * * @param {Conduit} conduit * * @return {boolean} */ RouterClass.setMethod(function checkPermission(conduit) { // Does this section require a permission? if (this.permissions?.length) { let permission, has_permission; for (permission of this.permissions) { if (conduit.hasPermission(permission)) { has_permission = true; break; } } if (!has_permission) { return false; } } // If this section has a parent, check those permissions too if (this.parent) { return this.parent.checkPermission(conduit); } return true; }); /** * Get the complete section identifier * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.3 * @version 0.3.3 */ RouterClass.setMethod(function generateSectionIdentifier() { var identifier, title; identifier = this.name; title = this.name.titleize(); if (this.parent && this.parent.name !== 'default') { identifier = this.parent.section_identifier + ':' + identifier; title = this.parent.title + ':' + title; } this.section_identifier = identifier; this.title = title; // Register this section allsections[this.section_identifier] = this; }); /** * Get and/or create a section * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 */ RouterClass.setMethod(function section(name, mount) { // The default section is not stored in any object if (name == 'default') { return Router; } // Create the section if it doesn't exist yet if (typeof this.subSections[name] === 'undefined') { if (!mount) { mount = '/' + name; } this.subSections[name] = new RouterClass(name, mount, this); } return this.subSections[name]; }); /** * Capture vhost requests in the header with the given prefix * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 */ RouterClass.setMethod(function headerBypass(prefix) { this.bypass = prefix; }); /** * Get the full mount path of this router * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 0.3.0 */ RouterClass.setMethod(function getFullMount() { var result = this.mount, parent_mount; if (this.parent) { parent_mount = this.parent.getFullMount(); if (parent_mount && parent_mount.length > 1) { result = parent_mount + result; } } return result; }); /** * Get the prefix (locale) * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 */ RouterClass.setMethod(function getPrefix(path) { var prefix, begin; // See if the path starts with any set prefix for (key in prefixes) { begin = '/' + key + '/'; if (path.indexOf(begin) === 0) { return { prefix: key, path: path.slice(begin.length-1) }; } } }); /** * Get the section of the path * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.0.0 * * @param {string} path The path without prefix * * @return {Router} */ RouterClass.setMethod(function getPathSection(path) { var name, temp; if (typeof path != 'string') { throw new Error('Router#getPathSection requires `path` argument to be a string'); } // First do the children (because the root section is just "/") for (name in this.subSections) { temp = this.subSections[name].getPathSection(path); if (temp) { return temp; } } if (path.indexOf(this.mount) === 0) { return this; } }); /** * Get a route by breadcrumb pattern * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 0.3.0 * * @param {string} breadcrumb_pattern * * @return {Alchemy.Route} */ RouterClass.setMethod(function getRouteByBreadcrumb(breadcrumb_pattern) { var routes, route, key, i; routes = this.routes.getSorted(); for (i = 0; i < routes.length; i++) { route = routes[i]; if (route.breadcrumb == breadcrumb_pattern) { return route; } } // If it hasn't been found yet, search through the subsections for (key in this.subSections) { route = this.subSections[key].getRouteByBreadcrumb(breadcrumb_pattern); if (route) { return route; } } }); /** * Get breadcrumb info by pattern * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 0.3.0 * * @param {string} breadcrumb_pattern * * @return {Object} */ RouterClass.setMethod(function lookupBreadcrumbPath(breadcrumb_pattern) { var result, key; result = Object.path(this.breadcrumb_info, breadcrumb_pattern); if (result) { return result; } // If it hasn't been found yet, search through the subsections for (key in this.subSections) { result = this.subSections[key].lookupBreadcrumbPath(breadcrumb_pattern); if (result) { return result; } } }); /** * Get a route based on information inside the request * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.1.0 * @version 1.1.0 * * @param {Conduit} conduit * @param {string} path The path without prefix (but WITH possible router mount) * @param {string} method * * @return {Object} */ RouterClass.setMethod(function getRouteByPath(conduit, path, method) { let section = this.getPathSection(path); return this.getRouteBySectionPath(conduit, method || 'get', section, path); }); /** * Get a route based on information inside the request * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.1.7 * * @param {Conduit} conduit * @param {string|Array} methods * @param {Router} section * @param {string} path The path without prefix (but WITH possible router mount) * @param {string} prefix * @param {Route} last_match If this is a rematch, skip everything before this * * @return {Object} */ RouterClass.setMethod(async function getRouteBySectionPath(conduit, methods, section, path, prefix, last_match) { let passed_last_match, method, result, route, info; let routes = section.routes.getSorted(); methods = Array.cast(methods); for (method of methods) { for (route of routes) { if (route.is_middleware) { continue; } // If we've been given a last match, and haven't seen it yet, skip if (last_match && !passed_last_match) { // If this route matches the last matched route, indicate so if (last_match == route) { passed_last_match = true; } continue; } info = route.match(conduit, method, path, prefix); if (info && info.then) { info = await info; } if (!info) { continue; } result = { route : route, definition : info.definition, parameters : info.parameters, original_parameters : info.original_parameters, parameters_array : info.parameters_array }; return result; } } }); /** * Return the route by the requested name * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.3.0 * * @param {string} name The name of the route to get * * @result {Route} The Route instance, or false */ RouterClass.setMethod(function getRouteByName(name) { var section, route, key; route = this.routes.get(name); if (route) { return route; } // If a subsection is set in the name, look there if (name.indexOf('@') > -1) { section = name.before('@'); name = name.after('@'); if (this.subSections[section]) { return this.subSections[section].getRouteByName(name); } } for (key in this.subSections) { route = this.subSections[key].getRouteByName(name); if (route) { return route; } } return false; }); /** * Get middleware routes * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.2.5 * * @param {Conduit} conduit * @param {Router} section * @param {string} path The full requested path * @param {string} prefix * * @return {Array} */ RouterClass.setMethod(async function getMiddleware(conduit, section, path, prefix) { var sectionPath, definition, result, routes, route, temp, i; result = []; // @todo: sub section middleware will happen before parent middleware! while (section) { sectionPath = path; if (section.mount && section.mount.length > 1 && sectionPath.indexOf(section.mount) == 0) { sectionPath = sectionPath.slice(section.mount.length); if (!sectionPath) { sectionPath = '/'; } } routes = section.routes.getSorted(); for (i = 0; i < routes.length; i++) { route = routes[i]; // If this route matches the main route of the conduit, // don't look for more middleware in this section if (conduit.route && conduit.route == route) { break; } if (conduit.hasRouteBeenTested(route)) { continue; } if (route.is_middleware) { temp = route.match(conduit, conduit.method, sectionPath, prefix); if (temp && temp.then) { temp = await temp; } if (temp) { result.push(route); } } else { temp = route.match(conduit, conduit.method, sectionPath, prefix); if (temp && temp.then) { temp = await temp; } // If this non-middleware route matches // don't look for more middleware in this section if (temp) { break; } } } // This child is done, go to the parent section = section.parent; } return result; }); /** * Resolve a request * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.1.3 */ RouterClass.setMethod(function resolve(req, res) { var c = new Classes.Alchemy.Conduit.Http(req, res); }); /** * Resolve a request inside an electron app * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.1.3 */ RouterClass.setMethod(function resolveElectron(request, callback) { var c = new Classes.Alchemy.Conduit.Electron(request, callback); }); /** * Add breadcrumb information * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 0.3.0 */ RouterClass.setMethod(function addBreadcrumb(pattern, options) { var entry; if (!options) { options = {}; } // Try getting the entry first entry = Object.path(this.breadcrumb_info, pattern); if (!entry) { entry = {}; Object.setPath(this.breadcrumb_info, pattern, entry); } if (!entry._options) { entry._options = {}; } if (typeof options.route == 'string') { options.route = Router.getRouteByName(options.route); } Object.assign(entry._options, options); }); /** * Add middleware * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.0.0 * * @param {string} paths Optional path * @param {Function} fnc The actual middleware function * @param {Object} options */ RouterClass.setMethod(function use(_paths, _fnc, _options) { var options, weight, route, name, paths, fnc; if (typeof _paths === 'function') { options = _fnc; fnc = _paths; paths = '/'; } else { paths = _paths; fnc = _fnc; options = _options; } if (typeof options === 'number') { weight = options; options = undefined; } if (typeof options === 'undefined') { options = {}; } if (typeof weight !== 'number') { weight = options.weight || 20; } if (options.name) { name = options.name; } else { if (fnc && fnc.name) { name = fnc.name + '-' + (name_count++); } else { name = 'middleware-' + Date.now() + '-' + Number.random(); } } options.is_middleware = true; route = new Classes.Alchemy.Route(this, paths, options); route.weight = weight; route.name = name; if (options.methods) { route.methods = options.methods.slice(0); } else { route.methods = ['head', 'get', 'post', 'put', 'delete']; } route.setHandler(fnc); this.routes.set(name, route, weight); }); /** * Serve the file of a dependency * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.1.0 * @version 1.4.0 * * @param {string} module_name The name of the dependency * @param {Object} options The options or single string */ RouterClass.setMethod(function serveDependencyFile(module_name, options) { if (typeof module_name == 'object') { options = module_name; module_name = options.module; } const module = alchemy.findModule(module_name, {require: false}); if (typeof options == 'string') { options = { file : options }; } if (!options.path) { options.path = libpath.resolve(module.module_dir, options.file); } else if (!options.file) { options.file = libpath.basename(options.path); } if (!options.alias) { options.alias = options.file; } if (options.alias[0] != '/') { options.alias = '/scripts/' + options.alias; } if (!options.module) { options.module = module_name; } this.use(options.alias, function serveFile(req, res) { // Cache this file so earlier middleware can handle this from now on alchemy.getCache('files.assets').set(req.url, {path: options.path}); req.conduit.serveFile(options.path, options?.serve_options); }); }); /** * Set a custom option * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.1.0 * @version 1.1.0 * * @param {string} key * @param {Mixed} value */ RouterClass.setMethod(function setOption(name, value) { this.options[name] = value; }); /** * Add a route * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.3.21 * * @param {Object} args * @param {string} args.name Optional route name * @param {Array} args.methods Supported HTTP methods * @param {number} args.weight Importance of this route * @param {string} args.breadcrumb String used for breadcrumb * @param {string|Function} args.handler Route handler * @param {string|Object} args.paths The paths of this route * * @return {Route} */ RouterClass.setMethod(function add(args) { var breadcrumb_options, route; // Handle old-style adds if (arguments.length > 1 || Array.isArray(args)) { return this._add.apply(this, arguments); } // Apply to the default route settings args = Object.assign({}, this.default_route_settings, args); // Ensure certain types if (typeof args.weight !== 'number') { args.weight = 10; } // Ensure the route has a name if (!args.name) { args.name = 'nameless-' + (name_count++); } // Make sure methods is an array args.methods = Array.cast(args.methods); // If no method was given, always default to a GET if (args.methods.length == 0) { args.methods.push('get'); } // Create the new route route = new Classes.Alchemy.Route(this, args.paths, args.options || args); route.weight = args.weight; route.name = args.name; route.methods = args.methods; route.setBreadcrumb(args.breadcrumb); route.sitemap = args.sitemap; route.title = args.title; route.cache = args.cache; if (args.visible_location != null) { route.visible_location = args.visible_location; } if (args.is_system_route != null) { route.is_system_route = args.is_system_route; } if (args.permission) { route.setPermission(args.permission); } if (args.can_be_postponed != null) { route.setCanBePostponed(args.can_be_postponed); } if (args.breadcrumb) { breadcrumb_options = { route : route }; if (args.name) { breadcrumb_options.name = args.name; } if (args.title) { breadcrumb_options.title = args.title; } if (args.breadcrumb_link) { breadcrumb_options.link_fnc = args.breadcrumb_link; } this.addBreadcrumb(args.breadcrumb, breadcrumb_options); } route.setHandler(args.handler || args.name); this.routes.set(args.name, route, args.weight); if (args.methods.indexOf('get') > -1) { allroutes[this.name + '::' + args.name] = '' + this.name + ' - ' + args.name; } alchemy.checkExposedRouteData(); return route; }); /** * Add a route * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.3.0 * * @param {Array} methods The methods to listen to (all) * @param {string} name The name of the path (timestamp) * @param {string|Object} paths Path to listen to * @param {Function} fnc Optional function to handle route * @param {Object} options */ RouterClass.setMethod(function _add(_methods, _name, _paths, _fnc, _options) { var options, methods, weight, route, paths, name, fnc; if (!Array.isArray(_methods)) { options = _fnc; fnc = _paths; paths = _name; name = _methods; methods = ['get', 'post', 'put', 'delete']; } else { methods = _methods; name = _name; paths = _paths; fnc = _fnc; options = _options; } if (typeof paths === 'function') { options = fnc; fnc = paths; paths = undefined; } if (typeof paths === 'undefined') { paths = name; name = 'nameless-' + (name_count++); } if (typeof fnc === 'object' && fnc) { options = fnc; fnc = undefined; } if (typeof options === 'undefined') { options = {}; } else if (typeof options === 'number') { weight = options; options = {}; } if (typeof weight !== 'number') { weight = 10; } return this.add({ name : name, paths : paths, methods : methods, handler : fnc, options : options, weight : weight }); }); /** * Add a get route * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 * * @param {string} name The name of the path (timestamp) * @param {string|Object} paths Path to listen to * @param {Function} fnc Optional function to handle route * @param {Object} options */ RouterClass.setMethod(function get(name, paths, fnc, options) { return this._add(['get'], name, paths, fnc, options); }); /** * Add a post route * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 * * @param {string} name The name of the path (timestamp) * @param {string|Object} paths Path to listen to * @param {Function} fnc Optional function to handle route * @param {Object} options */ RouterClass.setMethod(function post(name, paths, fnc, options) { return this._add(['post'], name, paths, fnc, options); }); /** * Add a put route * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 * * @param {string} name The name of the path (timestamp) * @param {string|Object} paths Path to listen to * @param {Function} fnc Optional function to handle route * @param {Object} options */ RouterClass.setMethod(function put(name, paths, fnc, options) { return this._add(['put'], name, paths, fnc, options); }); /** * Add a socket route * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 * * @param {string} name The name of the socket event * @param {Function} action The action, as a string or function */ RouterClass.setMethod(function socket(name, action) { var route; // Add this socket event as a route route = this.add(['socket'], name, null, action); // Save it in the socket routes object too this.socketRoutes[name] = route; return route; }); /** * Add a socket linkup route * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 * * @param {string} name The name of the path (timestamp) * @param {string} type The message type (name) * @param {Function} fnc Optional function to handle route */ RouterClass.setMethod(function linkup(name, eventname, fnc) { this.linkupRoutes[eventname] = fnc; }); /** * Add a delete route * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 * * @param {string} name The name of the path (timestamp) * @param {string|Object} paths Path to listen to * @param {Function} fnc Optional function to handle route * @param {Object} options */ RouterClass.setMethod('delete', function _delete(name, paths, fnc, options) { return this.add(['delete'], name, paths, fnc, options); }); /** * Get an object of all the routes in this router and its children * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 */ RouterClass.setMethod(function getFullMount() { var result = this.mount; if (this.parent != null && this.parent.mount != '/') { result = this.parent.mount + result; } if (result[result.length-1] == '/') { result = result.slice(0, -1); } return result; }); /** * Get the full route object, for internal use * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.5.0 * @version 0.5.0 * * @param {Object} result Optional object to store sectioned results in * * @return {Object} The `result` object */ RouterClass.setMethod(function getFullRoutes(result) { var section, routes, prefix, route, mount, temp, key, i; section = {}; routes = this.routes.getSorted(false); mount = this.getFullMount(); for (i = 0; i < routes.length; i++) { route = routes[i]; if (route.is_middleware) { continue; } temp = {}; for (prefix in route.paths) { temp[prefix] = mount + route.paths[prefix].source; } section[route.name] = route; } if (result == null) { result = {}; } // Get the routes of all the sub sections for (key in this.subSections) { this.subSections[key].getFullRoutes(result); } if (Array.isArray(result)) { let key; for (key in section) { result.push(section[key]); } } else { result[this.name] = section; } return result; }); /** * Get an object of all the options in this router and its children * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.1.0 * @version 1.1.0 * * @param {Object} result Optional object to store sectioned results in * * @return {Object} The `result` object */ RouterClass.setMethod(function getOptions(result) { var section = {}, key; if (result == null) { result = {}; } Object.assign(section, this.options); result[this.name] = section; // Get the routes of all the sub sections for (key in this.subSections) { this.subSections[key].getOptions(result); } return result; }); /** * Get an object of all the routes in this router and its children * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.3.21 * * @param {Object} result Optional object to store sectioned results in * * @return {Object} The `result` object */ RouterClass.setMethod(function getRoutes(result) { var section, routes, prefix, route, mount, temp, key, i; section = {}; routes = this.routes.getSorted(false); mount = this.getFullMount(); for (i = 0; i < routes.length; i++) { route = routes[i]; if (route.is_middleware) { continue; } temp = {}; for (prefix in route.paths) { temp[prefix] = mount + route.paths[prefix].source; } section[route.name] = { name : route.name, paths : temp, prefer : route.options.prefer, section : this.name, breadcrumb : route.breadcrumb, controller : route.controller, action : route.action, keys : route.keys, has_breadcrumb_assignments : route.has_breadcrumb_assignments || undefined, methods : route.methods, permission : route.permission || undefined, has_permission_assignments : route.has_permission_assignments || undefined, title : route.title, visible_location : route.visible_location, is_system_route : route.is_system_route, schema : route.schema, param_definitions : route.param_definitions, cache : route.cache, }; } if (result == null) { result = {}; } // Get the routes of all the sub sections for (key in this.subSections) { this.subSections[key].getRoutes(result); } if (Array.isArray(result)) { let key; for (key in section) { result.push(section[key]); } } else { result[this.name] = section; } return result; }); /** * Get an object of all the socket routes in this router and its children * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 * * @param {Object} result Optional object to store sectioned results in * * @return {Object} The `result` object */ RouterClass.setMethod(function getSocketRoutes(result) { var section, routes, prefix, route, mount, temp, key, i; section = {}; routes = this.socketRoutes; // Store all the routes of this section for (key in routes) { section[key] = key; } // Create a new result object if it does not exist yet if (result == null) { result = {}; } // Get the routes of all the sub sections for (key in this.subSections) { this.subSections[key].getSocketRoutes(result); } // Store the result of this section under its name result[this.name] = section; return result; }); /** * Get an object of all the breadcrumbs * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 0.3.0 * * @param {Object} result Optional object to store sectioned results in * * @return {Object} The `result` object */ RouterClass.setMethod(function getBreadcrumbInfo(result) { var key; if (result == null) { result = {}; } // Get the routes of all the sub sections for (key in this.subSections) { this.subSections[key].getBreadcrumbInfo(result); } result[this.name] = this.breadcrumb_info; return result; }); /** * Create a route url outside of a view * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.4.1 * @version 1.0.0 * * @param {string} name * @param {Object} parameters * @param {Object} options * * @return {string} */ RouterClass.setMethod(function getUrl(name, parameters, options) { if (router_helper == null) { router_helper = new Classes.Alchemy.Helper.Router(); } return router_helper.routeUrl(name, parameters, options); }); // Create the main, default Router instance Router = new RouterClass('default', '/'); // Augment it Router = Object.create(Router); // Indicate that this is actually the main router, so it behaves different Router.main = true; // Turn it into a global global.Router = Router;