From 06d70266cdbd0936b17274f583d6753bd01a692b Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 11 Apr 2022 22:32:12 +0100 Subject: [PATCH] View/Table/Row Import/Export events --- packages/backend-core/src/events/constants.js | 1 + .../backend-core/src/events/handlers/index.js | 4 +- .../backend-core/src/events/handlers/row.js | 11 ++- .../backend-core/src/events/handlers/table.js | 14 +-- .../backend-core/src/events/handlers/view.js | 5 +- .../src/tests/utilities/mocks/events.js | 8 +- .../src/api/controllers/table/external.js | 2 + .../server/src/api/controllers/table/index.js | 5 + .../server/src/api/controllers/table/utils.ts | 8 +- .../server/src/api/controllers/view/index.js | 12 ++- .../server/src/api/routes/tests/table.spec.js | 75 +++++++++++--- .../server/src/api/routes/tests/view.spec.js | 97 ++++++++++++++----- 12 files changed, 180 insertions(+), 62 deletions(-) diff --git a/packages/backend-core/src/events/constants.js b/packages/backend-core/src/events/constants.js index 7ea68c5a98..354f92f56b 100644 --- a/packages/backend-core/src/events/constants.js +++ b/packages/backend-core/src/events/constants.js @@ -103,6 +103,7 @@ exports.Events = { // ROW // ROW_CREATED: "row:created", + ROW_IMPORT: "row:import", // COMPONENT COMPONENT_CREATED: "component:created", diff --git a/packages/backend-core/src/events/handlers/index.js b/packages/backend-core/src/events/handlers/index.js index c9007ae28f..a1b92b0332 100644 --- a/packages/backend-core/src/events/handlers/index.js +++ b/packages/backend-core/src/events/handlers/index.js @@ -9,7 +9,8 @@ const layout = require("./layout") const org = require("./org") const query = require("./query") const role = require("./role") -const row = require("./screen") +const screen = require("./screen") +const row = require("./row") const table = require("./table") const serve = require("./serve") const user = require("./user") @@ -27,6 +28,7 @@ module.exports = { org, query, role, + screen, row, table, serve, diff --git a/packages/backend-core/src/events/handlers/row.js b/packages/backend-core/src/events/handlers/row.js index 07c18e241f..28cb45645a 100644 --- a/packages/backend-core/src/events/handlers/row.js +++ b/packages/backend-core/src/events/handlers/row.js @@ -1,7 +1,14 @@ -// const events = require("../events") -// const { Events } = require("../constants") +const events = require("../events") +const { Events } = require("../constants") + +/* eslint-disable */ // exports.created = () => { // const properties = {} // events.processEvent(Events.ROW_CREATED, properties) // } + +exports.import = (table, format, count) => { + const properties = {} + events.processEvent(Events.ROW_IMPORT, properties) +} diff --git a/packages/backend-core/src/events/handlers/table.js b/packages/backend-core/src/events/handlers/table.js index 891701ef7d..e212faad15 100644 --- a/packages/backend-core/src/events/handlers/table.js +++ b/packages/backend-core/src/events/handlers/table.js @@ -1,29 +1,29 @@ const events = require("../events") const { Events } = require("../constants") -exports.created = () => { +/* eslint-disable */ + +exports.created = table => { const properties = {} events.processEvent(Events.TABLE_CREATED, properties) } -exports.updated = () => { +exports.updated = table => { const properties = {} events.processEvent(Events.TABLE_UPDATED, properties) } -exports.deleted = () => { +exports.deleted = table => { const properties = {} events.processEvent(Events.TABLE_DELETED, properties) } -// TODO -exports.exported = () => { +exports.exported = (table, format) => { const properties = {} events.processEvent(Events.TABLE_EXPORTED, properties) } -// TODO -exports.imported = () => { +exports.imported = (table, format) => { const properties = {} events.processEvent(Events.TABLE_IMPORTED, properties) } diff --git a/packages/backend-core/src/events/handlers/view.js b/packages/backend-core/src/events/handlers/view.js index 551960fcb0..785eaa0434 100644 --- a/packages/backend-core/src/events/handlers/view.js +++ b/packages/backend-core/src/events/handlers/view.js @@ -1,6 +1,8 @@ const events = require("../events") const { Events } = require("../constants") +/* eslint-disable */ + exports.created = () => { const properties = {} events.processEvent(Events.VIEW_CREATED, properties) @@ -18,8 +20,7 @@ exports.deleted = () => { events.processEvent(Events.VIEW_DELETED, properties) } -// TODO -exports.exported = () => { +exports.exported = (table, format) => { const properties = {} events.processEvent(Events.VIEW_EXPORTED, properties) } diff --git a/packages/backend-core/src/tests/utilities/mocks/events.js b/packages/backend-core/src/tests/utilities/mocks/events.js index 1176bbb3ee..1d0fdc3f8c 100644 --- a/packages/backend-core/src/tests/utilities/mocks/events.js +++ b/packages/backend-core/src/tests/utilities/mocks/events.js @@ -69,7 +69,9 @@ jest.mock("../../../events", () => { assigned: jest.fn(), unassigned: jest.fn(), }, - row: {}, + row: { + import: jest.fn(), + }, screen: { created: jest.fn(), deleted: jest.fn(), @@ -103,7 +105,9 @@ jest.mock("../../../events", () => { imported: jest.fn(), permissionUpdated: jest.fn(), }, - view: {}, + view: { + exported: jest.fn(), + }, } }) diff --git a/packages/server/src/api/controllers/table/external.js b/packages/server/src/api/controllers/table/external.js index 86d855a28e..f61a31dac1 100644 --- a/packages/server/src/api/controllers/table/external.js +++ b/packages/server/src/api/controllers/table/external.js @@ -19,6 +19,7 @@ const { cloneDeep } = require("lodash/fp") const csvParser = require("../../../utilities/csvParser") const { handleRequest } = require("../row/external") const { getAppDB } = require("@budibase/backend-core/context") +const { events } = require("@budibase/backend-core") async function makeTableRequest( datasource, @@ -314,5 +315,6 @@ exports.bulkImport = async function (ctx) { await handleRequest(DataSourceOperation.BULK_CREATE, table._id, { rows, }) + events.row.import(table, "csv", rows.length) return table } diff --git a/packages/server/src/api/controllers/table/index.js b/packages/server/src/api/controllers/table/index.js index 1f88172d0a..701b805b50 100644 --- a/packages/server/src/api/controllers/table/index.js +++ b/packages/server/src/api/controllers/table/index.js @@ -56,12 +56,17 @@ exports.find = async function (ctx) { exports.save = async function (ctx) { const appId = ctx.appId const table = ctx.request.body + const importFormat = + table.dataImport && table.dataImport.csvString ? "csv" : undefined const savedTable = await pickApi({ table }).save(ctx) if (!table._id) { events.table.created(savedTable) } else { events.table.updated(savedTable) } + if (importFormat) { + events.table.imported(savedTable, importFormat) + } ctx.status = 200 ctx.message = `Table ${table.name} saved successfully.` ctx.eventEmitter && diff --git a/packages/server/src/api/controllers/table/utils.ts b/packages/server/src/api/controllers/table/utils.ts index 20bd3cb090..d72c8788fc 100644 --- a/packages/server/src/api/controllers/table/utils.ts +++ b/packages/server/src/api/controllers/table/utils.ts @@ -26,7 +26,8 @@ import { getViews, saveView } from "../view/utils" import viewTemplate from "../view/viewBuilder" const { getAppDB } = require("@budibase/backend-core/context") import { cloneDeep } from "lodash/fp" -import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro" +import { quotas } from "@budibase/pro" +import { events } from "@budibase/backend-core" export async function clearColumns(table: any, columnNames: any) { const db = getAppDB() @@ -148,8 +149,7 @@ export async function handleDataImport(user: any, table: any, dataImport: any) { } await quotas.addRows(finalData.length, () => db.bulkDocs(finalData)) - let response = await db.put(table) - table._rev = response._rev + events.row.import(table, "csv", finalData.length) return table } @@ -245,7 +245,7 @@ class TableSaveFunctions { // after saving async after(table: any) { table = await handleSearchIndexes(table) - table = await handleDataImport(this.user, table, this.dataImport) + await handleDataImport(this.user, table, this.dataImport) return table } diff --git a/packages/server/src/api/controllers/view/index.js b/packages/server/src/api/controllers/view/index.js index fd6b32f3d6..1174abc904 100644 --- a/packages/server/src/api/controllers/view/index.js +++ b/packages/server/src/api/controllers/view/index.js @@ -6,6 +6,8 @@ const { fetchView } = require("../row") const { getTable } = require("../table/utils") const { FieldTypes } = require("../../../constants") const { getAppDB } = require("@budibase/backend-core/context") +const { events } = require("@budibase/backend-core") +const { DocumentTypes } = require("../../../db/utils") exports.fetch = async ctx => { ctx.body = await getViews() @@ -79,9 +81,9 @@ exports.exportView = async ctx => { let rows = ctx.body let schema = view && view.meta && view.meta.schema + const tableId = ctx.params.tableId || view.meta.tableId + const table = await getTable(tableId) if (!schema) { - const tableId = ctx.params.tableId || view.meta.tableId - const table = await getTable(tableId) schema = table.schema } @@ -116,4 +118,10 @@ exports.exportView = async ctx => { // send down the file ctx.attachment(filename) ctx.body = apiFileReturn(exporter(headers, rows)) + + if (viewName.startsWith(DocumentTypes.TABLE)) { + events.table.exported(table, format) + } else { + events.view.exported(table, format) + } } diff --git a/packages/server/src/api/routes/tests/table.spec.js b/packages/server/src/api/routes/tests/table.spec.js index a7d6f3037d..293b37c7b9 100644 --- a/packages/server/src/api/routes/tests/table.spec.js +++ b/packages/server/src/api/routes/tests/table.spec.js @@ -14,37 +14,56 @@ describe("/tables", () => { }) describe("create", () => { - it("returns a success message when the table is successfully created", async () => { - const res = await request + + beforeEach(() => { + jest.clearAllMocks() + }) + + const createTable = (table) => { + if (!table) { + table = basicTable() + } + return request .post(`/api/tables`) - .send({ - name: "TestTable", - key: "name", - schema: { - name: {type: "string"} - } - }) + .send(table) .set(config.defaultHeaders()) .expect('Content-Type', /json/) .expect(200) + + } + + it("returns a success message when the table is successfully created", async () => { + const res = await createTable() + expect(res.res.statusMessage).toEqual("Table TestTable saved successfully.") expect(res.body.name).toEqual("TestTable") expect(events.table.created).toBeCalledTimes(1) expect(events.table.created).toBeCalledWith(res.body) }) + it("creates a table via data import CSV", async () => { + const table = basicTable() + table.dataImport = { + csvString: "\"name\",\"description\"\n\"test-name\",\"test-desc\"", + } + table.dataImport.schema = table.schema + + const res = await createTable(table) + + expect(events.table.created).toBeCalledTimes(1) + expect(events.table.created).toBeCalledWith(res.body) + expect(events.table.imported).toBeCalledTimes(1) + expect(events.table.imported).toBeCalledWith(res.body, "csv") + expect(events.row.import).toBeCalledTimes(1) + expect(events.row.import).toBeCalledWith(res.body, "csv", 1) + }) + it("should apply authorization to endpoint", async () => { await checkBuilderEndpoint({ config, method: "POST", url: `/api/tables`, - body: { - name: "TestTable", - key: "name", - schema: { - name: {type: "string"} - } - } + body: basicTable() }) }) }) @@ -124,6 +143,30 @@ describe("/tables", () => { }) }) + describe("import", () => { + it("imports rows successfully", async () => { + const table = await config.createTable() + const importRequest = { + dataImport: { + csvString: "\"name\",\"description\"\n\"test-name\",\"test-desc\"", + schema: table.schema + } + } + jest.clearAllMocks() + + await request + .post(`/api/tables/${table._id}/import`) + .send(importRequest) + .set(config.defaultHeaders()) + .expect('Content-Type', /json/) + .expect(200) + + expect(events.table.created).not.toHaveBeenCalled() + expect(events.row.import).toBeCalledTimes(1) + expect(events.row.import).toBeCalledWith(table, "csv", 1) + }) + }) + describe("fetch", () => { let testTable diff --git a/packages/server/src/api/routes/tests/view.spec.js b/packages/server/src/api/routes/tests/view.spec.js index b1c5f655c6..cf5c81b0f1 100644 --- a/packages/server/src/api/routes/tests/view.spec.js +++ b/packages/server/src/api/routes/tests/view.spec.js @@ -1,4 +1,5 @@ const setup = require("./utilities") +const { events } = require("@budibase/backend-core") function priceTable() { return { @@ -205,33 +206,77 @@ describe("/views", () => { }) describe("exportView", () => { - it("should be able to export a view", async () => { - await config.createTable(priceTable()) - await config.createRow() + + beforeEach(() => { + jest.clearAllMocks() + }) + + const setupExport = async () => { + const table = await config.createTable() + await config.createRow({ name: "test-name", description: "test-desc" }) + 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) + expect(rows.length).toBe(1) + expect(rows[0].name).toBe("test-name") + expect(rows[0].description).toBe("test-desc") + } + + const assertCSVExport = (res) => { + expect(res.text).toBe("\"name\",\"description\"\n\"test-name\",\"test-desc\"") + } + + it("should be able to export a table as JSON", async () => { + const table = await setupExport() + + const res = await exportView(table._id, "json") + + assertJsonExport(res) + expect(events.table.exported).toBeCalledTimes(1) + expect(events.table.exported).toBeCalledWith(table, "json") + }) + + it("should be able to export a table as CSV", async () => { + const table = await setupExport() + + const res = await exportView(table._id, "csv") + + assertCSVExport(res) + expect(events.table.exported).toBeCalledTimes(1) + expect(events.table.exported).toBeCalledWith(table, "csv") + }) + + it("should be able to export a view as JSON", async () => { + let table = await setupExport() const view = await config.createView() - let res = await request - .get(`/api/views/export?view=${view.name}&format=json`) - .set(config.defaultHeaders()) - .expect(200) - let error - try { - const obj = JSON.parse(res.text) - expect(obj.length).toBe(1) - } catch (err) { - error = err - } - expect(error).toBeUndefined() - res = await request - .get(`/api/views/export?view=${view.name}&format=csv`) - .set(config.defaultHeaders()) - .expect(200) - // this shouldn't be JSON - try { - JSON.parse(res.text) - } catch (err) { - error = err - } - expect(error).toBeDefined() + table = await config.getTable(table._id) + + let res = await exportView(view.name, "json") + + assertJsonExport(res) + expect(events.view.exported).toBeCalledTimes(1) + expect(events.view.exported).toBeCalledWith(table, "json") + }) + + it("should be able to export a view as CSV", async () => { + let table = await setupExport() + const view = await config.createView() + table = await config.getTable(table._id) + + let res = await exportView(view.name, "csv") + + assertCSVExport(res) + expect(events.view.exported).toBeCalledTimes(1) + expect(events.view.exported).toBeCalledWith(table, "csv") }) }) })