diff --git a/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte b/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte index c2cda1f2d8..f2c726c8bf 100644 --- a/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte +++ b/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte @@ -28,7 +28,6 @@ let deleteTableName $: externalTable = table?.sourceType === DB_TYPE_EXTERNAL - $: allowDeletion = !externalTable || table?.created function showDeleteModal() { templateScreens = $screenStore.screens.filter( @@ -56,7 +55,7 @@ $goto(`./datasource/${table.datasourceId}`) } } catch (error) { - notifications.error("Error deleting table") + notifications.error(`Error deleting table - ${error.message}`) } } @@ -86,17 +85,15 @@ } -{#if allowDeletion} - -
- -
- {#if !externalTable} - Edit - {/if} - Delete -
-{/if} + +
+ +
+ {#if !externalTable} + Edit + {/if} + Delete +
import { getContext } from "svelte" + import { get } from "svelte/store" import { generate } from "shortid" import Block from "components/Block.svelte" import BlockComponent from "components/BlockComponent.svelte" @@ -33,8 +34,9 @@ export let sidePanelDeleteLabel export let notificationOverride - const { fetchDatasourceSchema, API } = getContext("sdk") + const { fetchDatasourceSchema, API, generateGoldenSample } = getContext("sdk") const component = getContext("component") + const context = getContext("context") const stateKey = `ID_${generate()}` let formId @@ -48,20 +50,6 @@ let schemaLoaded = false $: deleteLabel = setDeleteLabel(sidePanelDeleteLabel, sidePanelShowDelete) - - const setDeleteLabel = sidePanelDeleteLabel => { - // Accommodate old config to ensure delete button does not reappear - let labelText = sidePanelShowDelete === false ? "" : sidePanelDeleteLabel - - // Empty text is considered hidden. - if (labelText?.trim() === "") { - return "" - } - - // Default to "Delete" if the value is unset - return labelText || "Delete" - } - $: isDSPlus = dataSource?.type === "table" || dataSource?.type === "viewV2" $: fetchSchema(dataSource) $: enrichSearchColumns(searchColumns, schema).then( @@ -105,6 +93,30 @@ }, ] + // Provide additional data context for live binding eval + export const getAdditionalDataContext = () => { + const rows = get(context)[dataProviderId]?.rows + const goldenRow = generateGoldenSample(rows) + return { + eventContext: { + row: goldenRow, + }, + } + } + + const setDeleteLabel = sidePanelDeleteLabel => { + // Accommodate old config to ensure delete button does not reappear + let labelText = sidePanelShowDelete === false ? "" : sidePanelDeleteLabel + + // Empty text is considered hidden. + if (labelText?.trim() === "") { + return "" + } + + // Default to "Delete" if the value is unset + return labelText || "Delete" + } + // Load the datasource schema so we can determine column types const fetchSchema = async dataSource => { if (dataSource?.type === "table") { diff --git a/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte b/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte index 5e3a035d89..ed09301bb9 100644 --- a/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte +++ b/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte @@ -40,16 +40,18 @@ } } + // Handle certain key presses regardless of selection state + if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && $config.canAddRows) { + e.preventDefault() + dispatch("add-row-inline") + return + } + // If nothing selected avoid processing further key presses if (!$focusedCellId) { if (e.key === "Tab" || e.key?.startsWith("Arrow")) { e.preventDefault() focusFirstCell() - } else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { - if ($config.canAddRows) { - e.preventDefault() - dispatch("add-row-inline") - } } else if (e.key === "Delete" || e.key === "Backspace") { if (Object.keys($selectedRows).length && $config.canDeleteRows) { dispatch("request-bulk-delete") diff --git a/packages/server/__mocks__/mysql2.ts b/packages/server/__mocks__/mysql2.ts deleted file mode 100644 index 67ff897811..0000000000 --- a/packages/server/__mocks__/mysql2.ts +++ /dev/null @@ -1,11 +0,0 @@ -const client = { - connect: jest.fn(), - query: jest.fn((query, bindings, fn) => { - fn(null, []) - }), -} - -module.exports = { - createConnection: jest.fn(() => client), - client, -} diff --git a/packages/server/__mocks__/mysql2/promise.ts b/packages/server/__mocks__/mysql2/promise.ts deleted file mode 100644 index f8a4c7b2d6..0000000000 --- a/packages/server/__mocks__/mysql2/promise.ts +++ /dev/null @@ -1,18 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -module MySQLMock { - const mysql: any = {} - - const client = { - connect: jest.fn(), - end: jest.fn(), - query: jest.fn(async () => { - return [[]] - }), - } - - mysql.createConnection = jest.fn(async () => { - return client - }) - - module.exports = mysql -} diff --git a/packages/server/src/api/controllers/table/external.ts b/packages/server/src/api/controllers/table/external.ts index c85b46a95c..7c036bec9d 100644 --- a/packages/server/src/api/controllers/table/external.ts +++ b/packages/server/src/api/controllers/table/external.ts @@ -61,9 +61,6 @@ export async function destroy(ctx: UserCtx) { const tableToDelete: TableRequest = await sdk.tables.getTable( ctx.params.tableId ) - if (!tableToDelete || !tableToDelete.created) { - ctx.throw(400, "Cannot delete tables which weren't created in Budibase.") - } const datasourceId = getDatasourceId(tableToDelete) try { const { datasource, table } = await sdk.tables.external.destroy( diff --git a/packages/server/src/api/routes/tests/debug.spec.ts b/packages/server/src/api/routes/tests/debug.spec.ts index 53e1f67823..546344a646 100644 --- a/packages/server/src/api/routes/tests/debug.spec.ts +++ b/packages/server/src/api/routes/tests/debug.spec.ts @@ -1,13 +1,5 @@ -const { checkBuilderEndpoint } = require("./utilities/TestFunctions") -const setup = require("./utilities") - -import os from "os" - -jest.mock("process", () => ({ - arch: "arm64", - version: "v14.20.1", - platform: "darwin", -})) +import * as setup from "./utilities" +import { checkBuilderEndpoint } from "./utilities/TestFunctions" describe("/component", () => { let request = setup.getRequest() @@ -17,21 +9,6 @@ describe("/component", () => { beforeAll(async () => { await config.init() - os.cpus = () => [ - { - model: "test", - speed: 12323, - times: { - user: 0, - nice: 0, - sys: 0, - idle: 0, - irq: 0, - }, - }, - ] - os.uptime = () => 123123123123 - os.totalmem = () => 10000000000 }) describe("/api/debug", () => { @@ -43,14 +20,16 @@ describe("/component", () => { .expect(200) expect(res.body).toEqual({ budibaseVersion: "0.0.0+jest", - cpuArch: "arm64", - cpuCores: 1, - cpuInfo: "test", + cpuArch: expect.any(String), + cpuCores: expect.any(Number), + cpuInfo: expect.any(String), hosting: "docker-compose", - nodeVersion: "v14.20.1", - platform: "darwin", - totalMemory: "9.313225746154785GB", - uptime: "1425036 day(s), 3 hour(s), 32 minute(s)", + nodeVersion: expect.stringMatching(/^v\d+\.\d+\.\d+$/), + platform: expect.any(String), + totalMemory: expect.stringMatching(/^[0-9\\.]+GB$/), + uptime: expect.stringMatching( + /^\d+ day\(s\), \d+ hour\(s\), \d+ minute\(s\)$/ + ), }) }) diff --git a/packages/server/src/api/routes/tests/queries/mysql.spec.ts b/packages/server/src/api/routes/tests/queries/mysql.spec.ts index 6c97ab5835..d04e53971d 100644 --- a/packages/server/src/api/routes/tests/queries/mysql.spec.ts +++ b/packages/server/src/api/routes/tests/queries/mysql.spec.ts @@ -2,9 +2,7 @@ import { Datasource, Query } from "@budibase/types" import * as setup from "../utilities" import { databaseTestProviders } from "../../../../integrations/tests/utils" import mysql from "mysql2/promise" - -jest.unmock("mysql2") -jest.unmock("mysql2/promise") +import { generator } from "@budibase/backend-core/tests" const createTableSQL = ` CREATE TABLE test_table ( @@ -76,164 +74,306 @@ describe("/queries", () => { }) }) - it("should execute a query", async () => { - const query = await createQuery({ - fields: { - sql: "SELECT * FROM test_table ORDER BY id", - }, + describe("read", () => { + it("should execute a query", async () => { + const query = await createQuery({ + fields: { + sql: "SELECT * FROM test_table ORDER BY id", + }, + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { + id: 1, + name: "one", + }, + { + id: 2, + name: "two", + }, + { + id: 3, + name: "three", + }, + { + id: 4, + name: "four", + }, + { + id: 5, + name: "five", + }, + ]) }) - const result = await config.api.query.execute(query._id!) - - expect(result.data).toEqual([ - { - id: 1, - name: "one", - }, - { - id: 2, - name: "two", - }, - { - id: 3, - name: "three", - }, - { - id: 4, - name: "four", - }, - { - id: 5, - name: "five", - }, - ]) - }) - - it("should be able to transform a query", async () => { - const query = await createQuery({ - fields: { - sql: "SELECT * FROM test_table WHERE id = 1", - }, - transformer: ` + it("should be able to transform a query", async () => { + const query = await createQuery({ + fields: { + sql: "SELECT * FROM test_table WHERE id = 1", + }, + transformer: ` data[0].id = data[0].id + 1; return data; `, + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { + id: 2, + name: "one", + }, + ]) }) - const result = await config.api.query.execute(query._id!) + it("should coerce numeric bindings", async () => { + const query = await createQuery({ + fields: { + sql: "SELECT * FROM test_table WHERE id = {{ id }}", + }, + parameters: [ + { + name: "id", + default: "", + }, + ], + }) - expect(result.data).toEqual([ - { - id: 2, - name: "one", - }, - ]) + const result = await config.api.query.execute(query._id!, { + parameters: { + id: "1", + }, + }) + + expect(result.data).toEqual([ + { + id: 1, + name: "one", + }, + ]) + }) }) - it("should be able to insert with bindings", async () => { - const query = await createQuery({ - fields: { - sql: "INSERT INTO test_table (name) VALUES ({{ foo }})", - }, - parameters: [ + describe("create", () => { + it("should be able to insert with bindings", async () => { + const query = await createQuery({ + fields: { + sql: "INSERT INTO test_table (name) VALUES ({{ foo }})", + }, + parameters: [ + { + name: "foo", + default: "bar", + }, + ], + queryVerb: "create", + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { + foo: "baz", + }, + }) + + expect(result.data).toEqual([ { + created: true, + }, + ]) + + await withConnection(async connection => { + const [rows] = await connection.query( + "SELECT * FROM test_table WHERE name = 'baz'" + ) + expect(rows).toHaveLength(1) + }) + }) + + it.each(["2021-02-05T12:01:00.000Z", "2021-02-05"])( + "should coerce %s into a date", + async dateStr => { + const date = new Date(dateStr) + const tableName = `\`${generator.guid()}\`` + await withConnection(async connection => { + await connection.query(`CREATE TABLE ${tableName} ( + id INT AUTO_INCREMENT PRIMARY KEY, + date DATETIME NOT NULL + )`) + }) + + const query = await createQuery({ + fields: { + sql: `INSERT INTO ${tableName} (date) VALUES ({{ date }})`, + }, + parameters: [ + { + name: "date", + default: "", + }, + ], + queryVerb: "create", + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { date: dateStr }, + }) + + expect(result.data).toEqual([{ created: true }]) + + await withConnection(async connection => { + const [rows] = await connection.query( + `SELECT * FROM ${tableName} WHERE date = '${date.toISOString()}'` + ) + expect(rows).toHaveLength(1) + }) + } + ) + + it.each(["2021,02,05", "202205-1500"])( + "should not coerce %s as a date", + async date => { + const query = await createQuery({ + fields: { + sql: "INSERT INTO test_table (name) VALUES ({{ name }})", + }, + parameters: [ + { + name: "name", + default: "", + }, + ], + queryVerb: "create", + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { + name: date, + }, + }) + + expect(result.data).toEqual([{ created: true }]) + + await withConnection(async connection => { + const [rows] = await connection.query( + `SELECT * FROM test_table WHERE name = '${date}'` + ) + expect(rows).toHaveLength(1) + }) + } + ) + }) + + describe("update", () => { + it("should be able to update rows", async () => { + const query = await createQuery({ + fields: { + sql: "UPDATE test_table SET name = {{ name }} WHERE id = {{ id }}", + }, + parameters: [ + { + name: "id", + default: "", + }, + { + name: "name", + default: "updated", + }, + ], + queryVerb: "update", + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { + id: "1", name: "foo", - default: "bar", }, - ], - queryVerb: "create", + }) + + expect(result.data).toEqual([ + { + updated: true, + }, + ]) + + await withConnection(async connection => { + const [rows] = await connection.query( + "SELECT * FROM test_table WHERE id = 1" + ) + expect(rows).toEqual([{ id: 1, name: "foo" }]) + }) }) - const result = await config.api.query.execute(query._id!, { - parameters: { - foo: "baz", - }, - }) + it("should be able to execute an update that updates no rows", async () => { + const query = await createQuery({ + fields: { + sql: "UPDATE test_table SET name = 'updated' WHERE id = 100", + }, + queryVerb: "update", + }) - expect(result.data).toEqual([ - { - created: true, - }, - ]) + const result = await config.api.query.execute(query._id!) - await withConnection(async connection => { - const [rows] = await connection.query( - "SELECT * FROM test_table WHERE name = 'baz'" - ) - expect(rows).toHaveLength(1) + expect(result.data).toEqual([ + { + updated: true, + }, + ]) }) }) - it("should be able to update rows", async () => { - const query = await createQuery({ - fields: { - sql: "UPDATE test_table SET name = {{ name }} WHERE id = {{ id }}", - }, - parameters: [ - { - name: "id", - default: "", + describe("delete", () => { + it("should be able to delete rows", async () => { + const query = await createQuery({ + fields: { + sql: "DELETE FROM test_table WHERE id = {{ id }}", }, - { - name: "name", - default: "updated", + parameters: [ + { + name: "id", + default: "", + }, + ], + queryVerb: "delete", + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { + id: "1", }, - ], - queryVerb: "update", - }) + }) - const result = await config.api.query.execute(query._id!, { - parameters: { - id: "1", - name: "foo", - }, - }) - - expect(result.data).toEqual([ - { - updated: true, - }, - ]) - - await withConnection(async connection => { - const [rows] = await connection.query( - "SELECT * FROM test_table WHERE id = 1" - ) - expect(rows).toEqual([{ id: 1, name: "foo" }]) - }) - }) - - it("should be able to delete rows", async () => { - const query = await createQuery({ - fields: { - sql: "DELETE FROM test_table WHERE id = {{ id }}", - }, - parameters: [ + expect(result.data).toEqual([ { - name: "id", - default: "", + deleted: true, }, - ], - queryVerb: "delete", + ]) + + await withConnection(async connection => { + const [rows] = await connection.query( + "SELECT * FROM test_table WHERE id = 1" + ) + expect(rows).toHaveLength(0) + }) }) - const result = await config.api.query.execute(query._id!, { - parameters: { - id: "1", - }, - }) + it("should be able to execute a delete that deletes no rows", async () => { + const query = await createQuery({ + fields: { + sql: "DELETE FROM test_table WHERE id = 100", + }, + queryVerb: "delete", + }) - expect(result.data).toEqual([ - { - deleted: true, - }, - ]) + const result = await config.api.query.execute(query._id!) - await withConnection(async connection => { - const [rows] = await connection.query( - "SELECT * FROM test_table WHERE id = 1" - ) - expect(rows).toHaveLength(0) + expect(result.data).toEqual([ + { + deleted: true, + }, + ]) }) }) }) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index aff85b4d0e..f638f2c4bf 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -29,8 +29,6 @@ import * as uuid from "uuid" const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString() tk.freeze(timestamp) -jest.unmock("mysql2") -jest.unmock("mysql2/promise") jest.unmock("mssql") jest.unmock("pg") diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 778d9d6e5a..f9d213a26b 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -25,8 +25,6 @@ import merge from "lodash/merge" import { quotas } from "@budibase/pro" import { roles } from "@budibase/backend-core" -jest.unmock("mysql2") -jest.unmock("mysql2/promise") jest.unmock("mssql") jest.unmock("pg") diff --git a/packages/server/src/integration-test/mysql.spec.ts b/packages/server/src/integration-test/mysql.spec.ts index a22410b812..92420fb336 100644 --- a/packages/server/src/integration-test/mysql.spec.ts +++ b/packages/server/src/integration-test/mysql.spec.ts @@ -20,7 +20,6 @@ fetch.mockSearch() const config = setup.getConfig()! -jest.unmock("mysql2/promise") jest.mock("../websockets", () => ({ clientAppSocket: jest.fn(), gridAppSocket: jest.fn(), diff --git a/packages/server/src/integrations/tests/mysql.spec.ts b/packages/server/src/integrations/tests/mysql.spec.ts deleted file mode 100644 index 5180645885..0000000000 --- a/packages/server/src/integrations/tests/mysql.spec.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { default as MySQLIntegration, bindingTypeCoerce } from "../mysql" - -jest.mock("mysql2") - -class TestConfiguration { - integration: any - - constructor(config: any = { ssl: {} }) { - this.integration = new MySQLIntegration.integration(config) - } -} - -describe("MySQL Integration", () => { - let config: any - - beforeEach(() => { - config = new TestConfiguration() - }) - - it("calls the create method with the correct params", async () => { - const sql = "insert into users (name, age) values ('Joe', 123);" - await config.integration.create({ - sql, - }) - expect(config.integration.client.query).toHaveBeenCalledWith(sql, []) - }) - - it("calls the read method with the correct params", async () => { - const sql = "select * from users;" - await config.integration.read({ - sql, - }) - expect(config.integration.client.query).toHaveBeenCalledWith(sql, []) - }) - - it("calls the update method with the correct params", async () => { - const sql = "update table users set name = 'test';" - await config.integration.update({ - sql, - }) - expect(config.integration.client.query).toHaveBeenCalledWith(sql, []) - }) - - it("calls the delete method with the correct params", async () => { - const sql = "delete from users where name = 'todelete';" - await config.integration.delete({ - sql, - }) - expect(config.integration.client.query).toHaveBeenCalledWith(sql, []) - }) - - describe("no rows returned", () => { - it("returns the correct response when the create response has no rows", async () => { - const sql = "insert into users (name, age) values ('Joe', 123);" - const response = await config.integration.create({ - sql, - }) - expect(response).toEqual([{ created: true }]) - }) - - it("returns the correct response when the update response has no rows", async () => { - const sql = "update table users set name = 'test';" - const response = await config.integration.update({ - sql, - }) - expect(response).toEqual([{ updated: true }]) - }) - - it("returns the correct response when the delete response has no rows", async () => { - const sql = "delete from users where name = 'todelete';" - const response = await config.integration.delete({ - sql, - }) - expect(response).toEqual([{ deleted: true }]) - }) - }) - - describe("binding type coerce", () => { - it("ignores non-string types", async () => { - const sql = "select * from users;" - const date = new Date() - await config.integration.read({ - sql, - bindings: [11, date, ["a", "b", "c"], { id: 1 }], - }) - expect(config.integration.client.query).toHaveBeenCalledWith(sql, [ - 11, - date, - ["a", "b", "c"], - { id: 1 }, - ]) - }) - - it("parses strings matching a number regex", async () => { - const sql = "select * from users;" - await config.integration.read({ - sql, - bindings: ["101", "3.14"], - }) - expect(config.integration.client.query).toHaveBeenCalledWith( - sql, - [101, 3.14] - ) - }) - - it("parses strings matching a valid date format", async () => { - const sql = "select * from users;" - await config.integration.read({ - sql, - bindings: [ - "2001-10-30", - "2010-09-01T13:30:59.123Z", - "2021-02-05 12:01 PM", - ], - }) - expect(config.integration.client.query).toHaveBeenCalledWith(sql, [ - new Date("2001-10-30T00:00:00.000Z"), - new Date("2010-09-01T13:30:59.123Z"), - new Date("2021-02-05T12:01:00.000Z"), - ]) - }) - - it("does not parse string matching a valid array of numbers as date", async () => { - const sql = "select * from users;" - await config.integration.read({ - sql, - bindings: ["1,2,2017"], - }) - expect(config.integration.client.query).toHaveBeenCalledWith(sql, [ - "1,2,2017", - ]) - }) - }) -}) - -describe("bindingTypeCoercion", () => { - it("shouldn't coerce something that looks like a date", () => { - const response = bindingTypeCoerce(["202205-1500"]) - expect(response[0]).toBe("202205-1500") - }) - - it("should coerce an actual date", () => { - const date = new Date("2023-06-13T14:24:22.620Z") - const response = bindingTypeCoerce(["2023-06-13T14:24:22.620Z"]) - expect(response[0]).toEqual(date) - }) - - it("should coerce numbers", () => { - const response = bindingTypeCoerce(["0"]) - expect(response[0]).toEqual(0) - }) -}) diff --git a/packages/server/src/sdk/app/rows/search/tests/external.spec.ts b/packages/server/src/sdk/app/rows/search/tests/external.spec.ts index bae84592ca..8ecec784dd 100644 --- a/packages/server/src/sdk/app/rows/search/tests/external.spec.ts +++ b/packages/server/src/sdk/app/rows/search/tests/external.spec.ts @@ -17,8 +17,6 @@ import { generator, } from "@budibase/backend-core/tests" -jest.unmock("mysql2/promise") - jest.setTimeout(30000) describe("external search", () => { diff --git a/qa-core/src/integrations/external-schema/mysql.integration.spec.ts b/qa-core/src/integrations/external-schema/mysql.integration.spec.ts index c34651ea0e..5a7e1989d2 100644 --- a/qa-core/src/integrations/external-schema/mysql.integration.spec.ts +++ b/qa-core/src/integrations/external-schema/mysql.integration.spec.ts @@ -1,8 +1,6 @@ import { GenericContainer } from "testcontainers" import mysql from "../../../../packages/server/src/integrations/mysql" -jest.unmock("mysql2/promise") - describe("datasource validators", () => { describe("mysql", () => { let config: any diff --git a/qa-core/src/integrations/validators/mysql.integration.spec.ts b/qa-core/src/integrations/validators/mysql.integration.spec.ts index 0f0de132fe..e828d192af 100644 --- a/qa-core/src/integrations/validators/mysql.integration.spec.ts +++ b/qa-core/src/integrations/validators/mysql.integration.spec.ts @@ -1,8 +1,6 @@ import { GenericContainer } from "testcontainers" import mysql from "../../../../packages/server/src/integrations/mysql" -jest.unmock("mysql2/promise") - describe("datasource validators", () => { describe("mysql", () => { let host: string