// (C) 2007-2020 GoodData Corporation import { ExecuteAFM as AFM, Execution } from "@gooddata/typings"; import * as express from "express"; import * as HttpStatusCodes from "http-status-codes"; import * as querystring from "querystring"; import * as request from "supertest"; import { schema } from "../../../schema/fixtures/dummySchema"; import { AFMBuilder } from "../../../utils/AFMBuilder/AFMBuilder"; import { createEndpoint } from "../../../utils/tests"; import { addToCache, cleanOldCache, executeAfm, getFromCache, getNormalizedExecution, hasCachedResult, isExecutionValid, sanitize, } from "../executeAfm"; import { customDimensionIdentifiers, executionWithArithmeticAndDerivedMeasures, } from "./fixtures/execution.1"; async function pollAndGetResponseAndResult( app: express.Application, afm: AFM.IExecution, limit?: string, offset?: string, ) { const pollingResponse = await request(app) .post("/gdc/app/projects/mockproject/executeAfm") .send(afm); const response = pollingResponse.body; let url = response.executionResponse.links.executionResult; if (limit !== undefined) { url += "&limit=" + querystring.escape(limit); } if (offset !== undefined) { url += "&offset=" + querystring.escape(offset); } const result = await request(app) .get(url) .send(); return { response, result, }; } async function getResponse(app: express.Application, afm: AFM.IExecution) { const pollingResponse = await request(app) .post("/gdc/app/projects/mockproject/executeAfm") .send(afm); return { response: pollingResponse, }; } async function pollAndGetResult( app: express.Application, afm: AFM.IExecution, limit?: string, offset?: string, ) { const response = await pollAndGetResponseAndResult(app, afm, limit, offset); return response.result; } describe("executeAfm", () => { describe("on the fly results", () => { it("should return polling URI with correct number of dimensions from resultSpec", async () => { const app = createEndpoint(executeAfm, schema); const builder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttribute() .addResultSpec({ dimensions: [ { itemIdentifiers: ["a0"], }, ], }); const response = await request(app) .post("/gdc/app/projects/mockproject/executeAfm") .send(builder.build()) .expect(201); expect(response.body).toMatchSnapshot(); }); it("should return polling URI with default number of dimensions when resultSpec is not defined", async () => { const app = createEndpoint(executeAfm, schema); const builder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttribute(); const response = await request(app) .post("/gdc/app/projects/mockproject/executeAfm") .send(builder.build()) .expect(201); expect(response.body).toMatchSnapshot(); }); it("should return result on polling uri", async () => { const app = createEndpoint(executeAfm, schema); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttributeBasedMeasure() .addAttribute() .addAttribute() .addSortByMeasure(0, "desc"); const response = await pollAndGetResult(app, afmBuilder.build()); expect(response.status).toEqual(200); expect(response.body).toEqual( expect.objectContaining({ executionResult: { paging: { count: expect.any(Array), offset: expect.any(Array), total: expect.any(Array), }, data: expect.arrayContaining([ expect.arrayContaining([expect.stringMatching(/^[0-9]+(\.\d+)?$/)]), ]), headerItems: expect.arrayContaining([ expect.arrayContaining([ expect.arrayContaining([ expect.objectContaining({ attributeHeaderItem: { uri: expect.any(String), name: expect.any(String), }, }), ]), ]), ]), }, }), ); }); it("should return random result with randomSeed not set", async () => { const app = createEndpoint(executeAfm, schema); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttributeBasedMeasure() .addAttribute() .addAttribute() .addSortByMeasure(0, "desc"); const first = await pollAndGetResult(app, afmBuilder.build()); const second = await pollAndGetResult(app, afmBuilder.build()); expect(first.body.executionResult.data).not.toEqual(second.body.executionResult.data); }); it("should return deterministic result with randomSeed set", async () => { const app = createEndpoint(executeAfm, schema, { randomSeed: "this-is-seed" }); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttributeBasedMeasure() .addAttribute() .addAttribute() .addSortByMeasure(0, "desc"); const response = await pollAndGetResult(app, afmBuilder.build()); expect(response.status).toEqual(200); expect(response.body).toEqual({ executionResult: { data: [ ["899", "841", "652", "608", "499", "458"], ["186", "844", "99", "317", "248", "792"], ], headerItems: expect.anything(), paging: expect.anything(), }, }); }); it("should persist data to metric when randomSeed used", async () => { const app = createEndpoint(executeAfm, schema, { randomSeed: "this-is-seed" }); const afmBuilder1 = AFMBuilder.buildFromSchema(schema) .addAttribute() .addMeasure(null, null, null, 1) .addMeasure(null, null, null, 2); const response1 = await pollAndGetResult(app, afmBuilder1.build()); const afmBuilder2 = AFMBuilder.buildFromSchema(schema) .addAttribute() .addMeasure(null, null, null, 2) .addMeasure(null, null, null, 1); const response2 = await pollAndGetResult(app, afmBuilder2.build()); expect(response1.body.executionResult.data).not.toEqual(response2.body.executionResult.data); expect(response1.body.executionResult.data).toEqual([ ["186", "99", "792"], ["638", "446", "510"], ]); expect(response2.body.executionResult.data).toEqual([ ["638", "446", "510"], ["186", "99", "792"], ]); }); it("should return 400 when resultSpec contains measure group and AFM does not contain measures", async () => { const app = createEndpoint(executeAfm, schema); const builder = AFMBuilder.buildFromSchema(schema) .addAttribute() .addResultSpec({ dimensions: [ { itemIdentifiers: ["a0"], }, { itemIdentifiers: ["measureGroup"], }, ], }); const response = await request(app) .post("/gdc/app/projects/mockproject/executeAfm") .send(builder.build()); expect(response.status).toEqual(HttpStatusCodes.BAD_REQUEST); expect(response.text).toEqual("Specified 'measureGroup' but no measures found in AFM"); }); it("should return 400 when sort item references missing attribute", async () => { const app = createEndpoint(executeAfm, schema); const builder = AFMBuilder.buildFromSchema(schema) .addAttribute() .addMeasure() .addResultSpec({ dimensions: [ { itemIdentifiers: ["a0"], }, { itemIdentifiers: ["measureGroup"], }, ], sorts: [{ attributeSortItem: { attributeIdentifier: "BANG", direction: "desc" } }], }); const response = await request(app) .post("/gdc/app/projects/mockproject/executeAfm") .send(builder.build()); expect(response.status).toEqual(HttpStatusCodes.BAD_REQUEST); expect(response.text).toEqual("Bad sort - attribute BANG not in AFM"); }); }); it("should not return headerItems property if no data exist", async () => { const app = createEndpoint(executeAfm, schema, { randomSeed: "this-is-seed" }); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttribute() .addFilter({ measureValueFilter: { measure: { localIdentifier: "m0", }, condition: { comparison: { operator: "LESS_THAN", value: 0, // this value will never be generated by dataResult.ts }, }, }, }); const response = await pollAndGetResult(app, afmBuilder.build()); expect(response.status).toBe(200); expect(response.body.executionResult.headerItems).toBeUndefined(); }); it("should return result for single attribute", async () => { const app = createEndpoint(executeAfm, schema); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addAttribute() .addResultSpec({ dimensions: [ { itemIdentifiers: [], }, { itemIdentifiers: ["a0"], }, ], }); const response = await request(app) .post("/gdc/app/projects/mockproject/executeAfm") .send(afmBuilder.build()); expect(response.body).toEqual( expect.objectContaining({ executionResponse: { dimensions: expect.arrayContaining([ expect.objectContaining({ headers: [], }), expect.objectContaining({ headers: expect.arrayContaining([ expect.objectContaining({ attributeHeader: { localIdentifier: expect.any(String), name: expect.any(String), identifier: expect.any(String), uri: expect.any(String), formOf: { uri: expect.any(String), identifier: expect.any(String), name: expect.any(String), }, }, }), ]), }), ]), links: { executionResult: expect.any(String), }, }, }), ); }); it("should return correct execution result for stackBy chart with a0=stackBy and a1=viewBy", async () => { const app = createEndpoint(executeAfm, schema); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttribute() .addAttribute() .addResultSpec({ dimensions: [ { itemIdentifiers: ["a0"], // a0 = stackBy attribute }, { itemIdentifiers: ["a1", "measureGroup"], // a1 = viewBy attribute }, ], }); const response = await pollAndGetResult(app, afmBuilder.build()); const expectedExecutionResult = { executionResult: { data: [["11", "12"], ["21", "22"], ["31", "32"]], headerItems: [ [ [ { attributeHeaderItem: { name: "John Doe", uri: "/gdc/md/mockproject/obj/attr.employee/elements?id=1", }, }, { attributeHeaderItem: { name: "Jane Doe", uri: "/gdc/md/mockproject/obj/attr.employee/elements?id=2", }, }, { attributeHeaderItem: { name: "Jim Doe", uri: "/gdc/md/mockproject/obj/attr.employee/elements?id=3", }, }, ], ], [ [ { attributeHeaderItem: { name: "Company A", uri: "/gdc/md/mockproject/obj/1234/elements?id=1", }, }, { attributeHeaderItem: { name: "Company B", uri: "/gdc/md/mockproject/obj/1234/elements?id=2", }, }, ], [ { measureHeaderItem: { name: "Simple metric", order: 0, }, }, { measureHeaderItem: { name: "Simple metric", order: 0, }, }, ], ], ], paging: { count: [3, 2], offset: [0, 0], total: [3, 2], }, }, }; expect(response.status).toEqual(HttpStatusCodes.OK); const { executionResult } = response.body; expect(executionResult.data.length).toEqual(3); expect(executionResult.data[0].length).toEqual(2); expect(executionResult.data[1].length).toEqual(2); expect(executionResult.data[2].length).toEqual(2); expect(executionResult.headerItems).toEqual(expectedExecutionResult.executionResult.headerItems); expect(executionResult.paging).toEqual(expectedExecutionResult.executionResult.paging); }); it("should return correct execution result for stackBy chart with a1=stackBy and a0=viewBy", async () => { const app = createEndpoint(executeAfm, schema); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttribute() .addAttribute() .addResultSpec({ dimensions: [ { itemIdentifiers: ["a1"], // a1 = stackBy attribute }, { itemIdentifiers: ["a0", "measureGroup"], // a0 = viewBy attribute }, ], }); const response = await pollAndGetResult(app, afmBuilder.build()); const expectedExecutionResult = { executionResult: { data: [["11", "12", "13"], ["21", "22", "23"]], headerItems: [ [ [ { attributeHeaderItem: { name: "Company A", uri: "/gdc/md/mockproject/obj/1234/elements?id=1", }, }, { attributeHeaderItem: { name: "Company B", uri: "/gdc/md/mockproject/obj/1234/elements?id=2", }, }, ], ], [ [ { attributeHeaderItem: { name: "John Doe", uri: "/gdc/md/mockproject/obj/attr.employee/elements?id=1", }, }, { attributeHeaderItem: { name: "Jane Doe", uri: "/gdc/md/mockproject/obj/attr.employee/elements?id=2", }, }, { attributeHeaderItem: { name: "Jim Doe", uri: "/gdc/md/mockproject/obj/attr.employee/elements?id=3", }, }, ], [ { measureHeaderItem: { name: "Simple metric", order: 0, }, }, { measureHeaderItem: { name: "Simple metric", order: 0, }, }, { measureHeaderItem: { name: "Simple metric", order: 0, }, }, ], ], ], paging: { count: [2, 3], offset: [0, 0], total: [2, 3], }, }, }; expect(response.status).toEqual(HttpStatusCodes.OK); const { executionResult } = response.body; expect(executionResult.data.length).toEqual(2); expect(executionResult.data[0].length).toEqual(3); expect(executionResult.data[1].length).toEqual(3); expect(executionResult.headerItems).toEqual(expectedExecutionResult.executionResult.headerItems); expect(executionResult.paging).toEqual(expectedExecutionResult.executionResult.paging); }); it("should return proper response and data for multiple measures, no attributes and one dimension", async () => { const app = createEndpoint(executeAfm, schema, { randomSeed: "this-is-seed" }); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addMeasure() .addResultSpec({ dimensions: [ { itemIdentifiers: ["measureGroup"], }, ], }); const response = await pollAndGetResult(app, afmBuilder.build()); const expectedExecutionResult = { executionResult: { data: ["899", "186"], headerItems: [ [ [ { measureHeaderItem: { name: "Simple metric", order: 0, }, }, { measureHeaderItem: { name: "Simple metric 2", order: 1, }, }, ], ], ], paging: { count: [2], offset: [0], total: [2], }, }, }; expect(response.status).toEqual(HttpStatusCodes.OK); const { executionResult } = response.body; expect(executionResult.data).toEqual(expectedExecutionResult.executionResult.data); expect(executionResult.headerItems).toEqual(expectedExecutionResult.executionResult.headerItems); expect(executionResult.paging).toEqual(expectedExecutionResult.executionResult.paging); }); it("should return result for table configuration", async () => { const app = createEndpoint(executeAfm, schema); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttribute() .addResultSpec({ dimensions: [ { itemIdentifiers: ["a0"], }, { itemIdentifiers: ["measureGroup"], }, ], }); const response = await request(app) .post("/gdc/app/projects/mockproject/executeAfm") .send(afmBuilder.build()); expect(response.body).toEqual( expect.objectContaining({ executionResponse: { dimensions: expect.arrayContaining([ expect.objectContaining({ headers: expect.arrayContaining([ expect.objectContaining({ attributeHeader: { localIdentifier: expect.any(String), name: expect.any(String), identifier: expect.any(String), uri: expect.any(String), formOf: { uri: expect.any(String), identifier: expect.any(String), name: expect.any(String), }, }, }), ]), }), expect.objectContaining({ headers: expect.arrayContaining([ expect.objectContaining({ measureGroupHeader: { items: expect.arrayContaining([ expect.objectContaining({ measureHeaderItem: { format: expect.any(String), identifier: expect.any(String), localIdentifier: expect.any(String), name: expect.any(String), uri: expect.any(String), }, }), ]), }, }), ]), }), ]), links: { executionResult: expect.any(String), }, }, }), ); }); it("should return 404 on non existent execution", () => { const app = createEndpoint(executeAfm, schema); return request(app) .get("/gdc/app/projects/mockproject/executionResults/1234567") .send() .expect(404); }); it("should return correct result for empty data", async () => { const app = createEndpoint(executeAfm, schema); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttributeByTitle("No data") .addResultSpec({ dimensions: [ { itemIdentifiers: ["measureGroup"], }, { itemIdentifiers: ["a0"], }, ], }); const response = await pollAndGetResult(app, afmBuilder.build()); expect(response.status).toEqual(HttpStatusCodes.OK); expect(response.body).toEqual( expect.objectContaining({ executionResult: { data: [], // headers are still returned headerItems: expect.any(Array), paging: { // paging doesn't matter count: expect.any(Array), offset: expect.any(Array), total: expect.any(Array), }, }, }), ); }); describe("execute api contracts", () => { it("should allow multiple same concurrent executions", async () => { const app = createEndpoint(executeAfm, schema); const pollingResponse1 = await request(app) .post("/gdc/app/projects/mockproject/executeAfm") .send(customDimensionIdentifiers); const pollingResponse2 = await request(app) .post("/gdc/app/projects/mockproject/executeAfm") .send(customDimensionIdentifiers); const response1 = await request(app) .get(pollingResponse1.body.executionResponse.links.executionResult) .send(); const response2 = await request(app) .get(pollingResponse2.body.executionResponse.links.executionResult) .send(); expect(response1.status).toEqual(HttpStatusCodes.OK); expect(response2.status).toEqual(HttpStatusCodes.OK); }); }); describe("resultSpec", () => { it("should allow custom naming of dimensions", async () => { const app = createEndpoint(executeAfm, schema); const response = await request(app) .post("/gdc/app/projects/mockproject/executeAfm") .send(customDimensionIdentifiers); expect(response.body.executionResponse.dimensions).toMatchSnapshot(); }); it("should allow custom metric formats", async () => { const app = createEndpoint(executeAfm, schema); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure("#,##0.00", "Measure 1") .addMeasure("#,##0", "Measure 2"); const response = await request(app) .post("/gdc/app/projects/mockproject/executeAfm") .send(afmBuilder.build()); expect(response.body.executionResponse.dimensions).toMatchSnapshot(); }); }); describe("table totals", () => { const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addMeasure() .addAttribute() .addAttribute() .addResultSpec({ dimensions: [ { itemIdentifiers: ["a0", "a1"], }, { itemIdentifiers: ["measureGroup"], }, ], }) .addTotals(["med", "nat"]); it("should return proper response for totals", async () => { const app = createEndpoint(executeAfm, schema); const response = await request(app) .post("/gdc/app/projects/mockproject/executeAfm") .send(afmBuilder.build()); expect(response.status).toEqual(HttpStatusCodes.CREATED); expect(response.body.executionResponse.dimensions[0].headers[0].attributeHeader).toMatchObject({ totalItems: [{ totalHeaderItem: { name: "med" } }, { totalHeaderItem: { name: "nat" } }], }); }); it("should return 400 Bad Request when `nat` total AFM missing `nativeTotals` field", async () => { const app = createEndpoint(executeAfm, schema); const afm = afmBuilder.build(); delete afm.execution.afm.nativeTotals; const response = await request(app) .post("/gdc/app/projects/mockproject/executeAfm") .send(afm); expect(response.status).toEqual(HttpStatusCodes.BAD_REQUEST); }); it("should return proper data for totals", async () => { const app = createEndpoint(executeAfm, schema, { randomSeed: "this-is-seed" }); const response = await pollAndGetResult(app, afmBuilder.build()); expect(response.status).toEqual(HttpStatusCodes.OK); expect(response.body.executionResult).toMatchObject({ totals: [[["119", "518"], ["442", "525"]], []], }); }); }); describe("derived measures executions", () => { it("should consume PoP afm request", async () => { const app = createEndpoint(executeAfm, schema); const requestBody = { execution: { afm: { attributes: [ { displayForm: { identifier: "attr.date.year.df", }, localIdentifier: "yearCreatedAttribute", }, ], measures: [ { localIdentifier: "simplePopMeasure", definition: { popMeasure: { measureIdentifier: "simpleMeasure", popAttribute: { identifier: "attr.date.month", }, }, }, alias: "Simple measure previous year", }, { localIdentifier: "simpleMeasure", definition: { measure: { item: { identifier: "simple_metric", }, }, }, alias: "Simple measure this year", }, ], }, resultSpec: { dimensions: [ { itemIdentifiers: ["measureGroup"] }, { itemIdentifiers: ["yearCreatedAttribute"] }, ], }, }, }; const testResponse = { executionResult: { data: [["235"], ["514"]], headerItems: [ [ [ { measureHeaderItem: { name: "Simple measure previous year", order: 0, }, }, { measureHeaderItem: { name: "Simple measure this year", order: 1, }, }, ], ], [ [ { attributeHeaderItem: { uri: "/gdc/md/mockproject/obj/attr.date.year/elements?id=1", name: "2015", }, }, ], ], ], paging: { count: [2, 1], offset: [0, 0], total: [2, 1], }, }, }; const response = await pollAndGetResult(app, requestBody); expect(response.status).toEqual(HttpStatusCodes.OK); const { executionResult } = response.body; expect(executionResult.data.length).toEqual(2); expect(executionResult.headerItems).toEqual(testResponse.executionResult.headerItems); expect(executionResult.paging).toEqual(testResponse.executionResult.paging); }); it("should consume previous period afm request", async () => { const app = createEndpoint(executeAfm, schema); const requestBody = { execution: { afm: { attributes: [ { displayForm: { identifier: "attr.date.year.df", }, localIdentifier: "yearCreatedAttribute", }, ], measures: [ { localIdentifier: "simplePreviousPeriodMeasure", definition: { previousPeriodMeasure: { measureIdentifier: "simpleMeasure", dateDataSets: [ { dataSet: { identifier: "attr.date.month", }, periodsAgo: 1, }, ], }, }, alias: "Simple measure previous year", }, { localIdentifier: "simpleMeasure", definition: { measure: { item: { identifier: "simple_metric", }, }, }, alias: "Simple measure this year", }, ], }, resultSpec: { dimensions: [ { itemIdentifiers: ["measureGroup"] }, { itemIdentifiers: ["yearCreatedAttribute"] }, ], }, }, }; const testResponse = { executionResult: { data: [["235"], ["514"]], headerItems: [ [ [ { measureHeaderItem: { name: "Simple measure previous year", order: 0, }, }, { measureHeaderItem: { name: "Simple measure this year", order: 1, }, }, ], ], [ [ { attributeHeaderItem: { uri: "/gdc/md/mockproject/obj/attr.date.year/elements?id=1", name: "2015", }, }, ], ], ], paging: { count: [2, 1], offset: [0, 0], total: [2, 1], }, }, }; const response = await pollAndGetResult(app, requestBody); expect(response.status).toEqual(HttpStatusCodes.OK); const { executionResult } = response.body; expect(executionResult.data.length).toEqual(2); expect(executionResult.headerItems).toEqual(testResponse.executionResult.headerItems); expect(executionResult.paging).toEqual(testResponse.executionResult.paging); }); }); describe("arithmetic measures executions", () => { it("should consume request with arithmetic measure", async () => { const app = createEndpoint(executeAfm, schema, { randomSeed: "this-is-seed" }); const requestBody: AFM.IExecution = { execution: { afm: { attributes: [ { displayForm: { identifier: "attr.date.year.df", }, localIdentifier: "yearCreatedAttribute", }, ], measures: [ { localIdentifier: "simpleMeasure", definition: { measure: { item: { identifier: "simple_metric", }, }, }, alias: "Simple measure 1", }, { localIdentifier: "simpleMeasure2", definition: { measure: { item: { identifier: "simple_metric", }, }, }, alias: "Simple measure 2", }, { localIdentifier: "arithmeticMeasure", definition: { arithmeticMeasure: { measureIdentifiers: ["simpleMeasure", "simpleMeasure2"], operator: "sum", }, }, alias: "Arithmetic measure alias", }, ], }, resultSpec: { dimensions: [ { itemIdentifiers: ["measureGroup"] }, { itemIdentifiers: ["yearCreatedAttribute"] }, ], }, }, }; const expectedHeaders = [ [ [ { measureHeaderItem: { name: "Simple measure 1", order: 0, }, }, { measureHeaderItem: { name: "Simple measure 2", order: 1, }, }, { measureHeaderItem: { name: "Arithmetic measure alias", order: 2, }, }, ], ], [ [ { attributeHeaderItem: { uri: "/gdc/md/mockproject/obj/attr.date.year/elements?id=1", name: "2015", }, }, ], ], ]; const expectedData = [["668"], ["837"], ["234"]]; const expectedPaging = { count: [3, 1], offset: [0, 0], total: [3, 1], }; const response = await pollAndGetResult(app, requestBody); expect(response.status).toEqual(HttpStatusCodes.OK); const { executionResult } = response.body; expect(executionResult.headerItems).toEqual(expectedHeaders); expect(executionResult.data).toEqual(expectedData); expect(executionResult.paging).toEqual(expectedPaging); }); it("should consume request with arithmetic & derived measures", async () => { const app = createEndpoint(executeAfm, schema, { randomSeed: "this-is-seed" }); const expectedHeaders = [ [ [ { measureHeaderItem: { name: "Simple measure 1", order: 0, }, }, { measureHeaderItem: { name: "Simple measure 2", order: 1, }, }, { measureHeaderItem: { name: "Arithmetic measure alias - SP year ago", order: 2, }, }, { measureHeaderItem: { name: "Arithmetic measure alias", order: 3, }, }, ], ], [], ]; const expectedData = [["668"], ["837"], ["778"], ["234"]]; const expectedPaging = { count: [4], offset: [0], total: [4], }; const response = await pollAndGetResult(app, executionWithArithmeticAndDerivedMeasures); expect(response.status).toEqual(HttpStatusCodes.OK); const { executionResult } = response.body; expect(executionResult.headerItems).toEqual(expectedHeaders); expect(executionResult.data).toEqual(expectedData); expect(executionResult.paging).toEqual(expectedPaging); }); }); describe("saved executions", () => { it("should return status code, if it's result of saved execution", async () => { const app = createEndpoint(executeAfm, schema); const afmRequestBody = { execution: { afm: { measures: [ { localIdentifier: "asdf", definition: { measure: { item: { identifier: "too_large", }, }, }, }, ], }, }, }; const response = await pollAndGetResult(app, afmRequestBody); expect(response.status).toEqual(HttpStatusCodes.REQUEST_TOO_LONG); }); it("should return result of saved execution", async () => { const app = createEndpoint(executeAfm, schema); const requestBody = { execution: { afm: { measures: [ { definition: { measure: { item: { identifier: "savedm_1", }, }, }, alias: "Amount", localIdentifier: "measure1", format: "#,##0.00", }, ], }, resultSpec: { dimensions: [ { itemIdentifiers: ["measureGroup"], }, { itemIdentifiers: [], }, ], }, }, }; const executionResult = { executionResult: { data: ["116625456.54"], paging: { count: [1], offset: [1], total: [1], }, }, }; const response = await pollAndGetResult(app, requestBody); expect(response.status).toEqual(HttpStatusCodes.OK); expect(response.body).toEqual(executionResult); }); it("should ignore measure value filter and return result of filtered saved execution if measure value filter references local AFM measure", async () => { const app = createEndpoint(executeAfm, schema); const requestBody: AFM.IExecution = { execution: { afm: { measures: [ { localIdentifier: "613d56c3ab474d038329e5b067f1de69", definition: { measure: { item: { uri: "/gdc/md/mockproject/obj/cinemas", }, }, }, alias: "# Cinemas", }, ], attributes: [ { displayForm: { uri: "/gdc/md/mockproject/obj/attr.movie_genre.df", }, localIdentifier: "d0e108ae5b114fada742f5a494b962e8", }, ], filters: [ { measureValueFilter: { measure: { localIdentifier: "613d56c3ab474d038329e5b067f1de69", }, condition: { comparison: { operator: "GREATER_THAN", value: 500, }, }, }, }, ], }, resultSpec: { sorts: [ { attributeSortItem: { attributeIdentifier: "d0e108ae5b114fada742f5a494b962e8", direction: "asc", }, }, ], dimensions: [ { itemIdentifiers: ["d0e108ae5b114fada742f5a494b962e8"] }, { itemIdentifiers: ["measureGroup"] }, ], }, }, }; const executionResult = { executionResult: { data: [["505"], ["560"]], headerItems: [ [ [ { attributeHeaderItem: { uri: "/gdc/md/mockproject/obj/attr.movie_genre/elements?id=2", name: "Action", }, }, { attributeHeaderItem: { uri: "/gdc/md/mockproject/obj/attr.movie_genre/elements?id=1", name: "Comedy", }, }, ], ], [ [ { measureHeaderItem: { name: "# Cinemas", order: 0, }, }, ], ], ], paging: { count: [2, 1], offset: [0, 0], total: [2, 1], }, }, }; const response = await pollAndGetResult(app, requestBody); expect(response.status).toEqual(HttpStatusCodes.OK); expect(response.body).toEqual(executionResult); }); it("should not ignore measure value filter if it references non AFM measure if searching for stored executions and built ad hoc result if none is found", async () => { const app = createEndpoint(executeAfm, schema); const requestBody: AFM.IExecution = { execution: { afm: { measures: [ { localIdentifier: "613d56c3ab474d038329e5b067f1de69", definition: { measure: { item: { identifier: "4321", }, }, }, alias: "# Cinemas", }, ], attributes: [ { displayForm: { identifier: "2345.df", }, localIdentifier: "d0e108ae5b114fada742f5a494b962e8", }, ], filters: [ { measureValueFilter: { measure: { identifier: "savedm_1", }, condition: { comparison: { operator: "GREATER_THAN", value: 500, }, }, }, }, ], }, resultSpec: { sorts: [ { attributeSortItem: { attributeIdentifier: "d0e108ae5b114fada742f5a494b962e8", direction: "asc", }, }, ], dimensions: [ { itemIdentifiers: ["d0e108ae5b114fada742f5a494b962e8"] }, { itemIdentifiers: ["measureGroup"] }, ], }, }, }; const executionResultAttributeHeaders = [ { attributeHeaderItem: { name: "Country 1", uri: "/gdc/md/mockproject/obj/2345/elements?id=1", }, }, { attributeHeaderItem: { name: "Country 2", uri: "/gdc/md/mockproject/obj/2345/elements?id=2", }, }, { attributeHeaderItem: { name: "Country 3", uri: "/gdc/md/mockproject/obj/2345/elements?id=3", }, }, { attributeHeaderItem: { name: "Country 4", uri: "/gdc/md/mockproject/obj/2345/elements?id=4", }, }, ]; const response = await pollAndGetResult(app, requestBody); const executionResult = response.body as Execution.IExecutionResultWrapper; expect(response.status).toEqual(HttpStatusCodes.OK); expect(executionResult.executionResult.headerItems[0]).toContainEqual( executionResultAttributeHeaders, ); }); it("should not ignore measure value filter if it references non AFM measure and find stored execution with it", async () => { const app = createEndpoint(executeAfm, schema); const requestBody: AFM.IExecution = { execution: { afm: { measures: [ { localIdentifier: "1a4aec6ebcfa4f28a3eb8dcdd036aad8", definition: { measure: { item: { uri: "/gdc/md/mockproject/obj/viewers", }, }, }, alias: "# Viewers", }, ], attributes: [ { displayForm: { uri: "/gdc/md/mockproject/obj/attr.movie_genre.df", }, localIdentifier: "d0e108ae5b114fada742f5a494b962e8", }, ], filters: [ { measureValueFilter: { measure: { uri: "/gdc/md/mockproject/obj/cinemas", }, condition: { comparison: { operator: "GREATER_THAN", value: 500, }, }, }, }, ], }, resultSpec: { sorts: [ { attributeSortItem: { attributeIdentifier: "d0e108ae5b114fada742f5a494b962e8", direction: "asc", }, }, ], dimensions: [ { itemIdentifiers: ["d0e108ae5b114fada742f5a494b962e8"] }, { itemIdentifiers: ["measureGroup"] }, ], }, }, }; const expected = { executionResult: { data: [["150"], ["145"], ["203"], ["715"]], headerItems: [ [ [ { attributeHeaderItem: { uri: "/gdc/md/mockproject/obj/attr.movie_genre/elements?id=2", name: "Action", }, }, { attributeHeaderItem: { uri: "/gdc/md/mockproject/obj/attr.movie_genre/elements?id=3", name: "Adventure", }, }, { attributeHeaderItem: { uri: "/gdc/md/mockproject/obj/attr.movie_genre/elements?id=1", name: "Comedy", }, }, { attributeHeaderItem: { uri: "/gdc/md/mockproject/obj/attr.movie_genre/elements?id=4", name: "Sci-Fi", }, }, ], ], [ [ { measureHeaderItem: { name: "# Viewers", order: 0, }, }, ], ], ], paging: { count: [4, 1], offset: [0, 0], total: [4, 1], }, }, }; const response = await pollAndGetResult(app, requestBody); expect(response.status).toEqual(HttpStatusCodes.OK); expect(response.body).toEqual(expected); }); it("should ignore ranking filter and return result of filtered saved execution if ranking filter references local AFM measure", async () => { const app = createEndpoint(executeAfm, schema); const requestBody: AFM.IExecution = { execution: { afm: { measures: [ { localIdentifier: "613d56c3ab474d038329e5b067f1de69", definition: { measure: { item: { uri: "/gdc/md/mockproject/obj/cinemas", }, }, }, alias: "# Cinemas", }, ], attributes: [ { displayForm: { uri: "/gdc/md/mockproject/obj/attr.movie_genre.df", }, localIdentifier: "d0e108ae5b114fada742f5a494b962e8", }, ], filters: [ { rankingFilter: { measures: [ { localIdentifier: "613d56c3ab474d038329e5b067f1de69", }, ], operator: "TOP", value: 2, }, }, ], }, resultSpec: { sorts: [ { attributeSortItem: { attributeIdentifier: "d0e108ae5b114fada742f5a494b962e8", direction: "asc", }, }, ], dimensions: [ { itemIdentifiers: ["d0e108ae5b114fada742f5a494b962e8"] }, { itemIdentifiers: ["measureGroup"] }, ], }, }, }; const executionResult = { executionResult: { data: [["505"], ["560"]], headerItems: [ [ [ { attributeHeaderItem: { uri: "/gdc/md/mockproject/obj/attr.movie_genre/elements?id=2", name: "Action", }, }, { attributeHeaderItem: { uri: "/gdc/md/mockproject/obj/attr.movie_genre/elements?id=1", name: "Comedy", }, }, ], ], [ [ { measureHeaderItem: { name: "# Cinemas", order: 0, }, }, ], ], ], paging: { count: [2, 1], offset: [0, 0], total: [2, 1], }, }, }; const response = await pollAndGetResult(app, requestBody); expect(response.status).toEqual(HttpStatusCodes.OK); expect(response.body).toEqual(executionResult); }); it("should not ignore ranking filter if it references non AFM measure if searching for stored executions and built ad hoc result if none is found", async () => { const app = createEndpoint(executeAfm, schema); const requestBody: AFM.IExecution = { execution: { afm: { measures: [ { localIdentifier: "613d56c3ab474d038329e5b067f1de69", definition: { measure: { item: { identifier: "4321", }, }, }, alias: "# Cinemas", }, ], attributes: [ { displayForm: { identifier: "2345.df", }, localIdentifier: "d0e108ae5b114fada742f5a494b962e8", }, ], filters: [ { rankingFilter: { measures: [ { localIdentifier: "savedm_1", }, ], operator: "BOTTOM", value: 3, }, }, ], }, resultSpec: { sorts: [ { attributeSortItem: { attributeIdentifier: "d0e108ae5b114fada742f5a494b962e8", direction: "asc", }, }, ], dimensions: [ { itemIdentifiers: ["d0e108ae5b114fada742f5a494b962e8"] }, { itemIdentifiers: ["measureGroup"] }, ], }, }, }; const executionResultAttributeHeaders = [ { attributeHeaderItem: { name: "Country 1", uri: "/gdc/md/mockproject/obj/2345/elements?id=1", }, }, { attributeHeaderItem: { name: "Country 2", uri: "/gdc/md/mockproject/obj/2345/elements?id=2", }, }, { attributeHeaderItem: { name: "Country 3", uri: "/gdc/md/mockproject/obj/2345/elements?id=3", }, }, { attributeHeaderItem: { name: "Country 4", uri: "/gdc/md/mockproject/obj/2345/elements?id=4", }, }, ]; const response = await pollAndGetResult(app, requestBody); const executionResult = response.body as Execution.IExecutionResultWrapper; expect(response.status).toEqual(HttpStatusCodes.OK); expect(executionResult.executionResult.headerItems[0]).toContainEqual( executionResultAttributeHeaders, ); }); it("should return response of saved execution", async () => { const app = createEndpoint(executeAfm, schema); const requestBody = { execution: { afm: { measures: [ { definition: { measure: { item: { identifier: "savedm_1", }, }, }, alias: "Amount", localIdentifier: "measure1", }, ], attributes: [ { displayForm: { identifier: "employee", }, localIdentifier: "attribute1", }, ], }, resultSpec: { dimensions: [ { itemIdentifiers: ["attribute1"], }, { itemIdentifiers: ["measureGroup"], }, ], }, }, }; const executionResponse = { executionResponse: { dimensions: [ { headers: [ { attributeHeader: { name: "Employee", localIdentifier: "attribute1", uri: "/gdc/md/mockproject/obj/1027", identifier: "employee", formOf: { name: "Employee", uri: "/gdc/md/mockproject/obj/1026", identifier: "employee", }, }, }, ], }, { headers: [ { measureGroupHeader: { items: [ { measureHeaderItem: { format: "#,##0.00", identifier: "savedm_1", localIdentifier: "measure1", name: "Amount", uri: "/gdc/md/mockproject/obj/savedm_1", }, }, ], }, }, ], }, ], links: { executionResult: "/gdc/app/projects/mockproject/executionResults/f4a12b5e174abe402d2de394f99a8b?dimensions=2", }, }, }; const { response } = await pollAndGetResponseAndResult(app, requestBody); expect(response).toEqual(executionResponse); }); it("should return error response of saved execution", async () => { const app = createEndpoint(executeAfm, schema); const requestBody = { execution: { afm: { measures: [ { definition: { measure: { item: { identifier: "error_response", }, }, }, localIdentifier: "asdf", }, ], }, }, }; const { response } = await getResponse(app, requestBody); expect(response.status).toEqual(500); expect(response.text).toEqual("Mocked response Error"); }); }); describe("paging", () => { it("should allow row paging", async () => { const app = createEndpoint(executeAfm, schema, { randomSeed: "this-is-seed" }); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttributeBasedMeasure() .addAttribute() .addAttribute() .addSortByMeasure(0, "desc"); const response = await pollAndGetResult(app, afmBuilder.build(), "1,100"); expect(response.status).toEqual(200); expect(response.body).toEqual({ executionResult: { data: [["899", "841", "652", "608", "499", "458"]], headerItems: expect.anything(), paging: expect.anything(), }, }); }); it("should allow row limit", async () => { const app = createEndpoint(executeAfm, schema, { randomSeed: "this-is-seed" }); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttributeBasedMeasure() .addAttribute() .addAttribute() .addSortByMeasure(0, "desc"); const response = await pollAndGetResult(app, afmBuilder.build(), "100, 2"); expect(response.status).toEqual(200); expect(response.body).toEqual({ executionResult: { data: [["899", "841"], ["186", "844"]], headerItems: expect.anything(), paging: expect.anything(), }, }); }); it("should allow col limit", async () => { const app = createEndpoint(executeAfm, schema, { randomSeed: "this-is-seed" }); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttributeBasedMeasure() .addAttribute() .addAttribute() .addSortByMeasure(0, "desc"); const response = await pollAndGetResult(app, afmBuilder.build(), "100, 2"); expect(response.status).toEqual(200); expect(response.body).toEqual({ executionResult: { data: [["899", "841"], ["186", "844"]], headerItems: expect.anything(), paging: expect.anything(), }, }); }); it("should allow row offset", async () => { const app = createEndpoint(executeAfm, schema, { randomSeed: "this-is-seed" }); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttributeBasedMeasure() .addAttribute() .addAttribute() .addSortByMeasure(0, "desc"); const response = await pollAndGetResult(app, afmBuilder.build(), "100,100", "1,0"); /// expect(response.status).toEqual(200); expect(response.body).toEqual({ executionResult: { data: [["186", "844", "99", "317", "248", "792"]], headerItems: expect.anything(), paging: expect.anything(), }, }); }); it("should allow col offset", async () => { const app = createEndpoint(executeAfm, schema, { randomSeed: "this-is-seed" }); const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttributeBasedMeasure() .addAttribute() .addAttribute() .addSortByMeasure(0, "desc"); const response = await pollAndGetResult(app, afmBuilder.build(), "100,100", "0,3"); expect(response.status).toEqual(200); expect(response.body).toEqual({ executionResult: { data: [["608", "499", "458"], ["317", "248", "792"]], headerItems: expect.anything(), paging: expect.anything(), }, }); }); }); }); describe("getNormalizedExecution", () => { it("should set default resultSpec if resultSpec missing", () => { const afmBuilder = AFMBuilder.buildFromSchema(schema) .addMeasure() .addAttribute(); const execution = afmBuilder.build(); expect(getNormalizedExecution(execution)).toEqual({ execution: { afm: { attributes: [ { displayForm: { identifier: "attr.employee.df", }, localIdentifier: "a0", }, ], filters: [], measures: [ { definition: { measure: { computeRatio: undefined, item: { identifier: "simple_metric", }, }, }, localIdentifier: "m0", }, ], }, resultSpec: { dimensions: [ { itemIdentifiers: ["measureGroup"], }, { itemIdentifiers: ["a0"], }, ], }, }, }); }); }); describe("Response cache", () => { describe("cache cleaning", () => { // tslint:disable-next-line no-string-based-set-timeout const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); it("should clean old entries in cache after given time", async () => { addToCache("a", 111); addToCache("b", 222); await sleep(200); addToCache("c", 333); cleanOldCache(100); expect(hasCachedResult("a")).toBeFalsy(); expect(hasCachedResult("b")).toBeFalsy(); expect(hasCachedResult("c")).toBeTruthy(); expect(getFromCache("c")).toBe(333); }); it("should refresh cache record when added another time", async () => { addToCache("a", 111); addToCache("b", 222); await sleep(150); addToCache("a", 333); await sleep(150); cleanOldCache(200); expect(hasCachedResult("a")).toBeTruthy(); expect(hasCachedResult("b")).toBeFalsy(); }); }); }); describe("Validate AFM", () => { const ATTRIBUTE = { localIdentifier: "a", displayForm: { identifier: "a1", }, }; const MEASURE = { localIdentifier: "m", definition: { measure: { item: { identifier: "m1", }, }, }, }; const ATTRIBUTE_DIMENSION = { itemIdentifiers: ["a"] }; const MEASURE_GROUP_DIMENSION = { itemIdentifiers: ["measureGroup"] }; it("should return TRUE when resultSpec contains measure group and AFM contains measures", () => { expect( isExecutionValid( { attributes: [ATTRIBUTE], measures: [MEASURE], }, { dimensions: [ATTRIBUTE_DIMENSION, MEASURE_GROUP_DIMENSION], }, ), ).toEqual(true); }); it("should return FALSE when resultSpec contains measure group and AFM contains empty measures array", () => { expect( isExecutionValid( { attributes: [ATTRIBUTE], measures: [], }, { dimensions: [ATTRIBUTE_DIMENSION, MEASURE_GROUP_DIMENSION], }, ), ).toEqual(false); }); it("should return FALSE when resultSpec contains measure group and AFM does not contain measures", () => { expect( isExecutionValid( { attributes: [ATTRIBUTE], }, { dimensions: [ATTRIBUTE_DIMENSION, MEASURE_GROUP_DIMENSION], }, ), ).toEqual(false); }); it("should return TRUE when resultSpec does not contain measure group and AFM does not contain measures", () => { expect( isExecutionValid( { attributes: [ATTRIBUTE], }, { dimensions: [ATTRIBUTE_DIMENSION], }, ), ).toEqual(true); }); it("should return TRUE when resultSpec does not contain measure group and AFM contains measures", () => { expect( isExecutionValid( { attributes: [ATTRIBUTE], measures: [MEASURE], }, { dimensions: [ATTRIBUTE_DIMENSION], }, ), ).toEqual(true); }); }); describe("sanitize", () => { it("should fill out required properties that are not in AFM", () => { const sanitizedExecution = sanitize({ execution: { afm: {}, }, }); expect(sanitizedExecution).toEqual({ execution: { afm: { attributes: [], filters: [], measures: [], }, resultSpec: { dimensions: [], sorts: [], }, }, }); }); it("should not touch native totals", () => { const execution: AFM.IExecution = { execution: { afm: { attributes: [], filters: [], measures: [], nativeTotals: [ { measureIdentifier: "m1", attributeIdentifiers: ["a1"], }, ], }, resultSpec: { dimensions: [], sorts: [], }, }, }; const sanitizedExecution = sanitize(execution); expect(sanitizedExecution).toEqual(execution); }); });