diff --git a/lerna.json b/lerna.json index ae6faef23d..7c47d0b3df 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "2.33.13", + "version": "2.33.14", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts index 7eeeb5cca0..e4b2b843af 100644 --- a/packages/backend-core/src/sql/sql.ts +++ b/packages/backend-core/src/sql/sql.ts @@ -316,11 +316,9 @@ class InternalBuilder { const columnSchema = schema[column] if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(columnSchema)) { - // TODO: figure out how to express this safely without string - // interpolation. - return this.knex.raw(`??::money::numeric as "${field}"`, [ + return this.knex.raw(`??::money::numeric as ??`, [ this.rawQuotedIdentifier([table, column].join(".")), - field, + this.knex.raw(this.quote(field)), ]) } @@ -330,8 +328,9 @@ class InternalBuilder { // TODO: figure out how to express this safely without string // interpolation. - return this.knex.raw(`CONVERT(varchar, ??, 108) as "${field}"`, [ + return this.knex.raw(`CONVERT(varchar, ??, 108) as ??`, [ this.rawQuotedIdentifier(field), + this.knex.raw(this.quote(field)), ]) } diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridViewCalculationButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridViewCalculationButton.svelte index f6ef6e6099..7b279d3948 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/grid/GridViewCalculationButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridViewCalculationButton.svelte @@ -90,8 +90,15 @@ const getFieldOptions = (self, calculations, schema) => { return Object.entries(schema) .filter(([field, fieldSchema]) => { - // Only allow numeric fields that are not calculations themselves - if (fieldSchema.calculationType || !isNumeric(fieldSchema.type)) { + // Don't allow other calculation columns + if (fieldSchema.calculationType) { + return false + } + // Only allow numeric columns for most calculation types + if ( + self.type !== CalculationType.COUNT && + !isNumeric(fieldSchema.type) + ) { return false } // Don't allow duplicates @@ -234,7 +241,7 @@
@@ -257,6 +264,4 @@ .group-by { grid-column: 2 / 5; } - span { - } diff --git a/packages/client/src/components/app/GridBlock.svelte b/packages/client/src/components/app/GridBlock.svelte index c6b08e3869..4a8dfb4fcb 100644 --- a/packages/client/src/components/app/GridBlock.svelte +++ b/packages/client/src/components/app/GridBlock.svelte @@ -60,8 +60,11 @@ // Provide additional data context for live binding eval export const getAdditionalDataContext = () => { - const rows = get(grid?.getContext()?.rows) - const goldenRow = generateGoldenSample(rows) + const gridContext = grid?.getContext() + const rows = get(gridContext?.rows) || [] + const clean = gridContext?.rows.actions.cleanRow || (x => x) + const cleaned = rows.map(clean) + const goldenRow = generateGoldenSample(cleaned) const id = get(component).id return { // Not sure what this one is for... diff --git a/packages/frontend-core/src/components/grid/lib/constants.js b/packages/frontend-core/src/components/grid/lib/constants.js index 6ea7a98178..81b4a2f823 100644 --- a/packages/frontend-core/src/components/grid/lib/constants.js +++ b/packages/frontend-core/src/components/grid/lib/constants.js @@ -10,6 +10,8 @@ export const DefaultColumnWidth = 200 export const MinColumnWidth = 80 export const NewRowID = "new" export const BlankRowID = "blank" +export const GeneratedIDPrefix = "‽‽" +export const CellIDSeparator = "‽‽" export const RowPageSize = 100 export const FocusedCellMinOffset = ScrollBarSize * 3 export const ControlsHeight = 50 diff --git a/packages/frontend-core/src/components/grid/lib/utils.js b/packages/frontend-core/src/components/grid/lib/utils.js index b5485abc36..f1f33d9950 100644 --- a/packages/frontend-core/src/components/grid/lib/utils.js +++ b/packages/frontend-core/src/components/grid/lib/utils.js @@ -1,18 +1,17 @@ -// We can't use "-" as a separator as this can be present in the ID -// or column name, so we use something very unusual to avoid this problem -const JOINING_CHARACTER = "‽‽" +import { GeneratedIDPrefix, CellIDSeparator } from "./constants" +import { Helpers } from "@budibase/bbui" export const parseCellID = cellId => { if (!cellId) { return { rowId: undefined, field: undefined } } - const parts = cellId.split(JOINING_CHARACTER) + const parts = cellId.split(CellIDSeparator) const field = parts.pop() - return { rowId: parts.join(JOINING_CHARACTER), field } + return { rowId: parts.join(CellIDSeparator), field } } export const getCellID = (rowId, fieldName) => { - return `${rowId}${JOINING_CHARACTER}${fieldName}` + return `${rowId}${CellIDSeparator}${fieldName}` } export const parseEventLocation = e => { @@ -21,3 +20,11 @@ export const parseEventLocation = e => { y: e.clientY ?? e.touches?.[0]?.clientY, } } + +export const generateRowID = () => { + return `${GeneratedIDPrefix}${Helpers.uuid()}` +} + +export const isGeneratedRowID = id => { + return id?.startsWith(GeneratedIDPrefix) +} diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index 4cfaa1cfd5..122bbc295a 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -1,7 +1,12 @@ import { writable, derived, get } from "svelte/store" import { fetchData } from "../../../fetch" import { NewRowID, RowPageSize } from "../lib/constants" -import { getCellID, parseCellID } from "../lib/utils" +import { + generateRowID, + getCellID, + isGeneratedRowID, + parseCellID, +} from "../lib/utils" import { tick } from "svelte" import { Helpers } from "@budibase/bbui" import { sleep } from "../../../utils/utils" @@ -634,10 +639,10 @@ export const createActions = context => { newRow = newRows[i] // Ensure we have a unique _id. - // This means generating one for non DS+, overwriting any that may already - // exist as we cannot allow duplicates. - if (!$hasBudibaseIdentifiers) { - newRow._id = Helpers.uuid() + // We generate one for non DS+ where required, but trust that any existing + // _id values are unique (e.g. Mongo) + if (!$hasBudibaseIdentifiers && !newRow._id?.length) { + newRow._id = generateRowID() } if (!rowCacheMap[newRow._id]) { @@ -674,7 +679,7 @@ export const createActions = context => { let clone = { ...row } delete clone.__idx delete clone.__metadata - if (!get(hasBudibaseIdentifiers)) { + if (!get(hasBudibaseIdentifiers) && isGeneratedRowID(clone._id)) { delete clone._id } return clone diff --git a/packages/server/src/api/controllers/table/utils.ts b/packages/server/src/api/controllers/table/utils.ts index 96c01a15b8..743cce410b 100644 --- a/packages/server/src/api/controllers/table/utils.ts +++ b/packages/server/src/api/controllers/table/utils.ts @@ -15,7 +15,7 @@ import { getViews, saveView } from "../view/utils" import viewTemplate from "../view/viewBuilder" import { cloneDeep } from "lodash/fp" import { quotas } from "@budibase/pro" -import { context, events, features } from "@budibase/backend-core" +import { context, events, features, HTTPError } from "@budibase/backend-core" import { AutoFieldSubType, Database, @@ -145,14 +145,21 @@ export async function importToRows( // the real schema of the table passed in, not the clone used for // incrementing auto IDs for (const [fieldName, schema] of Object.entries(originalTable.schema)) { - const rowVal = Array.isArray(row[fieldName]) - ? row[fieldName] - : [row[fieldName]] + if (schema.type === FieldType.LINK) { + throw new HTTPError( + `Can't bulk import relationship fields for internal databases, found value in field "${fieldName}"`, + 400 + ) + } + if ( (schema.type === FieldType.OPTIONS || schema.type === FieldType.ARRAY) && row[fieldName] ) { + const rowVal = Array.isArray(row[fieldName]) + ? row[fieldName] + : [row[fieldName]] let merged = [...schema.constraints!.inclusion!, ...rowVal] let superSet = new Set(merged) schema.constraints!.inclusion = Array.from(superSet) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 252731bc16..8c4a01b07b 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -1823,6 +1823,39 @@ describe.each([ expect(row.autoId).toEqual(3) }) + isInternal && + it("should reject bulkImporting relationship fields", async () => { + const table1 = await config.api.table.save(saveTableRequest()) + const table2 = await config.api.table.save( + saveTableRequest({ + schema: { + relationship: { + name: "relationship", + type: FieldType.LINK, + tableId: table1._id!, + relationshipType: RelationshipType.ONE_TO_MANY, + fieldName: "relationship", + }, + }, + }) + ) + + const table1Row1 = await config.api.row.save(table1._id!, {}) + await config.api.row.bulkImport( + table2._id!, + { + rows: [{ relationship: [table1Row1._id!] }], + }, + { + status: 400, + body: { + message: + 'Can\'t bulk import relationship fields for internal databases, found value in field "relationship"', + }, + } + ) + }) + it("should be able to bulkImport rows", async () => { const table = await config.api.table.save( saveTableRequest({ diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts index f2e0382deb..7654d84551 100644 --- a/packages/server/src/integration-test/postgres.spec.ts +++ b/packages/server/src/integration-test/postgres.spec.ts @@ -1,5 +1,5 @@ import * as setup from "../api/routes/tests/utilities" -import { Datasource, FieldType } from "@budibase/types" +import { Datasource, FieldType, Table } from "@budibase/types" import _ from "lodash" import { generator } from "@budibase/backend-core/tests" import { @@ -229,4 +229,54 @@ describe("postgres integrations", () => { ).toBeUndefined() }) }) + + describe("money field 💰", () => { + const tableName = "moneytable" + let table: Table + + beforeAll(async () => { + await client.raw(` + CREATE TABLE ${tableName} ( + id serial PRIMARY KEY, + price money + ) + `) + const response = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) + table = response.datasource.entities![tableName] + }) + + it("should be able to import a money field", async () => { + expect(table).toBeDefined() + expect(table?.schema.price.type).toBe(FieldType.NUMBER) + }) + + it("should be able to search a money field", async () => { + await config.api.row.bulkImport(table._id!, { + rows: [{ price: 200 }, { price: 300 }], + }) + + const { rows } = await config.api.row.search(table._id!, { + query: { + equal: { + price: 200, + }, + }, + }) + expect(rows).toHaveLength(1) + expect(rows[0].price).toBe("200.00") + }) + + it("should be able to update a money field", async () => { + let row = await config.api.row.save(table._id!, { price: 200 }) + expect(row.price).toBe("200.00") + + row = await config.api.row.save(table._id!, { ...row, price: 300 }) + expect(row.price).toBe("300.00") + + row = await config.api.row.save(table._id!, { ...row, price: "400.00" }) + expect(row.price).toBe("400.00") + }) + }) })