// (C) 2007-2019 GoodData Corporation import * as request from "supertest"; import { executions, findMatchingExecution } from "../executions"; import { ISimpleExecutionRequest, IExecution } from "../../../model/Executions"; import { createEndpoint } from "../../../utils/tests"; import { ISchema } from "../../../schema/model/Schema"; import { IConfig } from "../../../model/Config"; const ELEMENT_REGEXP = /Element \(attributeIdentifier\) [0-9]+/; const DECIMAL_REGEXP = /^[0-9]+(\.\d+)?$/; function randomIdGenerator() { return "42"; // HA HA ha, we had fun once, it was terrible } function prepareExpress(schema: ISchema, executionIdGenerator = randomIdGenerator, config: IConfig = {}) { return createEndpoint(executions, schema, config, executionIdGenerator); } interface IResult { result: any; pollCount: number; } function pollForResult(app: any, pollUri: string, pollCount: number = 0): Promise { return request(app) .get(pollUri) .then(res => { if (res.status === 202) { return pollForResult(app, pollUri, pollCount + 1); } if ([200, 204, 413, 400].some(endStatus => endStatus === res.status)) { return Promise.resolve({ result: res, pollCount, }); } throw new Error(`Unsupported status code: ${res.status}`); }); } describe("executions", () => { describe("defined in schema", () => { const schema: ISchema = { project: { identifier: "mockproject", title: "My title", }, executions: [ { columns: ["attributeIdentifier", "metricIdentifier"], objectMapping: { attributeIdentifier: "attribute", metricIdentifier: "metric", }, result: "ok", }, { columns: ["metricIdentifier"], objectMapping: { metricIdentifier: "metric", }, result: "ok", resultValues: [53], }, { columns: ["metricIdentifier2"], objectMapping: { metricIdentifier2: "metric", }, result: "ok", resultValues: [35], }, { columns: ["metricIdentifierEmpty"], objectMapping: { metricIdentifierEmpty: "metric", }, result: "empty", }, { columns: ["metricIdentifierTooLarge"], objectMapping: { metricIdentifierTooLarge: "metric", }, result: "too large", }, { columns: ["metricIdentifierNotComputable"], objectMapping: { metricIdentifierNotComputable: "metric", }, result: "not computable", }, { columns: ["customPollCount"], objectMapping: { customPollCount: "metric", }, result: "ok", pollCount: 5, }, ], }; const requestData: ISimpleExecutionRequest = { execution: { columns: ["attributeIdentifier", "metricIdentifier"], }, }; it("should return correct synchronous executionResult", () => { return request(prepareExpress(schema)) .post("/gdc/internal/projects/mockproject/experimental/executions") .send(requestData) .expect(201) .then(res => expect(res.body).toEqual({ executionResult: { extendedTabularDataResult: "/gdc/internal/projects/mockproject/" + "experimental/executions/extendedResults/42", headers: [ { id: "attributeIdentifier", title: "Title attributeIdentifier", type: "attrLabel", uri: "attributeIdentifier", }, { id: "metricIdentifier", title: "Title metricIdentifier", type: "metric", uri: "metricIdentifier", }, ], tabularDataResult: "/gdc/internal/projects/mockproject/experimental/executions/42", }, }), ); }); it("should return tabular data result", () => { const app = prepareExpress(schema); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send(requestData) .expect(201) .then(res => pollForResult(app, res.body.executionResult.tabularDataResult)) .then(res => expect(res.result.body).toEqual( expect.objectContaining({ tabularDataResult: { values: expect.arrayContaining([ expect.arrayContaining([ expect.stringMatching(ELEMENT_REGEXP), expect.any(Number), ]), ]), }, }), ), ); }); it("should return tabular data result with specified value", () => { const app = prepareExpress(schema); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send({ execution: { columns: ["metricIdentifier"], }, }) .expect(201) .then(res => pollForResult(app, res.body.executionResult.tabularDataResult)) .then(res => expect(res.result.body.tabularDataResult.values[0][0]).toEqual(53)); }); it("should return tabular data result for column specified by uri", () => { const app = prepareExpress(schema); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send({ execution: { columns: [ "/gdc/internal/projects/mockproject/experimental/executions/metricIdentifier2", ], }, }) .expect(201) .then(res => pollForResult(app, res.body.executionResult.tabularDataResult)) .then(res => expect(res.result.body.tabularDataResult.values[0][0]).toEqual(35)); }); it("should return extended tabular data result", () => { const app = prepareExpress(schema); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send(requestData) .expect(201) .then(res => pollForResult(app, res.body.executionResult.extendedTabularDataResult)) .then(res => expect(res.result.body).toEqual( expect.objectContaining({ extendedTabularDataResult: { links: { self: "/gdc/internal/projects/mockproject/experimental/executions/extendedResults/42", }, paging: { count: expect.any(Number), next: null, offset: 0, total: expect.any(Number), }, values: expect.arrayContaining([ expect.arrayContaining([ expect.objectContaining({ id: expect.any(Number), name: expect.stringMatching(ELEMENT_REGEXP), }), expect.any(Number), ]), ]), }, }), ), ); }); it("should return empty", () => { const app = prepareExpress(schema); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send({ execution: { columns: ["metricIdentifierEmpty"], }, }) .expect(201) .then(res => pollForResult(app, res.body.executionResult.extendedTabularDataResult)) .then(res => expect(res.result.status).toEqual(204)); }); it("should return too large", () => { const app = prepareExpress(schema); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send({ execution: { columns: ["metricIdentifierTooLarge"], }, }) .expect(201) .then(res => pollForResult(app, res.body.executionResult.extendedTabularDataResult)) .then(res => expect(res.result.status).toEqual(413)); }); it("should return not computable", () => { const app = prepareExpress(schema); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send({ execution: { columns: ["metricIdentifierNotComputable"], }, }) .expect(201) .then(res => pollForResult(app, res.body.executionResult.extendedTabularDataResult)) .then(res => expect(res.result.status).toEqual(400)); }); it("should be possible to set pollCount in config", () => { const config: IConfig = { pollCount: 4 }; const app = prepareExpress(schema, randomIdGenerator, config); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send(requestData) .expect(201) .then(res => pollForResult(app, res.body.executionResult.extendedTabularDataResult)) .then(res => expect(res.pollCount).toEqual(4)); }); it("should be possible to set pollCount for single execution config", () => { const config: IConfig = { pollCount: 4 }; const app = prepareExpress(schema, randomIdGenerator, config); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send({ execution: { columns: ["customPollCount"], }, }) .expect(201) .then(res => pollForResult(app, res.body.executionResult.extendedTabularDataResult)) .then(res => expect(res.pollCount).toEqual(5)); }); }); describe("defined by default mock project", () => { const schema: ISchema = { project: { identifier: "mockproject", title: "My title", }, groups: [ { attributes: [ { identifier: "my.attr", title: "My title", elements: ["A", "B", "C", "D", "E"], }, { identifier: "your.attr", title: "Your title", elements: ["1", "2", "3"], }, ], }, ], }; const requestData: ISimpleExecutionRequest = { execution: { columns: ["my.attr.df", "your.attr.df", "metricIdentifier"], }, }; const requestDataWithDefinitions: ISimpleExecutionRequest = { execution: { columns: ["my.attr.df", "m1"], definitions: [ { metricDefinition: { expression: "", identifier: "m1", title: "Custom title from transformation", }, }, ], }, }; it("should return correct synchronous executionResult, unknown as metric", () => { return request(prepareExpress(schema)) .post("/gdc/internal/projects/mockproject/experimental/executions") .send({ execution: { columns: ["attributeIdentifier", "metricIdentifier"], }, }) .expect(201) .then(res => { expect(res.body).toEqual({ executionResult: { extendedTabularDataResult: "/gdc/internal/projects/mockproject/experimental/" + "executions/extendedResults/42", headers: [ { id: "attributeIdentifier", title: "Title attributeIdentifier", type: "metric", // unknown is translated to metric uri: "attributeIdentifier", }, { id: "metricIdentifier", title: "Title metricIdentifier", type: "metric", uri: "metricIdentifier", }, ], tabularDataResult: "/gdc/internal/projects/mockproject/experimental/executions/42", }, }); }); }); it("should return correct synchronous executionResult by attribute mocks", () => { return request(prepareExpress(schema)) .post("/gdc/internal/projects/mockproject/experimental/executions") .send(requestData) .expect(201) .then(res => expect(res.body).toEqual({ executionResult: { extendedTabularDataResult: "/gdc/internal/projects/mockproject/experimental/" + "executions/extendedResults/42", headers: [ { id: "my.attr.df", title: "Title my.attr.df", type: "attrLabel", uri: "my.attr.df", }, { id: "your.attr.df", title: "Title your.attr.df", type: "attrLabel", uri: "your.attr.df", }, { id: "metricIdentifier", title: "Title metricIdentifier", type: "metric", uri: "metricIdentifier", }, ], tabularDataResult: "/gdc/internal/projects/mockproject/experimental/executions/42", }, }), ); }); it("should return executionResult with header using title from execution request", () => { return request(prepareExpress(schema)) .post("/gdc/internal/projects/mockproject/experimental/executions") .send(requestDataWithDefinitions) .expect(201) .then(res => expect(res.body).toEqual({ executionResult: { extendedTabularDataResult: "/gdc/internal/projects/mockproject/experimental/" + "executions/extendedResults/42", headers: [ { id: "my.attr.df", title: "Title my.attr.df", type: "attrLabel", uri: "my.attr.df", }, { id: "m1", title: "Custom title from transformation", type: "metric", uri: "m1", }, ], tabularDataResult: "/gdc/internal/projects/mockproject/experimental/executions/42", }, }), ); }); it("should return tabular data result", () => { const app = prepareExpress(schema); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send(requestData) .expect(201) .then(res => pollForResult(app, res.body.executionResult.tabularDataResult)) .then(res => expect(res.result.body).toEqual( expect.objectContaining({ tabularDataResult: { values: expect.arrayContaining([ expect.arrayContaining([ expect.stringMatching(/Element \(my|your\.attr\.df\) [0-9]+/), expect.any(Number), ]), ]), }, }), ), ); }); it("should handle two metrics with same identifier", () => { const app = prepareExpress(schema); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send({ execution: { columns: ["my.attr.df", "your.attr.df", "metricIdentifier", "metricIdentifier"], }, }) .expect(201) .then(res => pollForResult(app, res.body.executionResult.tabularDataResult)) .then(res => { const firstRow = res.result.body.tabularDataResult.values[0]; expect(firstRow[0]).toMatch(/Element \(my|your\.attr\.df\) [0-9]+/); expect(firstRow[1]).toMatch(/Element \(my|your\.attr\.df\) [0-9]+/); expect(String(firstRow[2])).toMatch(DECIMAL_REGEXP); expect(String(firstRow[3])).toMatch(DECIMAL_REGEXP); }); }); describe("result size", () => { function runExecutionForAttribute( attributeId: string, executionId: string, expectedResult: number, ) { const app = prepareExpress(schema, () => executionId); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send({ execution: { columns: [attributeId, "metricIdentifier"], }, }) .expect(201) .then(res => pollForResult(app, res.body.executionResult.tabularDataResult)) .then(res => expect(res.result.body.tabularDataResult.values).toHaveLength(expectedResult), ); } it("should return number of rows equal to number of attribute elements", () => { return Promise.all([ runExecutionForAttribute("my.attr.df", "42", 5), runExecutionForAttribute("your.attr.df", "43", 3), ]); }); }); it("should return extended tabular data result", () => { const app = prepareExpress(schema); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send(requestData) .expect(201) .then(res => pollForResult(app, res.body.executionResult.extendedTabularDataResult)) .then(res => expect(res.result.body).toEqual( expect.objectContaining({ extendedTabularDataResult: { links: { self: "/gdc/internal/projects/mockproject/experimental/executions/extendedResults/42", }, paging: { count: expect.any(Number), next: null, offset: 0, total: expect.any(Number), }, values: expect.arrayContaining([ expect.arrayContaining([ expect.objectContaining({ id: expect.any(Number), name: expect.stringMatching( /Element \((my|your)\.attr\.df\) [0-9]+/, ), }), expect.any(Number), ]), ]), }, }), ), ); }); }); describe("sorting values in table", () => { const schema: ISchema = { project: { identifier: "mockproject", title: "Sorting project", }, groups: [ { attributes: [ { identifier: "attrIdentifier", title: "Account", elements: ["1", "2", "3"], }, ], }, ], executions: [ { columns: ["attrIdentifier.df", "metricIdentifier"], objectMapping: { "attrIdentifier.df": "attribute", metricIdentifier: "metric", }, result: "ok", resultValues: [20, 50, 10], }, ], }; it("should return data sorted by attribute ascending", () => { const expectedValues = [ [{ id: 0, name: "Element (attrIdentifier.df) 0" }, 20], [{ id: 1, name: "Element (attrIdentifier.df) 1" }, 50], [{ id: 2, name: "Element (attrIdentifier.df) 2" }, 10], ]; const app = prepareExpress(schema); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send({ execution: { columns: ["attrIdentifier.df", "metricIdentifier"], orderBy: [{ column: "attrIdentifier.df", direction: "asc" }], }, }) .expect(201) .then(res => pollForResult(app, res.body.executionResult.extendedTabularDataResult)) .then(res => expect(res.result.body.extendedTabularDataResult.values).toEqual(expectedValues), ); }); it("should return data sorted by attribute descending", () => { const expectedValues = [ [{ id: 0, name: "Element (attrIdentifier.df) 2" }, 10], [{ id: 1, name: "Element (attrIdentifier.df) 1" }, 50], [{ id: 2, name: "Element (attrIdentifier.df) 0" }, 20], ]; const app = prepareExpress(schema); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send({ execution: { columns: ["attrIdentifier.df", "metricIdentifier"], orderBy: [{ column: "attrIdentifier.df", direction: "desc" }], }, }) .expect(201) .then(res => pollForResult(app, res.body.executionResult.extendedTabularDataResult)) .then(res => expect(res.result.body.extendedTabularDataResult.values).toEqual(expectedValues), ); }); it("should return data sorted by metric ascending", () => { const expectedValues = [ [{ id: 0, name: "Element (attrIdentifier.df) 2" }, 10], [{ id: 1, name: "Element (attrIdentifier.df) 0" }, 20], [{ id: 2, name: "Element (attrIdentifier.df) 1" }, 50], ]; const app = prepareExpress(schema); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send({ execution: { columns: ["attrIdentifier.df", "metricIdentifier"], orderBy: [{ column: "metricIdentifier", direction: "asc" }], }, }) .expect(201) .then(res => pollForResult(app, res.body.executionResult.extendedTabularDataResult)) .then(res => expect(res.result.body.extendedTabularDataResult.values).toEqual(expectedValues), ); }); it("should return data sorted by metric descending", () => { const expectedValues = [ [{ id: 0, name: "Element (attrIdentifier.df) 1" }, 50], [{ id: 1, name: "Element (attrIdentifier.df) 0" }, 20], [{ id: 2, name: "Element (attrIdentifier.df) 2" }, 10], ]; const app = prepareExpress(schema); return request(app) .post("/gdc/internal/projects/mockproject/experimental/executions") .send({ execution: { columns: ["attrIdentifier.df", "metricIdentifier"], orderBy: [{ column: "metricIdentifier", direction: "desc" }], }, }) .expect(201) .then(res => pollForResult(app, res.body.executionResult.extendedTabularDataResult)) .then(res => expect(res.result.body.extendedTabularDataResult.values).toEqual(expectedValues), ); }); }); describe("finding matching executions", () => { const amountExecution: IExecution = { columns: ["metric.amount"], objectMapping: { "metric.amount": "metric", }, }; const amountExecutionLastYear: IExecution = { ...amountExecution, where: { "metric.amount": { $between: [-1, -1], $granularity: "GDC.time.year", }, }, }; const amountExecutionJanuary2019: IExecution = { ...amountExecution, where: { "metric.amount": { $between: ["2019-01-01", "2019-01-31"], $granularity: "GDC.time.date", }, }, }; const availableExecutions = [amountExecution, amountExecutionLastYear, amountExecutionJanuary2019]; it("should return undefined when no execution is matched", () => { const actual = findMatchingExecution( { columns: ["/gdc/md/mockproject/obj/foo.amount"] }, availableExecutions, ); expect(actual).toBeUndefined(); }); it("should return matching execution without where", () => { const actual = findMatchingExecution( { columns: ["/gdc/md/mockproject/obj/metric.amount"] }, availableExecutions, ); expect(actual).toEqual(amountExecution); }); it("should return matching execution with empty where", () => { const actual = findMatchingExecution( { columns: ["/gdc/md/mockproject/obj/metric.amount"], where: {} }, availableExecutions, ); expect(actual).toEqual(amountExecution); }); it("should return matching execution with where", () => { const actual = findMatchingExecution( { columns: ["/gdc/md/mockproject/obj/metric.amount"], where: { "/gdc/md/mockproject/obj/metric.amount": { $between: [-1, -1], $granularity: "GDC.time.year", }, }, }, availableExecutions, ); expect(actual).toEqual(amountExecutionLastYear); }); it("should return matching execution with absolute date where", () => { const actual = findMatchingExecution( { columns: ["/gdc/md/mockproject/obj/metric.amount"], where: { "/gdc/md/mockproject/obj/metric.amount": { $between: ["2019-01-01", "2019-01-31"], $granularity: "GDC.time.date", }, }, }, availableExecutions, ); expect(actual).toEqual(amountExecutionJanuary2019); }); }); });