UNPKG

scvo-router

Version:

Pipes to open Elasticsearch to everything we do

330 lines (285 loc) 13.3 kB
// Module imports import { Client, SearchResponse, MSearchResponse, ConfigOptions } from 'elasticsearch'; import * as handlebars from 'handlebars'; const hbs = require('nymag-handlebars')(); // Internal imports import { IRouteMatch, ISearchTemplate, ISearchResponseSet, ISearchQuery, IDocumentResult, IPaging, INamedPattern, INamedTemplate, IContext, IMenus, IMenuItem, IRouteLayouts, IRouteLayout, IRouteResponse } from './interfaces'; import { Route } from './route'; import { SearchTemplate, SearchTemplateSet } from './search-template'; import { MapJsonify } from './map-jsonify'; import { Helpers } from './helpers'; /** Class that handles matched routes and gets results */ export class RouteMatch implements IRouteMatch { name: string = '_default'; metaData: any = {}; pattern: string|INamedPattern = null; queryDelimiter: string = '&'; queryEquals: string = '='; primarySearchTemplate: SearchTemplate; supplimentarySearchTemplates: SearchTemplateSet = {}; primaryResponse: SearchResponse<IDocumentResult> = null; supplimentaryResponses: ISearchResponseSet = {}; elasticsearchConfig: ConfigOptions = null; defaultParams: any = {}; layouts: IRouteLayouts = null; layoutName: string = 'default'; response: IRouteResponse = { contentBody: '', contentType: 'text/html', statusCode: 200 } get defaultParamsCopy(): any { var copy = {}; Object.assign(copy, this.defaultParams); return copy; } // Used to remember which order our supplimentary queries were executed in private orderMap: string[] = []; private _primaryQuery: ISearchQuery = null; // Build our primary query get primaryQuery() { if(!this._primaryQuery){ // If we haven't already got this query we need to generate it this._primaryQuery = this.primarySearchTemplate.getPrimary(this.params); } return this._primaryQuery; } private _supplimentaryQueries: any = null; // Build all supplimentary queries extending our parameters with the primary results we already have get supplimentaryQueries(): any { if(!this._supplimentaryQueries){ // If we haven't already got these queries we need to generate them // Extend our parameters that will be used when generating supplimentary queries // by adding the primary result set var supplimentaryParams = this.params; supplimentaryParams.primaryResponse = this.primaryResponse; this._supplimentaryQueries = { body: [] }; // Loop through all supplimentary queries Object.keys(this.supplimentarySearchTemplates).forEach((key: string, order: number) => { // Elasticsearch return msearch results in order, we need to keep // track of what order our supplimentary results are expected in // to assign them back to the correctly keyed result set this.orderMap[order] = key; var searchTemplate = this.supplimentarySearchTemplates[key]; // Get both the head and body for our msearch var head = searchTemplate.getHead(); var body = searchTemplate.getBody(supplimentaryParams); // Add them to our query array this._supplimentaryQueries.body.push(head); this._supplimentaryQueries.body.push(body); }); } return this._supplimentaryQueries; } private _esClient: Client = null; private get esClient(): Client { if(!this._esClient){ var config = Object.assign({}, this.elasticsearchConfig); this._esClient = new Client(config); } return this._esClient; } public get paging(): IPaging { var from = this.primaryQuery.body.from || 0; var size = this.primaryQuery.body.size || 10; var sort = this.primaryQuery.body.sort || null; var totalResults = this.primaryResponse.hits.total || 0; var totalPages = Math.ceil(totalResults / size); var currentPage = Math.floor(from / size) + 1; var nextPage = currentPage < totalPages ? Math.floor(currentPage + 1) : null; var prevPage = currentPage > 1 ? Math.floor(currentPage - 1) : null; // Setup an array (range) of 10 numbers surrounding our current page var pageRange = Array.from(new Array(9).keys(), (p, i) => i + (currentPage - 4)); // Move range forward until none of the numbers are less than 1 var rangeMin = pageRange[0]; var positiveShift = rangeMin < 1 ? 1 - rangeMin : 0; pageRange = pageRange.map(p => p + positiveShift); // Move range backwards until none of the numbers are greater than totalPages var rangeMax = pageRange[pageRange.length - 1]; var negativeShift = rangeMax > totalPages ? rangeMax - totalPages : 0; pageRange = pageRange.map(p => p - negativeShift); // Prune everything that appears outside of our 1 to totalPages range pageRange = pageRange.filter(p => p >= 1 && p <= totalPages); var pages = []; pageRange.forEach((page: number) => { var distance: number = Math.abs(currentPage - page); pages.push({ pageNumber: Math.floor(page), distance: distance, }); }); var paging = { from: from, size: size, sort: sort, totalResults: totalResults, totalPages: totalPages, currentPage: currentPage, nextPage: nextPage, prevPage: prevPage, pageRange: pages }; return paging; } _domainStripper: RegExp = null; get domainStripper(): RegExp { if(!this._domainStripper){ var stripDomains = this.context.domains.map((domain: string) => { return domain.replace(/\./g, '\\.'); }); var domainRegexString = '((https?)?:\\/\\/)((' + stripDomains.join(')|(') + '))'; this._domainStripper = new RegExp(domainRegexString, 'ig'); } return this._domainStripper; } public toJSON(): any { var templates = MapJsonify<ISearchTemplate>(this.supplimentarySearchTemplates); var responses = MapJsonify<SearchResponse<IDocumentResult>>(this.supplimentaryResponses); return { name: this.name, metaData: this.metaData, pattern: this.pattern, queryDelimiter: this.queryDelimiter, queryEquals: this.queryEquals, primarySearchTemplate: this.primarySearchTemplate.toJSON(), primaryQuery: this.primaryQuery, supplimentarySearchTemplates: templates, supplimentaryQueries: this.supplimentaryQueries, primaryResponse: this.primaryResponse, supplimentaryResponses: responses, elasticsearchConfig: this.elasticsearchConfig, layouts: this.layouts, response: this.response, params: this.params, paging: this.paging, defaultParams: this.defaultParams, context: null, layoutName: this.layoutName, }; } /** * Create a matched route to get results using parameters * @param {Route} route - The route that has been matched * @param {any} params - The parameters that the route recognizer has found */ constructor(route: Route, public params: any, public context: IContext){ // Implement route Object.assign(this, route); Helpers.register(handlebars); Object.keys(context.templatePartials).forEach((name: string) => { handlebars.registerPartial(name, context.templatePartials[name]); }); Object.keys(this.context.menus).forEach((name: string) => { this.context.menus[name] = this.traverseMenu(this.context.menus[name]); }); Object.keys(this.layouts).forEach((name: string) => { if (name === 'default' || this.layoutName !== 'default') return; if (this.context.layouts.hasOwnProperty(name)) { var pattern = this.context.layouts[name].pattern; var regex = new RegExp(pattern, 'ig'); if (regex.test(this.params.uri.href)) { this.layoutName = name; this.response.contentType = this.context.layouts[name].contentType; } else { } } }); } traverseMenu(menuItems: IMenuItem[], level: number = 0): IMenuItem[] { menuItems.forEach((menuItem) => { var pattern = new RegExp(menuItem.route, 'i'); menuItem.match = pattern.test(this.params.uri.path); menuItem.level = level; if (menuItem.children) { menuItem.children = this.traverseMenu(menuItem.children, level + 1); } }); return menuItems; } /** * Get primary and supplimentary results for this route match * @return {Promise<void>} A promise to tell when results have been fetched */ getResults(): Promise<void> { return new Promise<void>((resolve, reject) => { //console.log('#### ROUTE NAME:', this.name, '####'); // Perform our primary search this.esClient.search(this.primaryQuery, (err: any, primaryResponse: SearchResponse<IDocumentResult>) => { if(err) return reject(err); // Save the results for use in our rendered template this.primaryResponse = primaryResponse; if(Object.keys(this.supplimentarySearchTemplates).length > 0){ // If we have any supplimentary searches to do, do them this.esClient.msearch(this.supplimentaryQueries, (err: any, supplimentaryResponses: MSearchResponse<IDocumentResult>) => { if(err) return reject(err); // Loop through each of our supplimentary responses supplimentaryResponses.responses.map((supplimentaryResponse: SearchResponse<IDocumentResult>, i: number) => { // Find out the name/key of the associated supplimentary search var responseName = this.orderMap[i]; // Save the response to the appropriately named property of our supplimentary responses this.supplimentaryResponses[responseName] = supplimentaryResponse; }); this.renderResults(); // We're done so let the promise owner know resolve(); }); }else{ this.renderResults(); // We don't need to get anything else so let the promise owner know resolve(); } }); }); } renderResults() { try { // Compile our template var routeTemplateData: any = { name: this.name, primaryResponse: this.primaryResponse, supplimentaryResponses: this.supplimentaryResponses, params: this.params, metaData: this.metaData, paging: this.paging, context: this.context, }; var sections: IRouteLayout = {}; Object.keys(this.layouts[this.layoutName]).forEach((sectionName: string) => { var template = handlebars.compile(this.layouts[this.layoutName][sectionName]); var output = template(routeTemplateData); var doNotStripDomains = this.context.layouts[this.layoutName].doNotStripDomains; if (!doNotStripDomains) { output = output.replace(this.domainStripper, ''); } sections[sectionName] = output; }); var contextData = { metaData: this.context.metaData, domains: this.context.domains, menus: this.context.menus, routes: this.context.routes, route: this.toJSON(), uaId: this.context.uaId, templatePartials: this.context.templatePartials, }; var template = handlebars.compile(this.context.layouts[this.layoutName].template); var output = template(contextData); output = output.replace(/(<!--{section:)([a-z0-9_-]+)(}-->)/ig, (match, m1, m2, m3) => { if (sections.hasOwnProperty(m2)) { return sections[m2]; } else { return match; } }); this.response.contentBody = output; } catch (err) { console.error('Error rendering route:', err); this.response.contentBody = JSON.stringify(err); this.response.statusCode = 500; this.response.contentType = 'application/json'; } } }