UNPKG

@noggin/elastic-noggin-sdk

Version:
311 lines (281 loc) 8.73 kB
import { IVars } from "./vars"; import { Tip, IQueryResponse, IAttrResult, IDimResult, IQueryResult, Batch, ResponseHeaders, IHeaderValue, } from "./models/types"; import { EnoFactory } from "./EnoFactory"; import { Observable, of, forkJoin } from "rxjs"; import { map, switchMap, tap, timeout } from "rxjs/operators"; import { send } from "./send"; import { IEnSrvOptions } from "./IEnSrvOptions"; import { checkBatchForError } from "./error"; import { getLangs, nowVar } from "./locale"; import { get, has, set } from "lodash"; export interface IQueryExtraInfo { label: string; formula: string; } export interface IDimensionOption extends IQueryExtraInfo { sortby?: string[]; sortdir?: ("asc" | "desc")[]; offset?: number; limit?: number; } export interface IQueryOption { branch?: Tip; lang?: string | string[]; // watch?: boolean; vars?: IVars; extraFilters?: IQueryExtraInfo[]; extraAttributes?: IQueryExtraInfo[]; dimensionOptions?: IDimensionOption[]; includeFallbackLang?: boolean; responseHeadersToInclude?: ResponseHeaders; lastPersist?: string; } // Executes a one-dimensional query export function execute1d<T>( queryTip: Tip, enSrvOptions: IEnSrvOptions, options: IQueryOption = { branch: "branch/master", lang: "en-us", // watch: true, vars: {}, extraFilters: [], extraAttributes: [], dimensionOptions: [{ label: "Tip", formula: "TIP()" }], includeFallbackLang: true }, timeoutMs: number = 10000 ): Observable<T[]> { return execute(queryTip, enSrvOptions, options, timeoutMs).pipe( map((response: IQueryResponse) => { const keys = response.dimensions[0].values; return response.results.map((result, i) => { return <T>(<any>result[keys[i]]); }); }) ); } export function execute1dWithResponseHeaders<T>( queryTip: Tip, enSrvOptions: IEnSrvOptions, options: IQueryOption = { branch: "branch/master", lang: "en-us", vars: {}, extraFilters: [], extraAttributes: [], dimensionOptions: [{ label: "Tip", formula: "TIP()" }], includeFallbackLang: true, responseHeadersToInclude: [] }, timeoutMs: number = 10000 ): Observable<T[] | { results: T[]; responseHeaders: IHeaderValue[] }> { return execute(queryTip, enSrvOptions, options, timeoutMs).pipe( map((response: IQueryResponse) => { const keys = response.dimensions[0].values; const results = response.results.map((result, i) => { return <T>(<any>result[keys[i]]); }); return response.responseHeaders ? { results, responseHeaders: response.responseHeaders } : results; }) ); } // Executes a query on Ensrv export function execute( queryTip: Tip, enSrvOptions: IEnSrvOptions, options: IQueryOption = { branch: "branch/master", lang: "en-us", // watch: true, vars: {}, extraFilters: [], dimensionOptions: [{ label: "Tip", formula: "TIP()" }], includeFallbackLang: true, responseHeadersToInclude: [] }, timeoutMs: number = 10000 ): Observable<IQueryResponse> { const setNow = (now: string) => set(options, ["vars", "---NOW---"], [now]); const timezone$ = has(options, ["vars", "---NOW---"]) ? of(null) : nowVar(enSrvOptions).pipe(tap(setNow)); const langs$ = getLangs( enSrvOptions, get(options, "lang"), get(options, "includeFallbackLang", true) ); const send$ = (langs: string[]) => { const { queryTimeoutMs, observableTimeoutMs } = calcQueryTimeouts(timeoutMs); const enoFactory = new EnoFactory("op/query", "security/policy/op"); enoFactory.setField("op/query/tip", [queryTip]); enoFactory.setField("op/query/branch", [get(options, 'branch', 'branch/master')]); enoFactory.setField("op/query/lang", langs); enoFactory.setField("op/query/timeout", [queryTimeoutMs.toString()]); enoFactory.setField("op/query/query", [ JSON.stringify({ attributes: options.extraAttributes, filters: options.extraFilters, vars: options.vars || {}, dimensions: options.dimensionOptions || [], lastPersist: options.lastPersist, }), ]); return send([enoFactory.makeEno()], enSrvOptions, options).pipe( tap(checkBatchForError), map((batch) => parseResponse( batch, options?.responseHeadersToInclude as IHeaderValue[] ) ), timeout(observableTimeoutMs) ); }; // return send$(); return forkJoin({ tz: timezone$, langs: langs$ }).pipe( switchMap(({ langs }) => send$(langs)) ); } /** * There are two timeouts: * * (1) the timeout for EnSrv to abort early on a query * This will be minimum of 1s up to timeoutMs - 2s * * (2) the timeout for our Observable to abort early * This will be minimum of 1.5s up to timeoutMs * * So our observable should always timeout after EnSrv does */ export function calcQueryTimeouts(timeoutMs: number): { queryTimeoutMs: number; observableTimeoutMs: number; } { const timeoutBufferMs: number = 2000; const minimumQueryTimeoutMs: number = 1000; const maximumQueryTimeoutMs: number = 28000; const minimumObservableTimeoutMs: number = 1500; const queryTimeoutMs = Math.min( maximumQueryTimeoutMs, Math.max(minimumQueryTimeoutMs, timeoutMs - timeoutBufferMs) ); const observableTimeoutMs = Math.max(minimumObservableTimeoutMs, timeoutMs); return { queryTimeoutMs, observableTimeoutMs }; } // Convert the query response batch to a query response function parseResponse(sendBatch: Batch, responseHeaders?: IHeaderValue[]): IQueryResponse { let packedResults = undefined; const queryResponse: IQueryResponse = { attributes: [], dimensions: [], execTime: undefined, results: [], }; let runtimeDims = []; let runtimeAttrs = []; sendBatch.forEach((eno) => { switch (eno.getType()) { case "response/query": queryResponse.execTime = eno.getFieldNumberValue( "response/query/exec-time" ); runtimeAttrs = eno .getFieldValues("response/query/runtime-attributes") .map((attrJson) => JSON.parse(attrJson)); runtimeDims = eno .getFieldValues("response/query/runtime-dimensions") .map((dim) => JSON.parse(dim)) .map((dim) => ({ label: dim.label, values: dim.value })); packedResults = eno.getFieldValues("response/query/result"); break; case "response/query/dimension": queryResponse.dimensions.push({ tip: eno.tip, label: eno.getFieldStringValue("response/query/dimension/label"), values: eno.getFieldValues("response/query/dimension/value"), }); break; case "query/attribute": queryResponse.attributes.push({ tip: eno.tip, label: eno.getFieldStringValue("query/attribute/label"), formula: eno.getFieldStringValue("query/attribute/formula"), }); break; } }); if (packedResults === undefined) { throw new Error("No query response"); } runtimeAttrs.forEach((runtimeAttr) => queryResponse.attributes.push(runtimeAttr) ); runtimeDims.forEach((runtimeDim) => queryResponse.dimensions.push(runtimeDim) ); if (queryResponse.dimensions.length === 0) { throw new Error("No dimension in response"); } queryResponse.results = unpackQueryResults( packedResults, queryResponse.dimensions, queryResponse.attributes ); return { ...queryResponse, ...(responseHeaders ? { responseHeaders } : {}), }; } function unpackQueryResults( flatResult: string[], dims: IDimResult[], attrs: IAttrResult[], result: IQueryResult[] = [], depth: number = 0 ): IQueryResult[] { if (depth === dims.length - 1) { let i = 0; dims[dims.length - 1].values.forEach((lastDimValue) => { const leafResult: IQueryResult = {}; leafResult[lastDimValue] = {}; if (flatResult[i] === "{#}") { i += attrs.length; return; } for (const attr of attrs) { leafResult[lastDimValue][attr.label] = flatResult[i++]; } result.push(leafResult); }); return result; } dims[depth].values.forEach((dimValue, i) => { const subResult: IQueryResult[] = []; const subResultSize = flatResult.length / dims[depth].values.length; const subResultStart = i * subResultSize; const subResultEnd = subResultStart + subResultSize; result[i] = {}; result[i][dimValue] = subResult; unpackQueryResults( flatResult.slice(subResultStart, subResultEnd), dims, attrs, subResult, depth + 1 ); }); return result; }