@noggin/elastic-noggin-sdk
Version:
Elastic Noggin SDK
311 lines (281 loc) • 8.73 kB
text/typescript
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;
}