From b6bcf6719fe866f3f8de26932352349543cd9dd9 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 10 May 2024 11:27:49 +0100 Subject: [PATCH 01/21] Fixes an issue with fetch information being passed up from DatabaseImpl, making sure errors are fully sanitised. --- .../backend-core/src/db/couch/DatabaseImpl.ts | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index d220d0a8ac..d54e23217b 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -3,11 +3,11 @@ import { AllDocsResponse, AnyDocument, Database, - DatabaseOpts, - DatabaseQueryOpts, - DatabasePutOpts, DatabaseCreateIndexOpts, DatabaseDeleteIndexOpts, + DatabaseOpts, + DatabasePutOpts, + DatabaseQueryOpts, Document, isDocument, RowResponse, @@ -17,7 +17,7 @@ import { import { getCouchInfo } from "./connections" import { directCouchUrlCall } from "./utils" import { getPouchDB } from "./pouchDB" -import { WriteStream, ReadStream } from "fs" +import { ReadStream, WriteStream } from "fs" import { newid } from "../../docIds/newid" import { SQLITE_DESIGN_DOC_ID } from "../../constants" import { DDInstrumentedDatabase } from "../instrumentation" @@ -38,6 +38,34 @@ function buildNano(couchInfo: { url: string; cookie: string }) { type DBCall = () => Promise +class CouchDBError extends Error { + status: number + statusCode: number + reason: string + name: string + errid: string | undefined + description: string | undefined + + constructor( + message: string, + info: { + status: number + name: string + errid: string + description: string + reason: string + } + ) { + super(message) + this.status = info.status + this.statusCode = info.status + this.reason = info.reason + this.name = info.name + this.errid = info.errid + this.description = info.description + } +} + export function DatabaseWithConnection( dbName: string, connection: string, @@ -119,7 +147,7 @@ export class DatabaseImpl implements Database { } catch (err: any) { // Handling race conditions if (err.statusCode !== 412) { - throw err + throw new CouchDBError(err.message, err) } } } @@ -138,10 +166,15 @@ export class DatabaseImpl implements Database { if (err.statusCode === 404 && err.reason === DATABASE_NOT_FOUND) { await this.checkAndCreateDb() return await this.performCall(call) - } else if (err.statusCode) { - err.status = err.statusCode } - throw err + // stripping the error down the props which are safe/useful, drop everything else + throw new CouchDBError(`CouchDB error: ${err.message}`, { + status: err.status || err.statusCode, + name: err.name, + errid: err.errid, + description: err.description, + reason: err.reason, + }) } } @@ -281,16 +314,9 @@ export class DatabaseImpl implements Database { } async destroy() { - try { - return await this.nano().db.destroy(this.name) - } catch (err: any) { - // didn't exist, don't worry - if (err.statusCode === 404) { - return - } else { - throw { ...err, status: err.statusCode } - } - } + return this.performCall(async () => { + return () => this.nano().db.destroy(this.name) + }) } async compact() { From f036776a907d12204163a7ce3764afa64465013d Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 10 May 2024 11:32:57 +0100 Subject: [PATCH 02/21] One small change to keep 404 functionality on destroy DB. --- packages/backend-core/src/db/couch/DatabaseImpl.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index d54e23217b..ca8a22b54e 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -314,9 +314,16 @@ export class DatabaseImpl implements Database { } async destroy() { - return this.performCall(async () => { - return () => this.nano().db.destroy(this.name) - }) + try { + return await this.nano().db.destroy(this.name) + } catch (err: any) { + // didn't exist, don't worry + if (err.statusCode === 404) { + return + } else { + throw new CouchDBError(err.message, err) + } + } } async compact() { From c9ec06b5b1d4e790c29542a9d708aabb1709237d Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 10 May 2024 11:51:57 +0100 Subject: [PATCH 03/21] Adding error field. --- .../backend-core/src/db/couch/DatabaseImpl.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index ca8a22b54e..c520f4d81f 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -43,8 +43,9 @@ class CouchDBError extends Error { statusCode: number reason: string name: string - errid: string | undefined - description: string | undefined + errid: string + error: string + description: string constructor( message: string, @@ -54,6 +55,7 @@ class CouchDBError extends Error { errid: string description: string reason: string + error: string } ) { super(message) @@ -63,6 +65,7 @@ class CouchDBError extends Error { this.name = info.name this.errid = info.errid this.description = info.description + this.error = info.error } } @@ -168,13 +171,7 @@ export class DatabaseImpl implements Database { return await this.performCall(call) } // stripping the error down the props which are safe/useful, drop everything else - throw new CouchDBError(`CouchDB error: ${err.message}`, { - status: err.status || err.statusCode, - name: err.name, - errid: err.errid, - description: err.description, - reason: err.reason, - }) + throw new CouchDBError(`CouchDB error: ${err.message}`, err) } } From a1a50de61c611d7bcff22cbded4af61d9ce661f5 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 10 May 2024 11:59:11 +0100 Subject: [PATCH 04/21] Final final fix. --- packages/backend-core/src/db/couch/DatabaseImpl.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index c520f4d81f..ef351f7d4d 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -50,7 +50,8 @@ class CouchDBError extends Error { constructor( message: string, info: { - status: number + status: number | undefined + statusCode: number | undefined name: string errid: string description: string @@ -59,8 +60,9 @@ class CouchDBError extends Error { } ) { super(message) - this.status = info.status - this.statusCode = info.status + const statusCode = info.status || info.statusCode || 500 + this.status = statusCode + this.statusCode = statusCode this.reason = info.reason this.name = info.name this.errid = info.errid From 1365d190488e816f96f49a0568e74e4f05d861cc Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 10 May 2024 12:03:24 +0100 Subject: [PATCH 05/21] Updating pro reference. --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 479879246a..ff397e5454 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 479879246aac5dd3073cc695945c62c41fae5b0e +Subproject commit ff397e5454ad3361b25efdf14746c36dcbd3f409 From bec7b782775df65e298a483ce5f141e60c54320b Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Fri, 10 May 2024 11:10:28 +0000 Subject: [PATCH 06/21] Bump version to 2.24.3 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 9c5a6c6bab..7daf0b039b 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.24.2", + "version": "2.24.3", "npmClient": "yarn", "packages": [ "packages/*", From ad57776b7fe780296695a93a1c22e190157edc2e Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Fri, 10 May 2024 11:13:00 +0000 Subject: [PATCH 07/21] Bump version to 2.25.0 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 7daf0b039b..16dc73aa30 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.24.3", + "version": "2.25.0", "npmClient": "yarn", "packages": [ "packages/*", From e8b8e6e8b4385cb5e167b1467e1d91ad6768e377 Mon Sep 17 00:00:00 2001 From: melohagan <101575380+melohagan@users.noreply.github.com> Date: Fri, 10 May 2024 13:18:30 +0100 Subject: [PATCH 08/21] Allow Fancy Input validation to be triggered onBlur (#13658) * Add free_trial to deploy camunda script * Allow for more validation customisation on fancy input --- packages/bbui/src/FancyForm/FancyInput.svelte | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/bbui/src/FancyForm/FancyInput.svelte b/packages/bbui/src/FancyForm/FancyInput.svelte index 0c58b9b045..f665fa5724 100644 --- a/packages/bbui/src/FancyForm/FancyInput.svelte +++ b/packages/bbui/src/FancyForm/FancyInput.svelte @@ -11,6 +11,7 @@ export let error = null export let validate = null export let suffix = null + export let validateOn = "change" const dispatch = createEventDispatcher() @@ -24,7 +25,16 @@ const newValue = e.target.value dispatch("change", newValue) value = newValue - if (validate) { + if (validate && (error || validateOn === "change")) { + error = validate(newValue) + } + } + + const onBlur = e => { + focused = false + const newValue = e.target.value + dispatch("blur", newValue) + if (validate && validateOn === "blur") { error = validate(newValue) } } @@ -61,7 +71,7 @@ type={type || "text"} on:input={onChange} on:focus={() => (focused = true)} - on:blur={() => (focused = false)} + on:blur={onBlur} class:placeholder bind:this={ref} /> From 902613d6007a2b3c49d3cac30af9aa583046365f Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 20 May 2024 12:00:08 +0100 Subject: [PATCH 09/21] Working towards user relationship tests passing. --- .../src/api/routes/tests/search.spec.ts | 134 +++++++++++++++++- packages/server/src/integrations/base/sql.ts | 2 +- .../server/src/sdk/app/rows/search/sqs.ts | 3 +- 3 files changed, 131 insertions(+), 8 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index d036da646e..426f383ad0 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -1,6 +1,6 @@ import { tableForDatasource } from "../../../tests/utilities/structures" import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" -import { db as dbCore } from "@budibase/backend-core" +import { db as dbCore, utils } from "@budibase/backend-core" import * as setup from "./utilities" import { @@ -25,12 +25,12 @@ const serverTime = new Date("2024-05-06T00:00:00.000Z") tk.freeze(serverTime) describe.each([ - ["lucene", undefined], + //["lucene", undefined], ["sqs", undefined], - [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], - [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], - [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], - [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], + //[DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], + //[DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], + //[DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], + //[DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], ])("/api/:sourceId/search (%s)", (name, dsProvider) => { const isSqs = name === "sqs" const isLucene = name === "lucene" @@ -1152,4 +1152,126 @@ describe.each([ ])) }) }) + + describe("user", () => { + let user1: User + let user2: User + + beforeAll(async () => { + user1 = await config.createUser({ _id: `us_${utils.newid()}` }) + user2 = await config.createUser({ _id: `us_${utils.newid()}` }) + + await createTable({ + user: { + name: "user", + type: FieldType.BB_REFERENCE_SINGLE, + subtype: BBReferenceFieldSubType.USER, + }, + }) + + await createRows([ + { user: JSON.stringify(user1) }, + { user: JSON.stringify(user2) }, + ]) + }) + + describe("equal", () => { + it("successfully finds a row", () => + expectQuery({ equal: { user: user1._id } }).toContainExactly([ + { user: { _id: user1._id } }, + ])) + + it("fails to find nonexistent row", () => + expectQuery({ equal: { user: "us_none" } }).toFindNothing()) + }) + + describe("notEqual", () => { + it("successfully finds a row", () => + expectQuery({ notEqual: { user: user1._id } }).toContainExactly([ + { user: { _id: user2._id } }, + ])) + + it("fails to find nonexistent row", () => + expectQuery({ notEqual: { user: "us_none" } }).toContainExactly([ + { user: { _id: user1._id } }, + { user: { _id: user2._id } }, + ])) + }) + + describe("oneOf", () => { + it("successfully finds a row", () => + expectQuery({ oneOf: { user: [user1._id] } }).toContainExactly([ + { user: { _id: user1._id } }, + ])) + + it("fails to find nonexistent row", () => + expectQuery({ oneOf: { user: ["us_none"] } }).toFindNothing()) + }) + }) + + describe("multi user", () => { + let user1: User + let user2: User + + beforeAll(async () => { + user1 = await config.createUser({ _id: `us_${utils.newid()}` }) + user2 = await config.createUser({ _id: `us_${utils.newid()}` }) + + await createTable({ + users: { + name: "users", + type: FieldType.BB_REFERENCE, + subtype: BBReferenceFieldSubType.USER, + }, + }) + + await createRows([ + { users: JSON.stringify([user1]) }, + { users: JSON.stringify([user2]) }, + { users: JSON.stringify([user1, user2]) }, + { users: JSON.stringify([]) }, + ]) + }) + + describe("contains", () => { + it("successfully finds a row", () => + expectQuery({ contains: { users: [user1._id] } }).toContainExactly([ + { users: [{ _id: user1._id }] }, + { users: [{ _id: user1._id }, { _id: user2._id }] }, + ])) + + it("fails to find nonexistent row", () => + expectQuery({ contains: { users: ["us_none"] } }).toFindNothing()) + }) + + describe("notContains", () => { + it("successfully finds a row", () => + expectQuery({ notContains: { users: [user1._id] } }).toContainExactly([ + { users: [{ _id: user2._id }] }, + { users: [] }, + ])) + + it("fails to find nonexistent row", () => + expectQuery({ notContains: { users: ["us_none"] } }).toContainExactly([ + { users: [{ _id: user1._id }] }, + { users: [{ _id: user2._id }] }, + { users: [{ _id: user1._id }, { _id: user2._id }] }, + { users: [] }, + ])) + }) + + describe("containsAny", () => { + it("successfully finds rows", () => + expectQuery({ + containsAny: { users: [user1._id, user2._id] }, + }).toContainExactly([ + { users: [{ _id: user1._id }] }, + { users: [{ _id: user2._id }] }, + { users: [{ _id: user1._id }, { _id: user2._id }] }, + ])) + + it("fails to find nonexistent row", () => + expectQuery({ containsAny: { users: ["us_none"] } }).toFindNothing()) + }) + }) }) diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index 7a2b819007..33e276c81b 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -271,7 +271,7 @@ class InternalBuilder { } statement += (statement ? andOr : "") + - `LOWER(${likeKey(this.client, key)}) LIKE ?` + `COALESCE(LOWER(${likeKey(this.client, key)}) LIKE ?, FALSE)` } if (statement === "") { diff --git a/packages/server/src/sdk/app/rows/search/sqs.ts b/packages/server/src/sdk/app/rows/search/sqs.ts index 05b1a3bd96..7aaaa6bd6c 100644 --- a/packages/server/src/sdk/app/rows/search/sqs.ts +++ b/packages/server/src/sdk/app/rows/search/sqs.ts @@ -172,7 +172,8 @@ export async function search( sql = sql.replace(/`doc2`.`rowId`/g, "`doc2.rowId`") const db = context.getAppDB() - return await db.sql(sql, bindings) + const rows = await db.sql(sql, bindings) + return rows }) // process from the format of tableId.column to expected format From 1562e7b1f10ae920ece4c6006a75bb496dde93bc Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 20 May 2024 12:05:01 +0100 Subject: [PATCH 10/21] Working towards user relationship tests passing. --- packages/server/src/api/routes/tests/search.spec.ts | 2 +- packages/server/src/integrations/base/sql.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index f20e0534e5..f777eb6db1 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -406,7 +406,7 @@ describe.each([ ]) }) - it("should parse the encoded js binding. Return rows with appointments 2 weeks in the past", async () => { + it.only("should parse the encoded js binding. Return rows with appointments 2 weeks in the past", async () => { const jsBinding = "const currentTime = new Date()\ncurrentTime.setDate(currentTime.getDate()-14);\nreturn currentTime.toISOString();" const encodedBinding = encodeJSBinding(jsBinding) diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index 1140a1ac54..1c0c252b1c 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -272,7 +272,8 @@ class InternalBuilder { } statement += (statement ? andOr : "") + - `COALESCE(LOWER(${likeKey(this.client, key)}) LIKE ?, FALSE)` + // `COALESCE(LOWER(${likeKey(this.client, key)}) LIKE ?, FALSE)` + `LOWER(${likeKey(this.client, key)}) LIKE ?` } if (statement === "") { From e2a1ab7eaf40ff7140a0380778fba40b6ca6ce48 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 20 May 2024 17:01:52 +0100 Subject: [PATCH 11/21] All tests passing. --- .../src/api/routes/tests/search.spec.ts | 53 +++++++++++++++---- packages/server/src/integrations/base/sql.ts | 50 +++++++++++------ 2 files changed, 78 insertions(+), 25 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 3886822c21..0321cdf49e 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -22,12 +22,12 @@ import tk from "timekeeper" import { encodeJSBinding } from "@budibase/string-templates" describe.each([ - //["lucene", undefined], + ["lucene", undefined], ["sqs", undefined], - //[DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], - //[DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], - //[DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], - //[DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], + [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], + [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], + [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], + [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], ])("/api/:sourceId/search (%s)", (name, dsProvider) => { const isSqs = name === "sqs" const isLucene = name === "lucene" @@ -1288,6 +1288,7 @@ describe.each([ await createRows([ { user: JSON.stringify(user1) }, { user: JSON.stringify(user2) }, + { user: null }, ]) }) @@ -1305,12 +1306,14 @@ describe.each([ it("successfully finds a row", () => expectQuery({ notEqual: { user: user1._id } }).toContainExactly([ { user: { _id: user2._id } }, + {}, ])) it("fails to find nonexistent row", () => expectQuery({ notEqual: { user: "us_none" } }).toContainExactly([ { user: { _id: user1._id } }, { user: { _id: user2._id } }, + { user: {} }, ])) }) @@ -1323,6 +1326,19 @@ describe.each([ it("fails to find nonexistent row", () => expectQuery({ oneOf: { user: ["us_none"] } }).toFindNothing()) }) + + describe("empty", () => { + it("finds empty rows", () => + expectQuery({ empty: { user: null } }).toContainExactly([{}])) + }) + + describe("notEmpty", () => { + it("finds non-empty rows", () => + expectQuery({ notEmpty: { user: null } }).toContainExactly([ + { user: { _id: user1._id } }, + { user: { _id: user2._id } }, + ])) + }) }) describe("multi user", () => { @@ -1338,14 +1354,19 @@ describe.each([ name: "users", type: FieldType.BB_REFERENCE, subtype: BBReferenceFieldSubType.USER, + constraints: { type: "array" }, + }, + number: { + name: "number", + type: FieldType.NUMBER, }, }) await createRows([ - { users: JSON.stringify([user1]) }, - { users: JSON.stringify([user2]) }, - { users: JSON.stringify([user1, user2]) }, - { users: JSON.stringify([]) }, + { number: 1, users: JSON.stringify([user1]) }, + { number: 2, users: JSON.stringify([user2]) }, + { number: 3, users: JSON.stringify([user1, user2]) }, + { number: 4, users: JSON.stringify([]) }, ]) }) @@ -1389,5 +1410,19 @@ describe.each([ it("fails to find nonexistent row", () => expectQuery({ containsAny: { users: ["us_none"] } }).toFindNothing()) }) + + describe("multi-column equals", () => { + it("successfully finds a row", () => + expectQuery({ + equal: { number: 1 }, + contains: { users: [user1._id] }, + }).toContainExactly([{ users: [{ _id: user1._id }], number: 1 }])) + + it("fails to find nonexistent row", () => + expectQuery({ + equal: { number: 2 }, + contains: { users: [user1._id] }, + }).toFindNothing()) + }) }) }) diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index 1c0c252b1c..c3292cf424 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -226,8 +226,7 @@ class InternalBuilder { } const contains = (mode: object, any: boolean = false) => { - const fnc = allOr ? "orWhere" : "where" - const rawFnc = `${fnc}Raw` + const rawFnc = allOr ? "orWhereRaw" : "whereRaw" const not = mode === filters?.notContains ? "NOT " : "" function stringifyArray(value: Array, quoteStyle = '"'): string { for (let i in value) { @@ -240,24 +239,24 @@ class InternalBuilder { if (this.client === SqlClient.POSTGRES) { iterate(mode, (key: string, value: Array) => { const wrap = any ? "" : "'" - const containsOp = any ? "\\?| array" : "@>" + const op = any ? "\\?| array" : "@>" const fieldNames = key.split(/\./g) - const tableName = fieldNames[0] - const columnName = fieldNames[1] - // @ts-ignore + const table = fieldNames[0] + const col = fieldNames[1] query = query[rawFnc]( - `${not}"${tableName}"."${columnName}"::jsonb ${containsOp} ${wrap}${stringifyArray( + `${not}COALESCE("${table}"."${col}"::jsonb ${op} ${wrap}${stringifyArray( value, any ? "'" : '"' - )}${wrap}` + )}${wrap}, FALSE)` ) }) } else if (this.client === SqlClient.MY_SQL) { const jsonFnc = any ? "JSON_OVERLAPS" : "JSON_CONTAINS" iterate(mode, (key: string, value: Array) => { - // @ts-ignore query = query[rawFnc]( - `${not}${jsonFnc}(${key}, '${stringifyArray(value)}')` + `${not}COALESCE(${jsonFnc}(${key}, '${stringifyArray( + value + )}'), FALSE)` ) }) } else { @@ -272,8 +271,7 @@ class InternalBuilder { } statement += (statement ? andOr : "") + - // `COALESCE(LOWER(${likeKey(this.client, key)}) LIKE ?, FALSE)` - `LOWER(${likeKey(this.client, key)}) LIKE ?` + `COALESCE(LOWER(${likeKey(this.client, key)}), '') LIKE ?` } if (statement === "") { @@ -338,14 +336,34 @@ class InternalBuilder { } if (filters.equal) { iterate(filters.equal, (key, value) => { - const fnc = allOr ? "orWhere" : "where" - query = query[fnc]({ [key]: value }) + const fnc = allOr ? "orWhereRaw" : "whereRaw" + if (this.client === SqlClient.MS_SQL) { + query = query[fnc]( + `CASE WHEN ${likeKey(this.client, key)} = ? THEN 1 ELSE 0 END = 1`, + [value] + ) + } else { + query = query[fnc]( + `COALESCE(${likeKey(this.client, key)} = ?, FALSE)`, + [value] + ) + } }) } if (filters.notEqual) { iterate(filters.notEqual, (key, value) => { - const fnc = allOr ? "orWhereNot" : "whereNot" - query = query[fnc]({ [key]: value }) + const fnc = allOr ? "orWhereRaw" : "whereRaw" + if (this.client === SqlClient.MS_SQL) { + query = query[fnc]( + `CASE WHEN ${likeKey(this.client, key)} = ? THEN 1 ELSE 0 END = 0`, + [value] + ) + } else { + query = query[fnc]( + `COALESCE(${likeKey(this.client, key)} != ?, TRUE)`, + [value] + ) + } }) } if (filters.empty) { From 2131cc689cd01eaaaa5ae7eb3d76de5bd1bf0fe5 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 20 May 2024 17:13:12 +0100 Subject: [PATCH 12/21] Put pro back in line with master. --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index ff397e5454..d3c3077011 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit ff397e5454ad3361b25efdf14746c36dcbd3f409 +Subproject commit d3c3077011a8e20ed3c48dcd6301caca4120b6ac From 789b789c3e01b9eae69df2cd7b6eca2f0237d501 Mon Sep 17 00:00:00 2001 From: Dean Date: Tue, 21 May 2024 14:59:27 +0100 Subject: [PATCH 13/21] Added support for single user column type in automation block --- .../components/automation/SetupPanel/RowSelectorTypes.svelte | 2 +- packages/builder/src/components/common/LinkedRowSelector.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte index 087d0b94fc..0a27360347 100644 --- a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte +++ b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte @@ -99,7 +99,7 @@ on:change={e => onChange(e, field)} useLabel={false} /> -{:else if schema.type === "bb_reference"} +{:else if schema.type === "bb_reference" || schema.type === "bb_reference_single"} {linkedTable.name} table. -{:else if schema.relationshipType === "one-to-many"} +{:else if schema.relationshipType === "one-to-many" || schema.type === "bb_reference_single"} { - if ( - !uneditable && - !(linkEditDisabled && editableColumn.type === LINK_TYPE) - ) { - editableColumn.name = e.target.value - } - }} - disabled={uneditable || - (linkEditDisabled && editableColumn.type === LINK_TYPE)} - error={errors?.name} - /> - {/if} - - {:else if editableColumn.type === FieldType.OPTIONS} - - {:else if editableColumn.type === FieldType.LONGFORM} -
-
- - - - -
- - + + {#if mounted} + { + if ( + !uneditable && + !(linkEditDisabled && editableColumn.type === LINK_TYPE) + ) { + editableColumn.name = e.target.value + } + }} + disabled={uneditable || + (linkEditDisabled && editableColumn.type === LINK_TYPE)} + error={errors?.name} /> -
- {:else if editableColumn.type === FieldType.ARRAY} - - {:else if editableColumn.type === DATE_TYPE && !editableColumn.autocolumn} -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- {#if !editableColumn.timeOnly} - {#if datasource?.source !== SourceName.ORACLE && datasource?.source !== SourceName.SQL_SERVER && !editableColumn.dateOnly} -
-
- - - - -
- -
- {/if} - {/if} - {:else if editableColumn.type === FieldType.NUMBER && !editableColumn.autocolumn} -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- {:else if editableColumn.type === FieldType.LINK && !editableColumn.autocolumn} - field.name} + getOptionValue={field => field.fieldId} + getOptionIcon={field => field.icon} + isOptionEnabled={option => { + if (option.type === AUTO_TYPE) { + return availableAutoColumnKeys?.length > 0 + } + return true + }} /> - {:else if editableColumn.type === FORMULA_TYPE} - {#if !table.sql} + + {#if editableColumn.type === FieldType.STRING} + + {:else if editableColumn.type === FieldType.OPTIONS} + + {:else if editableColumn.type === FieldType.LONGFORM} +
+
+ + + + +
+ + +
+ {:else if editableColumn.type === FieldType.ARRAY} + + {:else if editableColumn.type === DATE_TYPE && !editableColumn.autocolumn}
- +
- (editableColumn.subtype = e.detail)} - options={Object.entries(autoColumnOptions)} - getOptionLabel={option => option[1].name} - getOptionValue={option => option[0]} - disabled={!availableAutoColumnKeys?.length || editableColumn.autocolumn} - error={errors?.subtype} - /> - {/if} - {#if canBeRequired || canBeDisplay} -
- {#if canBeRequired} - +
+
+ +
+
+ +
+
+ {#if !editableColumn.timeOnly} + {#if datasource?.source !== SourceName.ORACLE && datasource?.source !== SourceName.SQL_SERVER && !editableColumn.dateOnly} +
+
+ + + + +
+ +
+ {/if} + {/if} -
- {/if} - - -
- {#if !uneditable && originalName != null} - - {/if} - - + {/if} + + +
+ Date: Thu, 23 May 2024 17:45:01 +0100 Subject: [PATCH 20/21] Fix for tooltips in the Component tree in the builder --- .../builder/src/components/common/NavItem.svelte | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/builder/src/components/common/NavItem.svelte b/packages/builder/src/components/common/NavItem.svelte index 5cc6db65a0..5a04907570 100644 --- a/packages/builder/src/components/common/NavItem.svelte +++ b/packages/builder/src/components/common/NavItem.svelte @@ -1,5 +1,5 @@ - - - {#if mounted} - { - if ( - !uneditable && - !(linkEditDisabled && editableColumn.type === LINK_TYPE) - ) { - editableColumn.name = e.target.value - } - }} - disabled={uneditable || - (linkEditDisabled && editableColumn.type === LINK_TYPE)} - error={errors?.name} - /> - {/if} - { + if ( + !uneditable && + !(linkEditDisabled && editableColumn.type === LINK_TYPE) + ) { + editableColumn.name = e.target.value } - return true }} + disabled={uneditable || + (linkEditDisabled && editableColumn.type === LINK_TYPE)} + error={errors?.name} /> + {/if} + - {:else if editableColumn.type === FieldType.OPTIONS} - - {:else if editableColumn.type === FieldType.LONGFORM} -
-
- - - - -
+ {#if editableColumn.type === FieldType.STRING} + + {:else if editableColumn.type === FieldType.OPTIONS} + + {:else if editableColumn.type === FieldType.LONGFORM} +
+
+ + + + +
- +
+ {:else if editableColumn.type === FieldType.ARRAY} + + {:else if editableColumn.type === DATE_TYPE && !editableColumn.autocolumn} +
+
+ +
+
+
- {:else if editableColumn.type === FieldType.ARRAY} - - {:else if editableColumn.type === DATE_TYPE && !editableColumn.autocolumn} -
-
- -
-
- -
-
+
-
-
- -
-
- -
+
+
+
- {#if !editableColumn.timeOnly} - {#if datasource?.source !== SourceName.ORACLE && datasource?.source !== SourceName.SQL_SERVER && !editableColumn.dateOnly} -
-
- - - - -
- +
+ +
+
+ {#if !editableColumn.timeOnly} + {#if datasource?.source !== SourceName.ORACLE && datasource?.source !== SourceName.SQL_SERVER && !editableColumn.dateOnly} +
+
+ + + +
- {/if} - - {/if} - {:else if editableColumn.type === FieldType.NUMBER && !editableColumn.autocolumn} -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- {:else if editableColumn.type === FieldType.LINK && !editableColumn.autocolumn} - - {:else if editableColumn.type === FORMULA_TYPE} - {#if !table.sql} -
-
- -
-
- (editableColumn.subtype = e.detail)} - options={Object.entries(autoColumnOptions)} - getOptionLabel={option => option[1].name} - getOptionValue={option => option[0]} - disabled={!availableAutoColumnKeys?.length || editableColumn.autocolumn} - error={errors?.subtype} - /> - {/if} - - {#if canBeRequired || canBeDisplay} -
- {#if canBeRequired} - {/if} +
+ {/if} + + {/if} + {:else if editableColumn.type === FieldType.NUMBER && !editableColumn.autocolumn} +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ {:else if editableColumn.type === FieldType.LINK && !editableColumn.autocolumn} + + {:else if editableColumn.type === FORMULA_TYPE} + {#if !table.sql} +
+
+ +
+
+ (editableColumn.subtype = e.detail)} + options={Object.entries(autoColumnOptions)} + getOptionLabel={option => option[1].name} + getOptionValue={option => option[0]} + disabled={!availableAutoColumnKeys?.length || editableColumn.autocolumn} + error={errors?.subtype} + /> + {/if} + + {#if canBeRequired || canBeDisplay} +
+ {#if canBeRequired} + {/if} - -
- +
+ {/if} + + +
+ {#if !uneditable && originalName != null} + + {/if} + + +