Merge pull request #13238 from Budibase/budi-8067-sql-testing-more-datasource-types
Update row.spec.ts to test against more real databases.
This commit is contained in:
commit
5f563a9b93
|
@ -38,11 +38,18 @@ import * as uuid from "uuid"
|
||||||
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
|
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
|
||||||
tk.freeze(timestamp)
|
tk.freeze(timestamp)
|
||||||
|
|
||||||
|
jest.unmock("mysql2")
|
||||||
|
jest.unmock("mysql2/promise")
|
||||||
|
jest.unmock("mssql")
|
||||||
|
|
||||||
const { basicRow } = setup.structures
|
const { basicRow } = setup.structures
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
["internal", undefined],
|
["internal", undefined],
|
||||||
["postgres", databaseTestProviders.postgres],
|
["postgres", databaseTestProviders.postgres],
|
||||||
|
["mysql", databaseTestProviders.mysql],
|
||||||
|
["mssql", databaseTestProviders.mssql],
|
||||||
|
["mariadb", databaseTestProviders.mariadb],
|
||||||
])("/rows (%s)", (__, dsProvider) => {
|
])("/rows (%s)", (__, dsProvider) => {
|
||||||
const isInternal = !dsProvider
|
const isInternal = !dsProvider
|
||||||
|
|
||||||
|
@ -70,7 +77,7 @@ describe.each([
|
||||||
|
|
||||||
const generateTableConfig: () => SaveTableRequest = () => {
|
const generateTableConfig: () => SaveTableRequest = () => {
|
||||||
return {
|
return {
|
||||||
name: uuid.v4(),
|
name: uuid.v4().substring(0, 16),
|
||||||
type: "table",
|
type: "table",
|
||||||
primary: ["id"],
|
primary: ["id"],
|
||||||
primaryDisplay: "name",
|
primaryDisplay: "name",
|
||||||
|
@ -467,7 +474,6 @@ describe.each([
|
||||||
const createRowResponse = await config.api.row.save(
|
const createRowResponse = await config.api.row.save(
|
||||||
createViewResponse.id,
|
createViewResponse.id,
|
||||||
{
|
{
|
||||||
OrderID: "1111",
|
|
||||||
Country: "Aussy",
|
Country: "Aussy",
|
||||||
Story: "aaaaa",
|
Story: "aaaaa",
|
||||||
}
|
}
|
||||||
|
@ -477,7 +483,7 @@ describe.each([
|
||||||
expect(row.Story).toBeUndefined()
|
expect(row.Story).toBeUndefined()
|
||||||
expect(row).toEqual({
|
expect(row).toEqual({
|
||||||
...defaultRowFields,
|
...defaultRowFields,
|
||||||
OrderID: 1111,
|
OrderID: createRowResponse.OrderID,
|
||||||
Country: "Aussy",
|
Country: "Aussy",
|
||||||
_id: createRowResponse._id,
|
_id: createRowResponse._id,
|
||||||
_rev: createRowResponse._rev,
|
_rev: createRowResponse._rev,
|
||||||
|
@ -641,7 +647,7 @@ describe.each([
|
||||||
const createdRow = await config.createRow()
|
const createdRow = await config.createRow()
|
||||||
|
|
||||||
const res = await config.api.row.bulkDelete(table._id!, {
|
const res = await config.api.row.bulkDelete(table._id!, {
|
||||||
rows: [createdRow, { _id: "2" }],
|
rows: [createdRow, { _id: "9999999" }],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(res[0]._id).toEqual(createdRow._id)
|
expect(res[0]._id).toEqual(createdRow._id)
|
||||||
|
|
|
@ -4,11 +4,17 @@ import { QueryOptions } from "../../definitions/datasource"
|
||||||
import { isIsoDateString, SqlClient, isValidFilter } from "../utils"
|
import { isIsoDateString, SqlClient, isValidFilter } from "../utils"
|
||||||
import SqlTableQueryBuilder from "./sqlTable"
|
import SqlTableQueryBuilder from "./sqlTable"
|
||||||
import {
|
import {
|
||||||
|
BBReferenceFieldMetadata,
|
||||||
|
FieldSchema,
|
||||||
|
FieldSubtype,
|
||||||
|
FieldType,
|
||||||
|
JsonFieldMetadata,
|
||||||
Operation,
|
Operation,
|
||||||
QueryJson,
|
QueryJson,
|
||||||
RelationshipsJson,
|
RelationshipsJson,
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
SortDirection,
|
SortDirection,
|
||||||
|
Table,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import environment from "../../environment"
|
import environment from "../../environment"
|
||||||
|
|
||||||
|
@ -691,6 +697,37 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
|
||||||
return results.length ? results : [{ [operation.toLowerCase()]: true }]
|
return results.length ? results : [{ [operation.toLowerCase()]: true }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
convertJsonStringColumns(
|
||||||
|
table: Table,
|
||||||
|
results: Record<string, any>[]
|
||||||
|
): Record<string, any>[] {
|
||||||
|
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
|
||||||
|
): field is JsonFieldMetadata | BBReferenceFieldMetadata {
|
||||||
|
return (
|
||||||
|
field.type === FieldType.JSON ||
|
||||||
|
(field.type === FieldType.BB_REFERENCE &&
|
||||||
|
field.subtype === FieldSubtype.USERS)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
log(query: string, values?: any[]) {
|
log(query: string, values?: any[]) {
|
||||||
if (!environment.SQL_LOGGING_ENABLE) {
|
if (!environment.SQL_LOGGING_ENABLE) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -14,6 +14,8 @@ import {
|
||||||
Schema,
|
Schema,
|
||||||
TableSourceType,
|
TableSourceType,
|
||||||
DatasourcePlusQueryResponse,
|
DatasourcePlusQueryResponse,
|
||||||
|
FieldType,
|
||||||
|
FieldSubtype,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
getSqlQuery,
|
getSqlQuery,
|
||||||
|
@ -502,8 +504,14 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
|
||||||
}
|
}
|
||||||
const operation = this._operation(json)
|
const operation = this._operation(json)
|
||||||
const queryFn = (query: any, op: string) => this.internalQuery(query, op)
|
const queryFn = (query: any, op: string) => this.internalQuery(query, op)
|
||||||
const processFn = (result: any) =>
|
const processFn = (result: any) => {
|
||||||
result.recordset ? result.recordset : [{ [operation]: true }]
|
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 this.queryWithReturning(json, queryFn, processFn)
|
return this.queryWithReturning(json, queryFn, processFn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
Schema,
|
Schema,
|
||||||
TableSourceType,
|
TableSourceType,
|
||||||
DatasourcePlusQueryResponse,
|
DatasourcePlusQueryResponse,
|
||||||
|
FieldType,
|
||||||
|
FieldSubtype,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
getSqlQuery,
|
getSqlQuery,
|
||||||
|
@ -386,7 +388,13 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
||||||
try {
|
try {
|
||||||
const queryFn = (query: any) =>
|
const queryFn = (query: any) =>
|
||||||
this.internalQuery(query, { connect: false, disableCoercion: true })
|
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 {
|
} finally {
|
||||||
await this.disconnect()
|
await this.disconnect()
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { Datasource } from "@budibase/types"
|
||||||
import * as postgres from "./postgres"
|
import * as postgres from "./postgres"
|
||||||
import * as mongodb from "./mongodb"
|
import * as mongodb from "./mongodb"
|
||||||
import * as mysql from "./mysql"
|
import * as mysql from "./mysql"
|
||||||
|
import * as mssql from "./mssql"
|
||||||
|
import * as mariadb from "./mariadb"
|
||||||
import { StartedTestContainer } from "testcontainers"
|
import { StartedTestContainer } from "testcontainers"
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
@ -14,4 +16,10 @@ export interface DatabaseProvider {
|
||||||
datasource(): Promise<Datasource>
|
datasource(): Promise<Datasource>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const databaseTestProviders = { postgres, mongodb, mysql }
|
export const databaseTestProviders = {
|
||||||
|
postgres,
|
||||||
|
mongodb,
|
||||||
|
mysql,
|
||||||
|
mssql,
|
||||||
|
mariadb,
|
||||||
|
}
|
||||||
|
|
|
@ -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<StartedTestContainer> {
|
||||||
|
return await new GenericContainer("mariadb:lts")
|
||||||
|
.withExposedPorts(3306)
|
||||||
|
.withEnvironment({ MARIADB_ROOT_PASSWORD: "password" })
|
||||||
|
.withWaitStrategy(new MariaDBWaitStrategy())
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function datasource(): Promise<Datasource> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Datasource, SourceName } from "@budibase/types"
|
||||||
|
import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
|
||||||
|
|
||||||
|
let container: StartedTestContainer | undefined
|
||||||
|
|
||||||
|
export async function start(): Promise<StartedTestContainer> {
|
||||||
|
return await new GenericContainer(
|
||||||
|
"mcr.microsoft.com/mssql/server:2022-latest"
|
||||||
|
)
|
||||||
|
.withExposedPorts(1433)
|
||||||
|
.withEnvironment({
|
||||||
|
ACCEPT_EULA: "Y",
|
||||||
|
MSSQL_SA_PASSWORD: "Password_123",
|
||||||
|
// This is important, as Microsoft allow us to use the "Developer" edition
|
||||||
|
// of SQL Server for development and testing purposes. We can't use other
|
||||||
|
// versions without a valid license, and we cannot use the Developer
|
||||||
|
// version in production.
|
||||||
|
MSSQL_PID: "Developer",
|
||||||
|
})
|
||||||
|
.withWaitStrategy(
|
||||||
|
Wait.forSuccessfulCommand(
|
||||||
|
"/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Password_123 -q 'SELECT 1'"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function datasource(): Promise<Datasource> {
|
||||||
|
if (!container) {
|
||||||
|
container = await start()
|
||||||
|
}
|
||||||
|
const host = container.getHost()
|
||||||
|
const port = container.getMappedPort(1433)
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "datasource_plus",
|
||||||
|
source: SourceName.SQL_SERVER,
|
||||||
|
plus: true,
|
||||||
|
config: {
|
||||||
|
server: host,
|
||||||
|
port,
|
||||||
|
user: "sa",
|
||||||
|
password: "Password_123",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stop() {
|
||||||
|
if (container) {
|
||||||
|
await container.stop()
|
||||||
|
container = undefined
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,4 @@
|
||||||
import {
|
import { Row, SearchFilters, SearchParams, SortOrder } from "@budibase/types"
|
||||||
Row,
|
|
||||||
SearchFilters,
|
|
||||||
SearchParams,
|
|
||||||
SortOrder,
|
|
||||||
SortType,
|
|
||||||
} from "@budibase/types"
|
|
||||||
import { isExternalTableID } from "../../../integrations/utils"
|
import { isExternalTableID } from "../../../integrations/utils"
|
||||||
import * as internal from "./search/internal"
|
import * as internal from "./search/internal"
|
||||||
import * as external from "./search/external"
|
import * as external from "./search/external"
|
||||||
|
|
Loading…
Reference in New Issue