// (C) 2007-2019 GoodData Corporation import { Application } from "express"; import { isEqual, keys, partial, last, get, sortBy, uniqueId, mapKeys, isEmpty } from "lodash"; import * as HttpStatusCodes from "http-status-codes"; import { IExecution, ISimpleExecutionRequest, IDefinition, ISimpleExecution } from "../../model/Executions"; import { IMockProject, getAttributes, isKnownDisplayFormId, getAttributeElementsByDisplayForm, } from "../../model/MockProject"; import { INestedAttributeDisplayForm } from "../../model/NestedAttributeDisplayForm"; import { tabularResultUri, extendedTabularResultUri } from "../../route/routes"; import { IConfig } from "../../model/Config"; // TODO: Do we still need it? :-/ const otfExecutionsCache: IExecution[] = []; const DEFAULT_NUMBER_OF_ROWS = 1; interface IExecutionInfo { pollCounter: number; execution: IExecution; } const executionCache = new Map(); interface IExecutionsResponse { executionResult: { headers: IHeaderInfo[]; tabularDataResult: string; extendedTabularDataResult: string; }; } interface IHeaderInfo { type: string; id: string; title: string; uri?: string; format?: string; } interface IExecutionsResult { tabularDataResult: { values: generatedValue[][]; }; } interface IOrderBy { column: string; direction: string; } interface IExecutionsExtendedResult { extendedTabularDataResult: { links?: { self: string; }; paging: { offset: number; count: number; total: number; next: string; }; values: any[][]; }; } function getTitle(identifier: string, definitions: IDefinition[] = []): string { const definition = definitions.find( definition => get(definition, "metricDefinition.identifier") === identifier, ); return (get(definition, "metricDefinition.title") as string) || `Title ${identifier}`; } const getIdentifier = (uri: string): string => last(uri.split("/")); function getHeaders( requestColumns: string[], mockExecution: IExecution, body: ISimpleExecutionRequest, ): IHeaderInfo[] { const { objectMapping } = mockExecution; return requestColumns.map(identifier => ({ type: objectMapping[identifier] === "metric" ? "metric" : "attrLabel", id: getIdentifier(identifier), title: getTitle(getIdentifier(identifier), get(body, "execution.definitions")), uri: identifier, })); } function getExecutionResponse( requestColumns: string[], executionId: string, mockExecution: IExecution, projectId: string, body: ISimpleExecutionRequest, ): IExecutionsResponse { return { executionResult: { headers: getHeaders(requestColumns, mockExecution, body), tabularDataResult: tabularResultUri(projectId, executionId), extendedTabularDataResult: extendedTabularResultUri(projectId, executionId), }, }; } function createExecutionOnTheFly(requestColumns: string[], project: IMockProject): IExecution { const displayForms: INestedAttributeDisplayForm[] = getAttributes(project).reduce( (a, b) => [...a, ...b.content.displayForms], [], ); const objectMapping = requestColumns.reduce((mapping, uriOrIdentifier) => { const isColumnDf = displayForms.some(df => { return df.meta.identifier === uriOrIdentifier || df.meta.uri === uriOrIdentifier; }); return { ...mapping, [uriOrIdentifier]: isColumnDf ? "attrLabel" : "metric" }; }, {}); return { result: "ok", objectMapping, columns: requestColumns, }; } function uriToId(uri: string): string { return uri.substring(uri.lastIndexOf("/") + 1, uri.length); } export function findMatchingExecution( requestedExecution: ISimpleExecution, availableExecutions: IExecution[], ): IExecution { const requestedColumnIds = [...requestedExecution.columns].map(uriToId).sort(); const requestedWhere = !isEmpty(requestedExecution.where) ? mapKeys(requestedExecution.where, (_, key) => uriToId(key)) : undefined; return availableExecutions.find( e => isEqual(requestedColumnIds, keys(e.objectMapping).sort()) && isEqual(requestedWhere, e.where), ); } function createResponse( body: ISimpleExecutionRequest, project: IMockProject, projectId: string, executionId: string, ): IExecutionsResponse { const mockExecutions = [...project.executions, ...otfExecutionsCache]; const requestColumns = body.execution.columns; let mockExecution = findMatchingExecution(body.execution, mockExecutions); if (!mockExecution) { // default execution mockExecution = createExecutionOnTheFly(requestColumns, project); otfExecutionsCache.push(mockExecution); } executionCache.set(executionId, { pollCounter: 0, execution: mockExecution, }); return getExecutionResponse(requestColumns, executionId, mockExecution, projectId, body); } type valueGenerator = (row: number, column: number, identifier: string) => string | object; type generatedValue = number | string | object; const isUri = (str: string) => str.startsWith("/gdc"); function getRowsCount(project: IMockProject, execution: IExecution): number { const ensureIdentifier = (str: string): string => (isUri(str) ? getIdentifier(str) : str); return execution.columns .map(column => ensureIdentifier(column)) .filter(column => isKnownDisplayFormId(project, column)) .map(column => getAttributeElementsByDisplayForm(project, column).length) .reduce((prev, next) => Math.max(prev, next), DEFAULT_NUMBER_OF_ROWS); } function generateValues( project: IMockProject, execution: IExecution, generateElementFn: valueGenerator, orderBy: IOrderBy, ) { const columnIdentifiers = execution.columns; const values: generatedValue[][] = []; const rowsCount = getRowsCount(project, execution); // Create table and sort it if orderBy is defined const table = generateTable(project, execution, orderBy); // tslint:disable-next-line:no-param-reassign for (let row = 0; row < rowsCount; row += 1) { const columns = []; // tslint:disable-next-line:no-param-reassign for (let column = 0; column < columnIdentifiers.length; column += 1) { const columnType = execution.objectMapping[columnIdentifiers[column]]; const value = table[row][columnIdentifiers[column]]; if (columnType === "metric") { columns.push(value); } else { columns.push(generateElementFn(row, column, value)); } } values.push(columns); } return values; } function generateTable(project: IMockProject, execution: IExecution, orderBy: IOrderBy) { const rowsCount = getRowsCount(project, execution); const columnIdentifiers = execution.columns; const table = []; // tslint:disable-next-line:no-param-reassign for (let row = 0; row < rowsCount; row += 1) { const rowValue = {}; // tslint:disable-next-line:no-param-reassign for (const columnIdentifier of columnIdentifiers) { const columnType = execution.objectMapping[columnIdentifier]; let value; if (columnType === "metric") { value = execution.resultValues && execution.resultValues[row] ? execution.resultValues[row] : (value = Math.random() * 100); } else { value = `Element (${getIdentifier(columnIdentifier)}) ${row}`; } rowValue[columnIdentifier] = value; } table.push(rowValue); } if (orderBy) { const sortedTable = sortBy(table, [`${orderBy.column}`]); return orderBy.direction === "asc" ? sortedTable : sortedTable.reverse(); } else { return table; } } function createResult(project: IMockProject, execution: IExecution, orderBy: IOrderBy): IExecutionsResult { const values = generateValues( project, execution, (_row: number, _column: number, identifier: string) => identifier, orderBy, ); return { tabularDataResult: { values, }, }; } function createExtendedResult( projectId: string, project: IMockProject, execution: IExecution, id: string, orderBy: IOrderBy, ): IExecutionsExtendedResult { const values = generateValues( project, execution, (row: number, column: number, identifier: string) => ({ id: 10 * column + row, name: identifier, }), orderBy, ); return { extendedTabularDataResult: { links: { self: extendedTabularResultUri(projectId, id), }, paging: { offset: 0, count: values.length, total: values.length, next: null, }, values, }, }; } function maybeWaitAndCreateResult( res: any, project: IMockProject, executionId: string, createResponseFn: any, orderBy: IOrderBy, globalPollCount: number, ) { const { execution, pollCounter } = executionCache.get(executionId); const pollCount = execution.pollCount || globalPollCount; if (pollCounter < pollCount) { executionCache.set(executionId, { execution, pollCounter: pollCounter + 1 }); return res.status(HttpStatusCodes.ACCEPTED).send(); } executionCache.delete(executionId); if (execution.result === "empty") { // 'ok' | 'empty' | 'too large' | 'not computable' return res.status(HttpStatusCodes.NO_CONTENT).send(); } if (execution.result === "too large") { return res.status(HttpStatusCodes.REQUEST_TOO_LONG).send(); } if (execution.result === "not computable") { return res.status(HttpStatusCodes.BAD_REQUEST).send(); } return res.status(HttpStatusCodes.OK).send(createResponseFn(project, execution, executionId, orderBy)); } type IDGenerator = (s?: string) => string; export const executions = { register(app: Application, project: IMockProject, config: IConfig, idGenerator: IDGenerator = uniqueId) { let orderBy: IOrderBy; app.post( `/gdc/internal/projects/${project.project.meta.identifier}/experimental/executions`, (req, res) => { orderBy = req.body.execution.orderBy ? req.body.execution.orderBy[0] : null; const executionId = idGenerator(); const response = createResponse( req.body, project, project.project.meta.identifier, executionId, ); return res.status(HttpStatusCodes.CREATED).send(response); }, ); app.get( `/gdc/internal/projects/${project.project.meta.identifier}/experimental/executions/:id`, (req, res) => { return maybeWaitAndCreateResult( res, project, req.params.id, createResult, orderBy, config.pollCount, ); }, ); app.get( `/gdc/internal/projects/${ project.project.meta.identifier }/experimental/executions/extendedResults/:id`, (req, res) => { return maybeWaitAndCreateResult( res, project, req.params.id, partial(createExtendedResult, project.project.meta.identifier), orderBy, config.pollCount, ); }, ); return app; }, };