diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index f902084a48..89da4175d9 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -50,6 +50,7 @@ describe.each([ ["postgres", databaseTestProviders.postgres], ["mysql", databaseTestProviders.mysql], ["mssql", databaseTestProviders.mssql], + ["mariadb", databaseTestProviders.mariadb], ])("/rows (%s)", (__, dsProvider) => { const isInternal = !dsProvider diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index be1883c8ec..e6b9f047b9 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -4,11 +4,15 @@ import { QueryOptions } from "../../definitions/datasource" import { isIsoDateString, SqlClient, isValidFilter } from "../utils" import SqlTableQueryBuilder from "./sqlTable" import { + FieldSchema, + FieldSubtype, + FieldType, Operation, QueryJson, RelationshipsJson, SearchFilters, SortDirection, + Table, } from "@budibase/types" import environment from "../../environment" @@ -691,6 +695,35 @@ class SqlQueryBuilder extends SqlTableQueryBuilder { return results.length ? results : [{ [operation.toLowerCase()]: true }] } + convertJsonStringColumns( + table: Table, + results: Record[] + ): Record[] { + for (const [name, field] of Object.entries(table.schema)) { + if (!this._isJsonColumn(field)) { + continue + } + const fullName = `${table.name}.${name}` + for (let row of results) { + if (typeof row[fullName] === "string") { + row[fullName] = JSON.parse(row[fullName]) + } + if (typeof row[name] === "string") { + row[name] = JSON.parse(row[name]) + } + } + } + return results + } + + _isJsonColumn(field: FieldSchema) { + return ( + field.type === FieldType.JSON || + (field.type === FieldType.BB_REFERENCE && + field.subtype === FieldSubtype.USERS) + ) + } + log(query: string, values?: any[]) { if (!environment.SQL_LOGGING_ENABLE) { return diff --git a/packages/server/src/integrations/microsoftSqlServer.ts b/packages/server/src/integrations/microsoftSqlServer.ts index e53d2ddc44..0c9b8f4547 100644 --- a/packages/server/src/integrations/microsoftSqlServer.ts +++ b/packages/server/src/integrations/microsoftSqlServer.ts @@ -504,33 +504,15 @@ class SqlServerIntegration extends Sql implements DatasourcePlus { } const operation = this._operation(json) const queryFn = (query: any, op: string) => this.internalQuery(query, op) - const processFn = (result: any) => - result.recordset - ? this._postProcessJson(json, result.recordset) - : [{ [operation]: true }] - return this.queryWithReturning(json, queryFn, processFn) - } - - _postProcessJson(json: QueryJson, results: IRecordSet) { - const table = json.meta?.table - if (!table) { - return results - } - for (const [name, field] of Object.entries(table.schema)) { - if ( - field.type === FieldType.JSON || - (field.type === FieldType.BB_REFERENCE && - field.subtype === FieldSubtype.USERS) - ) { - const fullName = `${table.name}.${name}` - for (let row of results) { - if (typeof row[fullName] === "string") { - row[fullName] = JSON.parse(row[fullName]) - } - } + const processFn = (result: any) => { + if (json?.meta?.table && result.recordset) { + return this.convertJsonStringColumns(json.meta.table, result.recordset) + } else if (result.recordset) { + return result.recordset } + return [{ [operation]: true }] } - return results + return this.queryWithReturning(json, queryFn, processFn) } async getExternalSchema() { diff --git a/packages/server/src/integrations/mysql.ts b/packages/server/src/integrations/mysql.ts index f629381807..1f50525675 100644 --- a/packages/server/src/integrations/mysql.ts +++ b/packages/server/src/integrations/mysql.ts @@ -13,6 +13,8 @@ import { Schema, TableSourceType, DatasourcePlusQueryResponse, + FieldType, + FieldSubtype, } from "@budibase/types" import { getSqlQuery, @@ -386,12 +388,40 @@ class MySQLIntegration extends Sql implements DatasourcePlus { try { const queryFn = (query: any) => this.internalQuery(query, { connect: false, disableCoercion: true }) - return await this.queryWithReturning(json, queryFn) + const processFn = (result: any) => { + if (json?.meta?.table && Array.isArray(result)) { + return this.convertJsonStringColumns(json.meta.table, result) + } + return result + } + return await this.queryWithReturning(json, queryFn, processFn) } finally { await this.disconnect() } } + _postProcessJson(json: QueryJson, results: any) { + const table = json.meta?.table + if (!table) { + return results + } + for (const [name, field] of Object.entries(table.schema)) { + if ( + field.type === FieldType.JSON || + (field.type === FieldType.BB_REFERENCE && + field.subtype === FieldSubtype.USERS) + ) { + const fullName = `${table.name}.${name}` + for (let row of results) { + if (typeof row[fullName] === "string") { + row[fullName] = JSON.parse(row[fullName]) + } + } + } + } + return results + } + async getExternalSchema() { try { const [databaseResult] = await this.internalQuery({ diff --git a/packages/server/src/integrations/tests/utils/index.ts b/packages/server/src/integrations/tests/utils/index.ts index a5282bff6f..b2be3df4e0 100644 --- a/packages/server/src/integrations/tests/utils/index.ts +++ b/packages/server/src/integrations/tests/utils/index.ts @@ -5,6 +5,7 @@ import * as postgres from "./postgres" import * as mongodb from "./mongodb" import * as mysql from "./mysql" import * as mssql from "./mssql" +import * as mariadb from "./mariadb" import { StartedTestContainer } from "testcontainers" jest.setTimeout(30000) @@ -15,4 +16,10 @@ export interface DatabaseProvider { datasource(): Promise } -export const databaseTestProviders = { postgres, mongodb, mysql, mssql } +export const databaseTestProviders = { + postgres, + mongodb, + mysql, + mssql, + mariadb, +} diff --git a/packages/server/src/integrations/tests/utils/mariadb.ts b/packages/server/src/integrations/tests/utils/mariadb.ts new file mode 100644 index 0000000000..a097e0aaa1 --- /dev/null +++ b/packages/server/src/integrations/tests/utils/mariadb.ts @@ -0,0 +1,58 @@ +import { Datasource, SourceName } from "@budibase/types" +import { GenericContainer, Wait, StartedTestContainer } from "testcontainers" +import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy" + +let container: StartedTestContainer | undefined + +class MariaDBWaitStrategy extends AbstractWaitStrategy { + async waitUntilReady(container: any, boundPorts: any, startTime?: Date) { + // Because MariaDB first starts itself up, runs an init script, then restarts, + // it's possible for the mysqladmin ping to succeed early and then tests to + // run against a MariaDB that's mid-restart and fail. To get around this, we + // wait for logs and then do a ping check. + + const logs = Wait.forLogMessage("mariadbd: ready for connections", 2) + await logs.waitUntilReady(container, boundPorts, startTime) + + const command = Wait.forSuccessfulCommand( + `mysqladmin ping -h localhost -P 3306 -u root -ppassword` + ) + await command.waitUntilReady(container) + } +} + +export async function start(): Promise { + return await new GenericContainer("mariadb:lts") + .withExposedPorts(3306) + .withEnvironment({ MARIADB_ROOT_PASSWORD: "password" }) + .withWaitStrategy(new MariaDBWaitStrategy()) + .start() +} + +export async function datasource(): Promise { + if (!container) { + container = await start() + } + const host = container.getHost() + const port = container.getMappedPort(3306) + + return { + type: "datasource_plus", + source: SourceName.MYSQL, + plus: true, + config: { + host, + port, + user: "root", + password: "password", + database: "mysql", + }, + } +} + +export async function stop() { + if (container) { + await container.stop() + container = undefined + } +}