@scvo/router-task-elasticsearch
Version:
SCVO=>Router task for Elasticsearch
369 lines (319 loc) • 10.8 kB
text/typescript
/* tslint:disable:no-any */
import {Client, ConfigOptions, MSearchParams, MSearchResponse, SearchResponse} from 'elasticsearch';
const hbs = require('clayhandlebars')();
import {RouterTask, RouteMatch, Helpers, RouteTaskError, RouteTaskConfiguration} from '@scvo/router';
export class ElasticsearchRouterTask extends RouterTask {
name = 'elasticsearch';
constructor(handlebarsHelpers: HandlebarsHelpers) {
super();
Helpers.register(hbs);
Object.keys(handlebarsHelpers).forEach((name) => {
hbs.registerHelper(name, handlebarsHelpers[name]);
});
}
async execute(routeMatch: RouteMatch, task: RouteTaskConfiguration):
Promise<any> {
let data = {};
const connectionStringCompiled =
hbs.compile(task.config.connectionStringTemplate);
const connectionString = connectionStringCompiled(routeMatch);
const configOptions:
ConfigOptions = {host: connectionString, apiVersion: '5.6'};
Object.assign(configOptions, task.config.elasticsearchConfig || {});
const client = new Client(configOptions);
if (Array.isArray(task.config.queryTemplates)) {
data = await this.multiQuery(client, task, routeMatch);
} else {
data = await this.singleQuery(client, task, routeMatch);
}
return data;
}
async singleQuery(
client: Client, task: RouteTaskConfiguration,
routeMatch: RouteMatch): Promise<RouterSearchResponse<any>> {
try {
const queryTemplate = task.config.queryTemplates;
let queryCompiled, queryJson, query;
try {
queryCompiled = hbs.compile(queryTemplate.template);
} catch (err) {
err = new RouteTaskError(err, {
statusCode: 500,
sourceRoute: routeMatch,
task,
redirectTo: task.errorRoute || null,
data: {queryTemplate}
});
throw err;
}
try {
queryJson = queryCompiled(routeMatch);
} catch (err) {
err = new RouteTaskError(err, {
statusCode: 500,
sourceRoute: routeMatch,
task,
redirectTo: task.errorRoute || null,
data: {queryTemplate}
});
throw err;
}
try {
query = JSON.parse(queryJson);
} catch (err) {
err = new RouteTaskError(err, {
statusCode: 500,
sourceRoute: routeMatch,
task,
redirectTo: task.errorRoute || null,
data: {queryTemplate, queryJson}
});
throw err;
}
const payload = {
index: queryTemplate.index,
type: queryTemplate.type,
body: query
};
let response: RouterSearchResponse<any>|null = null;
try {
response = await client.search<any>(payload);
} catch (err) {
const queryError = new RouteTaskError(err, {
statusCode: 500,
sourceRoute: routeMatch,
task,
redirectTo: task.errorRoute || null,
data: {payload}
});
throw queryError;
}
if (queryTemplate.noResultsRoute && (!response.hits || !response.hits.total)) {
console.log('No results route for response:', response);
throw new RouteTaskError(new Error('No results'), {
statusCode: 404,
sourceRoute: routeMatch,
task,
redirectTo: queryTemplate.noResultsRoute,
data: {payload}
});
} else if (!response.hits || !response.hits.total) {
console.log('No no results route for response', JSON.stringify(response, null, 4));
response.hits = {
max_score: 0,
hits: [],
total: 0
};
}
const pagination = this.getPagination(
query.from || 0, query.size || 10, response.hits.total);
response.pagination = pagination;
response.request = payload;
return response;
} catch (err) {
if (!(err instanceof RouteTaskError)) {
err = new RouteTaskError(err, {
statusCode: 500,
sourceRoute: routeMatch,
task,
redirectTo: task.errorRoute || null,
data: {}
});
}
throw err;
}
}
async multiQuery(
client: Client, task: RouteTaskConfiguration,
routeMatch: RouteMatch): Promise<RouterSearchResponseMap<any>> {
try {
const queryTemplates =
task.config.queryTemplates as ElasticsearchQueryTemplate[];
const bulk: any[] = [];
queryTemplates.forEach((queryTemplate) => {
let queryCompiled, queryJson, body, head;
try {
queryCompiled = hbs.compile(queryTemplate.template);
} catch (err) {
err = new RouteTaskError(err, {
statusCode: 500,
sourceRoute: routeMatch,
task,
redirectTo: task.errorRoute || null,
data: {queryTemplate}
});
throw err;
}
try {
queryJson = queryCompiled(routeMatch);
} catch (err) {
err = new RouteTaskError(err, {
statusCode: 500,
sourceRoute: routeMatch,
task,
redirectTo: task.errorRoute || null,
data: {queryTemplate}
});
throw err;
}
try {
body = JSON.parse(queryJson);
} catch (err) {
err = new RouteTaskError(err, {
statusCode: 500,
sourceRoute: routeMatch,
task,
redirectTo: task.errorRoute || null,
data: {queryTemplate, queryJson}
});
throw err;
}
head = {index: queryTemplate.index, type: queryTemplate.type};
const paginationDetails:
PaginationDetails = {from: body.from || 0, size: body.size || 10};
bulk.push(head);
bulk.push(body);
queryTemplate.paginationDetails = {from: body.from, size: body.size};
});
const payload: MSearchParams = {body: bulk};
let multiResponse: MSearchResponse<any> = {responses: []};
try {
multiResponse = await client.msearch(payload);
} catch (err) {
err = new RouteTaskError(err, {
statusCode: 500,
sourceRoute: routeMatch,
task,
redirectTo: task.errorRoute || null,
data: {payload}
});
throw err;
}
const responseMap: RouterSearchResponseMap<any> = {};
if (!multiResponse.responses) {
return {};
}
multiResponse.responses.forEach(
(response: RouterSearchResponse<any>, i: number) => {
const name = queryTemplates[i].name;
const paginationDetails =
queryTemplates[i].paginationDetails || {from: 0, size: 10};
const noResultsRoute = queryTemplates[i].noResultsRoute;
responseMap[name] = response;
if (noResultsRoute && (!response.hits || !response.hits.total)) {
console.log('No results route for response', response);
throw new RouteTaskError(new Error('No results'), {
statusCode: 404,
sourceRoute: routeMatch,
task,
redirectTo: noResultsRoute,
data: {queryTemplate: queryTemplates[i], response}
});
} else if (!response.hits || !response.hits.total) {
console.log('No no results route for response', JSON.stringify(response, null, 4));
response.hits = {
max_score: 0,
hits: [],
total: 0
};
}
const pagination = this.getPagination(
paginationDetails.from, paginationDetails.size,
response.hits.total);
response.pagination = pagination;
response.request = bulk[i * 2 + 1];
});
return responseMap;
} catch (err) {
if (!(err instanceof RouteTaskError)) {
err = new RouteTaskError(err, {
statusCode: 500,
sourceRoute: routeMatch,
task,
redirectTo: task.errorRoute || null,
data: {}
});
}
throw err;
}
}
getPagination(from = 0, size = 10, totalResults = 0): Pagination {
const totalPages = Math.ceil(totalResults / size);
const currentPage = Math.floor(from / size) + 1;
const nextPage =
currentPage < totalPages ? Math.floor(currentPage + 1) : null;
const prevPage = currentPage > 1 ? Math.floor(currentPage - 1) : null;
// Setup an array (range) of 10 numbers surrounding our current page
let pageRange =
Array.from(new Array(9).keys(), (p, i) => i + (currentPage - 4));
// Move range forward until none of the numbers are less than 1
const rangeMin = pageRange[0];
const positiveShift = rangeMin < 1 ? 1 - rangeMin : 0;
pageRange = pageRange.map(p => p + positiveShift);
// Move range backwards until none of the numbers are greater than
// totalPages
const rangeMax = pageRange[pageRange.length - 1];
const 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);
const pages: PaginationPage[] = [];
pageRange.forEach((page: number) => {
pages.push({
pageNumber: Math.floor(page),
distance: Math.abs(currentPage - page),
});
});
const pagination = {
from,
size,
totalResults,
totalPages,
currentPage,
nextPage,
prevPage,
pageRange: pages
};
return pagination;
}
}
export interface ElasticsearchTaskConfig {
connectionStringTemplate: string;
elasticsearchConfig: ConfigOptions;
queryTemplates: ElasticsearchQueryTemplate[]|ElasticsearchQueryTemplate;
}
export interface ElasticsearchQueryTemplate {
name: string;
index: string;
type: string;
template: string;
paginationDetails?: PaginationDetails;
noResultsRoute?: string;
}
export interface HandlebarsHelpers { [name: string]: (...args: any[]) => any; }
export interface RouterSearchResponse<T> extends SearchResponse<T> {
pagination?: Pagination;
request?: any;
}
export interface RouterSearchResponseMap<T> {
[name: string]: RouterSearchResponse<T>;
}
export interface Pagination {
from?: number;
size?: number;
totalResults?: number;
totalPages?: number;
currentPage?: number;
nextPage?: number|null;
prevPage?: number|null;
pageRange?: PaginationPage[];
}
export interface PaginationPage {
pageNumber: number;
distance: number;
}
export interface PaginationDetails {
from: number;
size: number;
}
/* tslint:enable:no-any */