"use strict"; const Promise = require("bluebird"); const pathToRegexp = require("path-to-regexp"); const defaultValue = require("default-value"); const url = require("url"); const objectToFormData = require("../util/form-data/object-to-formdata"); const objectToURLSearchParams = require("../util/form-data/object-to-urlsearchparams"); module.exports = function createRouter(options = {}) { let baseUrl = defaultValue(options.baseBackendUrl, ""); let router = { _routes: [], _getRoute: function getRoute(method, path) { let matches; let matchingRoute = router._routes.find((route) => route.method === method && (matches = route.regex.exec(path))); if (matchingRoute == null) { throw new Error(`No matching routes found for ${method.toUpperCase()} ${path}`); } else { let params = {}; matchingRoute.keys.forEach((key, i) => { params[key] = matches[i + 1]; }); return { handler: matchingRoute.handler, params: params }; } }, get: function (...args) { return this.addRoute("get", ...args); }, post: function (...args) { return this.addRoute("post", ...args); }, put: function (...args) { return this.addRoute("put", ...args); }, delete: function (...args) { return this.addRoute("delete", ...args); }, head: function (...args) { return this.addRoute("head", ...args); }, patch: function (...args) { return this.addRoute("patch", ...args); }, addRoute: function addRoute(method, path, handler) { /* Mutable arguments? WTF. */ let keys = []; let regex = pathToRegexp(path, keys); router._routes.push({ method, path, regex, keys, handler }); }, handle: function handleRequest(method, uri, data, handleOptions = {}) { return Promise.try(() => { /* TODO: Support relative paths? */ let {pathname, query} = url.parse(uri, true); let route = this._getRoute(method, pathname); let tasks = []; let renderTaskAdded = false; let req = { path: pathname, query: query, body: data, params: route.params, pass: function(options = {}) { return Promise.try(() => { let body; if (handleOptions.multipart) { body = objectToFormData(this.body); } else { body = objectToURLSearchParams(this.body); } return window.fetch(url.resolve(baseUrl, uri), Object.assign({ // FIXME: Override URI but maintain query? method: method, credentials: "same-origin", /* FIXME: Allow sending them elsewhere as well? */ body: body }, options)); }).then((response) => { if (!response.ok) { /* TODO: Is this what we want? */ throw new Error(`Got a non-200 response: ${response.status}`, {response: response}); } else { return Promise.try(() => { return response.json(); }).then((json) => { return { status: response.status, body: json }; }); } }); }, /* TODO: passActions? */ passRender: function(viewName, options = {}) { return Promise.try(() => { return this.pass(options.requestOptions); }).then((response) => { let locals = defaultValue(options.locals, {}); let combinedLocals = Object.assign({}, locals, response.body); res.render(viewName, combinedLocals, options.renderOptions); }); } }; let res = { render: function(view, locals = {}, options = {}) { if (renderTaskAdded === false) { renderTaskAdded = true; tasks.push({ type: "render", view, locals, options }); } else { throw new Error("Can only render a view once per response"); } }, redirect: function(path, redirectMethod) { tasks.push({ type: "redirect", path: path, method: defaultValue(redirectMethod, method) }); }, open: function(path, options = {}) { tasks.push({ type: "open", path, options }); }, close: function(options = {}) { tasks.push({ type: "close", options }); }, notify: function(message, options = {}) { tasks.push({ type: "notify", message: message, options: { type: defaultValue(options.type, "info"), timeout: options.timeout, title: options.title } }); }, error: function(error, context = {}) { tasks.push({ type: "error", error, context }); } }; return Promise.try(() => { return route.handler(req, res); }).then((result) => { return { result: result, actions: tasks }; }); }); } }; return router; };