UNPKG

jumbo-core

Version:

Modern lightweight fast enterprise level MVW framework for Node.js

411 lines 16 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const RequestException_1 = require("../exceptions/RequestException"); if (Jumbo.config.jumboDebugMode) { console.log("[DEBUG] REQUIRE: Locator"); } const $qs = require("querystring"); const $url = require("url"); const $object = require("jumbo-core/utils/object"); const ParamType = { Integer: /[0-9]+/, StringId: /[a-zA-Z_]/, Number: /[0-9]*(?:\.[0-9]+)?/ }; const Method = { POST: "POST", PUT: "PUT", GET: "GET", DELETE: "DELETE" }; const GLOBALIZATION_ENABLED = Jumbo.config.globalization.enabled; const GLOBALIZATION_DEFAULT = Jumbo.config.globalization.default || "es-US"; const GLOBALIZATION_SUPPORTED = Jumbo.config.globalization.supportedLocales || ["en-US"]; exports.DEFAULT_CONTROLLER = "Home"; exports.DEFAULT_ACTION = "index"; exports.END_DELIMITER_TRIM_REGEX = /[\/]+$/; const LOCATION_PARAM_REGEX = /\$([a-z][a-zA-Z]*)/g; const LOCATION_URL_BUILDER_PARAM_REGEX = /(\[)?(\/)?\$([a-z][a-zA-Z]*)/g; const LOCATION_LANG_VARIABLE_NAME = "globlanguage"; const LOCATION_CTRL_VARIABLE_NAME = "controller"; const LOCATION_ACTION_VARIABLE_NAME = "action"; const DEFAULT_LOCATION_NAME = "default"; exports.ActionTypes = ["action"].concat(Object.getOwnPropertyNames(Method).map(m => m.toLowerCase())); const IS_IP_REGEX = /[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/; const PORT_REMOVE_REGEX = /:[0-9]+$/; const SQUARE_BRACKET_REGEX = /[\[\]]/g; const LOCATION_ALL_SLASHES_REGEX = /\//g; let DELIMITER_REGEX = LOCATION_ALL_SLASHES_REGEX; let controllerFactory = null; const ONLY_LOCALE_REGEX = /^[a-z]{2}-[A-Z]{2}$/; const LOCALE_OPT_COUNTRY_REGEX = /[a-z]{2}(-[A-Z]{2})?/; function locationParamReplacer(varName, lang, useLang, controller, action, params, location, isRequired, delimiter = "") { if (varName == LOCATION_LANG_VARIABLE_NAME) { if (!useLang) return ""; return delimiter + lang; } if (varName == LOCATION_CTRL_VARIABLE_NAME) { if (!controller && isRequired) { throw new Error("This location require controller but you didn't pass any in parameters."); } return controller ? (delimiter + controller) : ""; } if (varName == LOCATION_ACTION_VARIABLE_NAME) { if (!action && isRequired) { throw new Error("This location require action but you don't pass any in parameters."); } return action ? (delimiter + action) : ""; } let param = params[varName]; if (param) { delete params[varName]; return param ? (delimiter + param) : ""; } if (location.options[varName]) { return location.options[varName]; } return ""; } const istanceKey = Symbol.for("Jumbo.Application.Locator"); let instance = global[istanceKey] || null; class Locator { constructor() { this.locations = new Map(); this.main = "www"; this.subDomains = []; this.host = null; this.delimiter = "/"; this.delimiterEscaped = "/"; this.urlAliases = {}; if (new.target != LocatorActivator) { throw new Error("You cannot call private constructor!"); } } static get ParamType() { return ParamType; } static get Method() { return Method; } static get defaultController() { return exports.DEFAULT_CONTROLLER; } static get defaultAction() { return exports.DEFAULT_ACTION; } static get instance() { if (instance == null) { global[istanceKey] = instance = Reflect.construct(Locator, [], LocatorActivator); setImmediate(() => { controllerFactory = Jumbo.Application.ControllerFactory.instance; }); } return instance; } static get defaultLocationName() { return DEFAULT_LOCATION_NAME; } setHost(host) { this.host = host; } setDelimiter(delimiter) { if (delimiter.length != 1) { throw new Error("Delimiter must be exactly one character."); } this.delimiter = delimiter; if (["\\", ".", "*", "?", "+", "|", "(", ")", "[", "]", "{", "}"].indexOf(delimiter) != 1) { this.delimiterEscaped = "\\" + delimiter; } exports.END_DELIMITER_TRIM_REGEX = new RegExp(this.delimiterEscaped + "+$"); DELIMITER_REGEX = new RegExp(this.delimiterEscaped, "g"); } setMainSubdomain(subName) { this.main = subName.toLowerCase(); } addSubdomain(subName) { this.subDomains.push(subName.toLowerCase()); } addLocation(locationName, location, options = {}, subApp = null) { if (typeof location != "string") { throw new Error("Locaton must be string."); } if (options !== null && options.constructor !== Object) { throw new Error("Options parameter must be Object."); } if (this.locations.has(locationName)) { throw new Error("Location with this name already exists."); } let loc = this.prepareNewLocation(location, options, subApp); loc.locationName = locationName; this.locations.set(locationName, loc); } addDefaultLocation(location) { if (typeof location != "string") { throw new Error("Locaton must be string."); } if (this.locations.has(DEFAULT_LOCATION_NAME)) { throw new Error("Default location already exists."); } let locationEntries = this.locations.entries(); this.locations = new Map(); let loc = this.prepareNewLocation(location, {}, null); loc.locationName = DEFAULT_LOCATION_NAME; this.locations.set(DEFAULT_LOCATION_NAME, loc); let key, item; for ([key, item] of locationEntries) { this.locations.set(key, item); } } generateLocationUrl(locationName, controller = null, action = null, params = {}, subApp = null, lang = null, protocol = null, host = null) { let location = this.locations.get(locationName); if (!location) { throw new Error(`Location ${locationName} doesn't exists.`); } if (params.constructor !== Object) { throw new Error("Parameter 'params' must be Object"); } if (location.options.controller) { controller = location.options.controller; } if (location.options.action) { action = location.options.action; } let notVariablesParams = Object.keys(params); if (location.variables.length !== 0) { notVariablesParams = notVariablesParams.filter(param => !location.variables.includes(param)); } if (notVariablesParams.length === 0) { if (controller == exports.DEFAULT_CONTROLLER) controller = ""; if (action == exports.DEFAULT_ACTION) action = ""; } const useLang = GLOBALIZATION_ENABLED && lang; lang = (lang || ""); let loc = location.location; const langInLoc = loc.indexOf("$" + LOCATION_LANG_VARIABLE_NAME) !== -1; let url = loc .replace(LOCATION_URL_BUILDER_PARAM_REGEX, (_, startOptBracket, delimiter, varName) => { return locationParamReplacer(varName, lang, useLang, controller, action, params, location, !startOptBracket, delimiter); }) .replace(SQUARE_BRACKET_REGEX, "") .replace(LOCATION_ALL_SLASHES_REGEX, this.delimiter); if (url.charAt(url.length - 1) == this.delimiter) { url = url.slice(0, -1); } if (Object.keys(params).length) { url += "?" + $qs.stringify(params); } let baseUrl = "/"; if (host || protocol || location.subApp) { baseUrl = (protocol || "http") + "://" + (!!subApp ? (subApp + ".") : "") + (host || this.host) + "/"; } if (!langInLoc && useLang) { baseUrl += lang + (url ? this.delimiter : ""); } return baseUrl + url; } requestLocaleOrDefault(request) { let match = (request.headers["accept-language"] || "").match(LOCALE_OPT_COUNTRY_REGEX); if (match) { if (GLOBALIZATION_SUPPORTED.includes(match[0])) { return match[0]; } let subMatch = match[0].slice(0, 2); for (let loc of GLOBALIZATION_SUPPORTED) { if (loc.slice(0, 2) === subMatch) { return loc; } } } return GLOBALIZATION_DEFAULT; } parseUrl(request) { let url = request.url.replace(DELIMITER_REGEX, "/"); let parse = $url.parse(url); url = parse.pathname.slice(1); if (parse.pathname === "/" && GLOBALIZATION_ENABLED) { let lang = this.requestLocaleOrDefault(request); return new RequestException_1.RequestException("No locale specified", 301, false, "/" + lang + (parse.query || "")); } let subApp = this.getSubAppFromRequest(request); let emptyLocation = this.emptyLocationMatch(parse, subApp, request); if (emptyLocation) return emptyLocation; let match; let location; [location, match] = this.findLocationForUrl(url, subApp); if (!location) return null; let matchedAction = match[location.actionIndex]; let matchedController = match[location.controllerIndex]; let res = { location: location, subApp: subApp, controller: location.targetedController ? location.controller : (matchedController || exports.DEFAULT_CONTROLLER), action: location.targetedAction ? location.action : (matchedAction || exports.DEFAULT_ACTION), params: $object.clone(location.params), locale: match[1], actionInUrl: !!matchedAction, controllerInUrl: !!matchedController }; let c = location.variables.length; for (let i = 0; i < c; i++) { let variable = location.variables[i]; if (variable != LOCATION_CTRL_VARIABLE_NAME && variable != LOCATION_ACTION_VARIABLE_NAME && variable != LOCATION_LANG_VARIABLE_NAME) { res.params[variable] = match[i + 1]; } } let queryParams = $qs.parse(parse.query); for (let qp in queryParams) { res.params[qp] = queryParams[qp]; } return res; } addUrlAlias(url, alias) { this.urlAliases[alias] = url; } getUrlForAlias(alias) { return this.urlAliases[alias]; } emptyLocationMatch(parse, subApp, request) { let localeMatch; if (GLOBALIZATION_ENABLED) { localeMatch = parse.pathname.slice(1).match(ONLY_LOCALE_REGEX); if (localeMatch) localeMatch = localeMatch[0]; } else if (parse.pathname === "/") { localeMatch = this.requestLocaleOrDefault(request); } if (localeMatch) { return { location: this.locations.get(DEFAULT_LOCATION_NAME), subApp: subApp, controller: exports.DEFAULT_CONTROLLER, action: exports.DEFAULT_ACTION, params: $qs.parse(parse.query), locale: localeMatch, actionInUrl: false, controllerInUrl: false }; } return null; } extractSubApp(request) { let host = request.headers.host.replace(PORT_REMOVE_REGEX, ""); if ((IS_IP_REGEX).test(host)) { return this.main; } let s = host.split("."); if (s.length < 3) { return this.main; } return (s[0] || this.main).toLowerCase(); } findLocationForUrl(url, subApp) { let match; for (let [_, location] of this.locations) { match = url.match(location.locationMatcher); if (match !== null && ((subApp == null && location.subApp == this.main) || subApp == location.subApp || location.subApp === null)) { return [location, match]; } } return [null, null]; } getSubAppFromRequest(request) { let subApp = this.extractSubApp(request); if (subApp === this.main) { return subApp; } for (let sub of this.subDomains) { if (sub == subApp) { return subApp; } } throw new Error(`Subdomain '${subApp}' is not regitered!`); } createLocationMatcher(location, loc, options) { if (GLOBALIZATION_ENABLED && location.indexOf("$" + LOCATION_LANG_VARIABLE_NAME) === -1) { location = "[$" + LOCATION_LANG_VARIABLE_NAME + "[/]]" + location; } location = location .replace(/\[/g, "(?:") .replace(/]/g, ")?"); loc.locationMatcher = new RegExp("^" + location.replace(LOCATION_PARAM_REGEX, (_, varName) => { loc.variables.push(varName); if (varName == LOCATION_LANG_VARIABLE_NAME) { return "([a-z]{2}-[a-zA-Z]{2})"; } if (varName == LOCATION_CTRL_VARIABLE_NAME) { return "([a-zA-Z]{3,})"; } if (varName == LOCATION_ACTION_VARIABLE_NAME) { return "([a-zA-Z]{2,})"; } varName = "$" + varName; if (options[varName]) { if (options[varName].constructor != RegExp) { throw new Error(`Location parameter '$${varName}' must be regular expression.`); } return "(" + options[varName].toString().slice(1, -1) + ")"; } return "([a-zA-Z0-9-_]+)"; }) + "$"); loc.actionIndex = loc.variables.indexOf(LOCATION_ACTION_VARIABLE_NAME) + 1; loc.controllerIndex = loc.variables.indexOf(LOCATION_CTRL_VARIABLE_NAME) + 1; } prepareNewLocation(location, options, subApp) { let loc = { locationName: "", location: location, locationMatcher: null, controller: null, action: null, params: options.params || {}, targetedController: false, targetedAction: false, options: options, subApp: subApp, variables: [], controllerIndex: null, actionIndex: null }; if (options.controller) { loc.controller = options.controller; loc.targetedController = true; } else { if (location.indexOf("$controller") === -1) { throw new Error("No controller specified in this location."); } } if (options.action) { loc.action = options.action; loc.targetedAction = true; } else { if (!location.match(/\$action/)) { throw new Error("No action specified in this location."); } } this.createLocationMatcher(location, loc, options); return loc; } } exports.Locator = Locator; class LocatorActivator extends Locator { } if (Jumbo.config.jumboDebugMode) { console.log("[DEBUG] REQUIRE: Locator END"); } //# sourceMappingURL=Locator.js.map