diff --git a/packages/server/src/api/routes/tests/__snapshots__/view.spec.js.snap b/packages/server/src/api/routes/tests/__snapshots__/view.spec.js.snap deleted file mode 100644 index 357285a69a..0000000000 --- a/packages/server/src/api/routes/tests/__snapshots__/view.spec.js.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`/views query returns data for the created view 1`] = ` -[ - { - "avg": 2333.3333333333335, - "count": 3, - "group": null, - "max": 4000, - "min": 1000, - "sum": 7000, - "sumsqr": 21000000, - }, -] -`; - -exports[`/views query returns data for the created view using a group by 1`] = ` -[ - { - "avg": 1500, - "count": 2, - "group": "One", - "max": 2000, - "min": 1000, - "sum": 3000, - "sumsqr": 5000000, - }, - { - "avg": 4000, - "count": 1, - "group": "Two", - "max": 4000, - "min": 4000, - "sum": 4000, - "sumsqr": 16000000, - }, -] -`; diff --git a/packages/server/src/api/routes/tests/view.spec.js b/packages/server/src/api/routes/tests/view.spec.ts similarity index 59% rename from packages/server/src/api/routes/tests/view.spec.js rename to packages/server/src/api/routes/tests/view.spec.ts index 92ff097899..2e8c71b812 100644 --- a/packages/server/src/api/routes/tests/view.spec.js +++ b/packages/server/src/api/routes/tests/view.spec.ts @@ -1,30 +1,38 @@ -const setup = require("./utilities") -const { events } = require("@budibase/backend-core") +import { events } from "@budibase/backend-core" +import * as setup from "./utilities" +import { + FieldType, + INTERNAL_TABLE_SOURCE_ID, + SaveTableRequest, + Table, + TableSourceType, + View, + ViewCalculation, +} from "@budibase/types" -function priceTable() { - return { - name: "table", - type: "table", - key: "name", - schema: { - Price: { - type: "number", - constraints: {}, - }, - Category: { +const priceTable: SaveTableRequest = { + name: "table", + type: "table", + sourceId: INTERNAL_TABLE_SOURCE_ID, + sourceType: TableSourceType.INTERNAL, + schema: { + Price: { + name: "Price", + type: FieldType.NUMBER, + }, + Category: { + name: "Category", + type: FieldType.STRING, + constraints: { type: "string", - constraints: { - type: "string", - }, }, }, - } + }, } describe("/views", () => { - let request = setup.getRequest() let config = setup.getConfig() - let table + let table: Table afterAll(setup.afterAll) @@ -33,38 +41,34 @@ describe("/views", () => { }) beforeEach(async () => { - table = await config.createTable(priceTable()) + table = await config.api.table.save(priceTable) }) - const saveView = async view => { - const viewToSave = { + const saveView = async (view?: Partial) => { + const viewToSave: View = { name: "TestView", field: "Price", - calculation: "stats", - tableId: table._id, + calculation: ViewCalculation.STATISTICS, + tableId: table._id!, + filters: [], + schema: {}, ...view, } - return request - .post(`/api/views`) - .send(viewToSave) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) + return config.api.legacyView.save(viewToSave) } describe("create", () => { it("returns a success message when the view is successfully created", async () => { const res = await saveView() - expect(res.body.tableId).toBe(table._id) expect(events.view.created).toBeCalledTimes(1) }) it("creates a view with a calculation", async () => { jest.clearAllMocks() - const res = await saveView({ calculation: "count" }) + const view = await saveView({ calculation: ViewCalculation.COUNT }) - expect(res.body.tableId).toBe(table._id) + expect(view.tableId).toBe(table._id) expect(events.view.created).toBeCalledTimes(1) expect(events.view.updated).not.toBeCalled() expect(events.view.calculationCreated).toBeCalledTimes(1) @@ -78,8 +82,8 @@ describe("/views", () => { it("creates a view with a filter", async () => { jest.clearAllMocks() - const res = await saveView({ - calculation: null, + const view = await saveView({ + calculation: undefined, filters: [ { value: "1", @@ -89,7 +93,7 @@ describe("/views", () => { ], }) - expect(res.body.tableId).toBe(table._id) + expect(view.tableId).toBe(table._id) expect(events.view.created).toBeCalledTimes(1) expect(events.view.updated).not.toBeCalled() expect(events.view.calculationCreated).not.toBeCalled() @@ -101,52 +105,41 @@ describe("/views", () => { }) it("updates the table row with the new view metadata", async () => { - const res = await request - .post(`/api/views`) - .send({ - name: "TestView", - field: "Price", - calculation: "stats", - tableId: table._id, + await saveView() + const updatedTable = await config.api.table.get(table._id!) + expect(updatedTable.views).toEqual( + expect.objectContaining({ + TestView: expect.objectContaining({ + field: "Price", + calculation: "stats", + tableId: table._id, + filters: [], + schema: { + sum: { + type: "number", + }, + min: { + type: "number", + }, + max: { + type: "number", + }, + count: { + type: "number", + }, + sumsqr: { + type: "number", + }, + avg: { + type: "number", + }, + field: { + type: "string", + }, + }, + }), }) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(res.body.tableId).toBe(table._id) - - const updatedTable = await config.getTable(table._id) - const expectedObj = expect.objectContaining({ - TestView: expect.objectContaining({ - field: "Price", - calculation: "stats", - tableId: table._id, - filters: [], - schema: { - sum: { - type: "number", - }, - min: { - type: "number", - }, - max: { - type: "number", - }, - count: { - type: "number", - }, - sumsqr: { - type: "number", - }, - avg: { - type: "number", - }, - field: { - type: "string", - }, - }, - }), - }) - expect(updatedTable.views).toEqual(expectedObj) + ) }) }) @@ -168,10 +161,10 @@ describe("/views", () => { }) it("updates a view calculation", async () => { - await saveView({ calculation: "sum" }) + await saveView({ calculation: ViewCalculation.SUM }) jest.clearAllMocks() - await saveView({ calculation: "count" }) + await saveView({ calculation: ViewCalculation.COUNT }) expect(events.view.created).not.toBeCalled() expect(events.view.updated).toBeCalledTimes(1) @@ -184,10 +177,10 @@ describe("/views", () => { }) it("deletes a view calculation", async () => { - await saveView({ calculation: "sum" }) + await saveView({ calculation: ViewCalculation.SUM }) jest.clearAllMocks() - await saveView({ calculation: null }) + await saveView({ calculation: undefined }) expect(events.view.created).not.toBeCalled() expect(events.view.updated).toBeCalledTimes(1) @@ -258,100 +251,98 @@ describe("/views", () => { describe("fetch", () => { beforeEach(async () => { - table = await config.createTable(priceTable()) + table = await config.api.table.save(priceTable) }) it("returns only custom views", async () => { - await config.createLegacyView({ + await saveView({ name: "TestView", field: "Price", - calculation: "stats", + calculation: ViewCalculation.STATISTICS, tableId: table._id, }) - const res = await request - .get(`/api/views`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(res.body.length).toBe(1) - expect(res.body.find(({ name }) => name === "TestView")).toBeDefined() + const views = await config.api.legacyView.fetch() + expect(views.length).toBe(1) + expect(views.find(({ name }) => name === "TestView")).toBeDefined() }) }) describe("query", () => { it("returns data for the created view", async () => { - await config.createLegacyView({ + await saveView({ name: "TestView", field: "Price", - calculation: "stats", - tableId: table._id, + calculation: ViewCalculation.STATISTICS, + tableId: table._id!, }) - await config.createRow({ - tableId: table._id, + await config.api.row.save(table._id!, { Price: 1000, }) - await config.createRow({ - tableId: table._id, + await config.api.row.save(table._id!, { Price: 2000, }) - await config.createRow({ - tableId: table._id, + await config.api.row.save(table._id!, { Price: 4000, }) - const res = await request - .get(`/api/views/TestView?calculation=stats`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(res.body.length).toBe(1) - expect(res.body).toMatchSnapshot() + const rows = await config.api.legacyView.get("TestView", { + calculation: ViewCalculation.STATISTICS, + }) + expect(rows.length).toBe(1) + expect(rows[0]).toEqual({ + avg: 2333.3333333333335, + count: 3, + group: null, + max: 4000, + min: 1000, + sum: 7000, + sumsqr: 21000000, + }) }) it("returns data for the created view using a group by", async () => { - await config.createLegacyView({ - calculation: "stats", + await saveView({ + calculation: ViewCalculation.STATISTICS, name: "TestView", field: "Price", groupBy: "Category", tableId: table._id, }) - await config.createRow({ - tableId: table._id, + await config.api.row.save(table._id!, { Price: 1000, Category: "One", }) - await config.createRow({ - tableId: table._id, + await config.api.row.save(table._id!, { Price: 2000, Category: "One", }) - await config.createRow({ - tableId: table._id, + await config.api.row.save(table._id!, { Price: 4000, Category: "Two", }) - const res = await request - .get(`/api/views/TestView?calculation=stats&group=Category`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - - expect(res.body.length).toBe(2) - expect(res.body).toMatchSnapshot() + const rows = await config.api.legacyView.get("TestView", { + calculation: ViewCalculation.STATISTICS, + group: "Category", + }) + expect(rows.length).toBe(2) + expect(rows[0]).toEqual({ + avg: 1500, + count: 2, + group: "One", + max: 2000, + min: 1000, + sum: 3000, + sumsqr: 5000000, + }) }) }) describe("destroy", () => { it("should be able to delete a view", async () => { - const table = await config.createTable(priceTable()) - const view = await config.createLegacyView() - const res = await request - .delete(`/api/views/${view.name}`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(res.body.map).toBeDefined() - expect(res.body.meta.tableId).toEqual(table._id) + const table = await config.api.table.save(priceTable) + const view = await saveView({ tableId: table._id }) + const deletedView = await config.api.legacyView.destroy(view.name!) + expect(deletedView.map).toBeDefined() + expect(deletedView.meta?.tableId).toEqual(table._id) expect(events.view.deleted).toBeCalledTimes(1) }) }) @@ -362,33 +353,44 @@ describe("/views", () => { }) const setupExport = async () => { - const table = await config.createTable() - await config.createRow({ name: "test-name", description: "ùúûü" }) + const table = await config.api.table.save({ + name: "test-table", + type: "table", + sourceId: INTERNAL_TABLE_SOURCE_ID, + sourceType: TableSourceType.INTERNAL, + schema: { + name: { + name: "name", + type: FieldType.STRING, + }, + description: { + name: "description", + type: FieldType.STRING, + }, + }, + }) + await config.api.row.save(table._id!, { + name: "test-name", + description: "ùúûü", + }) return table } - const exportView = async (viewName, format) => { - return request - .get(`/api/views/export?view=${viewName}&format=${format}`) - .set(config.defaultHeaders()) - .expect(200) - } - - const assertJsonExport = res => { - const rows = JSON.parse(res.text) + const assertJsonExport = (res: string) => { + const rows = JSON.parse(res) expect(rows.length).toBe(1) expect(rows[0].name).toBe("test-name") expect(rows[0].description).toBe("ùúûü") } - const assertCSVExport = res => { - expect(res.text).toBe(`"name","description"\n"test-name","ùúûü"`) + const assertCSVExport = (res: string) => { + expect(res).toBe(`"name","description"\n"test-name","ùúûü"`) } it("should be able to export a table as JSON", async () => { const table = await setupExport() - const res = await exportView(table._id, "json") + const res = await config.api.legacyView.export(table._id!, "json") assertJsonExport(res) expect(events.table.exported).toBeCalledTimes(1) @@ -398,7 +400,7 @@ describe("/views", () => { it("should be able to export a table as CSV", async () => { const table = await setupExport() - const res = await exportView(table._id, "csv") + const res = await config.api.legacyView.export(table._id!, "csv") assertCSVExport(res) expect(events.table.exported).toBeCalledTimes(1) @@ -407,10 +409,15 @@ describe("/views", () => { it("should be able to export a view as JSON", async () => { let table = await setupExport() - const view = await config.createLegacyView() - table = await config.getTable(table._id) + const view = await config.api.legacyView.save({ + name: "test-view", + tableId: table._id!, + filters: [], + schema: {}, + }) + table = await config.api.table.get(table._id!) - let res = await exportView(view.name, "json") + let res = await config.api.legacyView.export(view.name!, "json") assertJsonExport(res) expect(events.view.exported).toBeCalledTimes(1) @@ -419,10 +426,15 @@ describe("/views", () => { it("should be able to export a view as CSV", async () => { let table = await setupExport() - const view = await config.createLegacyView() - table = await config.getTable(table._id) + const view = await config.api.legacyView.save({ + name: "test-view", + tableId: table._id!, + filters: [], + schema: {}, + }) + table = await config.api.table.get(table._id!) - let res = await exportView(view.name, "csv") + let res = await config.api.legacyView.export(view.name!, "csv") assertCSVExport(res) expect(events.view.exported).toBeCalledTimes(1) diff --git a/packages/server/src/tests/utilities/api/legacyView.ts b/packages/server/src/tests/utilities/api/legacyView.ts index 38ef70d62a..19a6eceb3b 100644 --- a/packages/server/src/tests/utilities/api/legacyView.ts +++ b/packages/server/src/tests/utilities/api/legacyView.ts @@ -1,8 +1,36 @@ import { Expectations, TestAPI } from "./base" -import { Row } from "@budibase/types" +import { Row, View, ViewCalculation } from "@budibase/types" export class LegacyViewAPI extends TestAPI { - get = async (id: string, expectations?: Expectations) => { - return await this._get(`/api/views/${id}`, { expectations }) + get = async ( + id: string, + query: { calculation: ViewCalculation; group?: string }, + expectations?: Expectations + ) => { + return await this._get(`/api/views/${id}`, { query, expectations }) + } + + save = async (body: View, expectations?: Expectations) => { + return await this._post(`/api/views/`, { body, expectations }) + } + + fetch = async (expectations?: Expectations) => { + return await this._get(`/api/views`, { expectations }) + } + + destroy = async (id: string, expectations?: Expectations) => { + return await this._delete(`/api/views/${id}`, { expectations }) + } + + export = async ( + viewName: string, + format: "json" | "csv" | "jsonWithSchema", + expectations?: Expectations + ) => { + const response = await this._requestRaw("get", `/api/views/export`, { + query: { view: viewName, format }, + expectations, + }) + return response.text } } diff --git a/packages/types/src/documents/app/view.ts b/packages/types/src/documents/app/view.ts index b5a22ec592..7b93d24f3d 100644 --- a/packages/types/src/documents/app/view.ts +++ b/packages/types/src/documents/app/view.ts @@ -30,6 +30,7 @@ export interface View { map?: string reduce?: any meta?: ViewTemplateOpts + groupBy?: string } export interface ViewV2 {