1
0
Fork 0
mirror of synced 2024-07-09 00:06:05 +12:00

Give SQL integrations their own database when fetching a new datasource.

This commit is contained in:
Sam Rose 2024-03-27 15:25:37 +00:00
parent c5dfd0c9ba
commit 831c174362
No known key found for this signature in database
10 changed files with 162 additions and 74 deletions

View file

@ -1,3 +1,4 @@
import Chance from "./Chance" import Chance from "./Chance"
export const generator = new Chance() export const generator = new Chance()

View file

@ -1,6 +1,8 @@
#!/bin/bash #!/bin/bash
set -e set -e
export DEBUG=testcontainers*
if [[ -n $CI ]] if [[ -n $CI ]]
then then
export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot $NODE_OPTIONS" export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot $NODE_OPTIONS"

View file

@ -3,6 +3,7 @@ import * as setup from "../utilities"
import { import {
DatabaseName, DatabaseName,
getDatasource, getDatasource,
rawQuery,
} from "../../../../integrations/tests/utils" } from "../../../../integrations/tests/utils"
import pg from "pg" import pg from "pg"
import mysql from "mysql2/promise" import mysql from "mysql2/promise"
@ -46,6 +47,7 @@ describe.each(
].map(name => [name, getDatasource(name)]) ].map(name => [name, getDatasource(name)])
)("queries (%s)", (dbName, dsProvider) => { )("queries (%s)", (dbName, dsProvider) => {
const config = setup.getConfig() const config = setup.getConfig()
let rawDatasource: Datasource
let datasource: Datasource let datasource: Datasource
async function createQuery(query: Partial<Query>): Promise<Query> { async function createQuery(query: Partial<Query>): Promise<Query> {
@ -62,56 +64,19 @@ describe.each(
return await config.api.query.save({ ...defaultQuery, ...query }) return await config.api.query.save({ ...defaultQuery, ...query })
} }
async function rawQuery(sql: string): Promise<any> {
// We re-fetch the datasource here because the one returned by
// config.api.datasource.create has the password field blanked out, and we
// need the password to connect to the database.
const ds = await dsProvider
switch (ds.source) {
case SourceName.POSTGRES: {
const client = new pg.Client(ds.config!)
await client.connect()
try {
const { rows } = await client.query(sql)
return rows
} finally {
await client.end()
}
}
case SourceName.MYSQL: {
const con = await mysql.createConnection(ds.config!)
try {
const [rows] = await con.query(sql)
return rows
} finally {
con.end()
}
}
case SourceName.SQL_SERVER: {
const pool = new mssql.ConnectionPool(ds.config! as mssql.config)
const client = await pool.connect()
try {
const { recordset } = await client.query(sql)
return recordset
} finally {
await pool.close()
}
}
}
}
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
datasource = await config.api.datasource.create(await dsProvider) rawDatasource = await dsProvider
datasource = await config.api.datasource.create(rawDatasource)
}) })
beforeEach(async () => { beforeEach(async () => {
await rawQuery(createTableSQL[datasource.source]) await rawQuery(rawDatasource, createTableSQL[datasource.source])
await rawQuery(insertSQL) await rawQuery(rawDatasource, insertSQL)
}) })
afterEach(async () => { afterEach(async () => {
await rawQuery(dropTableSQL) await rawQuery(rawDatasource, dropTableSQL)
}) })
afterAll(async () => { afterAll(async () => {
@ -145,7 +110,10 @@ describe.each(
}, },
]) ])
const rows = await rawQuery("SELECT * FROM test_table WHERE name = 'baz'") const rows = await rawQuery(
rawDatasource,
"SELECT * FROM test_table WHERE name = 'baz'"
)
expect(rows).toHaveLength(1) expect(rows).toHaveLength(1)
}) })
@ -173,6 +141,7 @@ describe.each(
expect(result.data).toEqual([{ created: true }]) expect(result.data).toEqual([{ created: true }])
const rows = await rawQuery( const rows = await rawQuery(
rawDatasource,
`SELECT * FROM test_table WHERE birthday = '${date.toISOString()}'` `SELECT * FROM test_table WHERE birthday = '${date.toISOString()}'`
) )
expect(rows).toHaveLength(1) expect(rows).toHaveLength(1)
@ -204,6 +173,7 @@ describe.each(
expect(result.data).toEqual([{ created: true }]) expect(result.data).toEqual([{ created: true }])
const rows = await rawQuery( const rows = await rawQuery(
rawDatasource,
`SELECT * FROM test_table WHERE name = '${notDateStr}'` `SELECT * FROM test_table WHERE name = '${notDateStr}'`
) )
expect(rows).toHaveLength(1) expect(rows).toHaveLength(1)
@ -340,7 +310,10 @@ describe.each(
}, },
]) ])
const rows = await rawQuery("SELECT * FROM test_table WHERE id = 1") const rows = await rawQuery(
rawDatasource,
"SELECT * FROM test_table WHERE id = 1"
)
expect(rows).toEqual([ expect(rows).toEqual([
{ id: 1, name: "foo", birthday: null, number: null }, { id: 1, name: "foo", birthday: null, number: null },
]) ])
@ -408,7 +381,10 @@ describe.each(
}, },
]) ])
const rows = await rawQuery("SELECT * FROM test_table WHERE id = 1") const rows = await rawQuery(
rawDatasource,
"SELECT * FROM test_table WHERE id = 1"
)
expect(rows).toHaveLength(0) expect(rows).toHaveLength(0)
}) })
}) })

View file

@ -18,6 +18,13 @@ import { generator } from "@budibase/backend-core/tests"
// @ts-ignore // @ts-ignore
fetch.mockSearch() fetch.mockSearch()
function uniqueTableName(length?: number): string {
return generator
.guid()
.replaceAll("-", "_")
.substring(0, length || 10)
}
const config = setup.getConfig()! const config = setup.getConfig()!
jest.mock("../websockets", () => ({ jest.mock("../websockets", () => ({
@ -53,7 +60,7 @@ describe("mysql integrations", () => {
beforeEach(async () => { beforeEach(async () => {
primaryMySqlTable = await config.createTable({ primaryMySqlTable = await config.createTable({
name: generator.guid().replaceAll("-", "_").substring(0, 10), name: uniqueTableName(),
type: "table", type: "table",
primary: ["id"], primary: ["id"],
schema: { schema: {
@ -249,7 +256,7 @@ describe("mysql integrations", () => {
const addColumnToTable: TableRequest = { const addColumnToTable: TableRequest = {
type: "table", type: "table",
sourceType: TableSourceType.EXTERNAL, sourceType: TableSourceType.EXTERNAL,
name: generator.guid().replaceAll("-", "_").substring(0, 10), name: uniqueTableName(),
sourceId: mysqlDatasource._id!, sourceId: mysqlDatasource._id!,
primary: ["id"], primary: ["id"],
schema: { schema: {

View file

@ -1,11 +1,11 @@
jest.unmock("pg") jest.unmock("pg")
import { Datasource } from "@budibase/types" import { Datasource, SourceName } from "@budibase/types"
import { postgres } from "./postgres" import * as postgres from "./postgres"
import { mongodb } from "./mongodb" import * as mongodb from "./mongodb"
import { mysql } from "./mysql" import * as mysql from "./mysql"
import { mssql } from "./mssql" import * as mssql from "./mssql"
import { mariadb } from "./mariadb" import * as mariadb from "./mariadb"
export type DatasourceProvider = () => Promise<Datasource> export type DatasourceProvider = () => Promise<Datasource>
@ -18,11 +18,11 @@ export enum DatabaseName {
} }
const providers: Record<DatabaseName, DatasourceProvider> = { const providers: Record<DatabaseName, DatasourceProvider> = {
[DatabaseName.POSTGRES]: postgres, [DatabaseName.POSTGRES]: postgres.getDatasource,
[DatabaseName.MONGODB]: mongodb, [DatabaseName.MONGODB]: mongodb.getDatasource,
[DatabaseName.MYSQL]: mysql, [DatabaseName.MYSQL]: mysql.getDatasource,
[DatabaseName.SQL_SERVER]: mssql, [DatabaseName.SQL_SERVER]: mssql.getDatasource,
[DatabaseName.MARIADB]: mariadb, [DatabaseName.MARIADB]: mariadb.getDatasource,
} }
export function getDatasourceProviders( export function getDatasourceProviders(
@ -46,3 +46,20 @@ export async function getDatasources(
): Promise<Datasource[]> { ): Promise<Datasource[]> {
return Promise.all(sourceNames.map(sourceName => providers[sourceName]())) return Promise.all(sourceNames.map(sourceName => providers[sourceName]()))
} }
export async function rawQuery(ds: Datasource, sql: string): Promise<any> {
switch (ds.source) {
case SourceName.POSTGRES: {
return postgres.rawQuery(ds, sql)
}
case SourceName.MYSQL: {
return mysql.rawQuery(ds, sql)
}
case SourceName.SQL_SERVER: {
return mssql.rawQuery(ds, sql)
}
default: {
throw new Error(`Unsupported source: ${ds.source}`)
}
}
}

View file

@ -1,6 +1,8 @@
import { Datasource, SourceName } from "@budibase/types" import { Datasource, SourceName } from "@budibase/types"
import { GenericContainer, Wait } from "testcontainers" import { GenericContainer, Wait } from "testcontainers"
import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy" import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy"
import { rawQuery } from "./mysql"
import { generator } from "@budibase/backend-core/tests"
class MariaDBWaitStrategy extends AbstractWaitStrategy { class MariaDBWaitStrategy extends AbstractWaitStrategy {
async waitUntilReady(container: any, boundPorts: any, startTime?: Date) { async waitUntilReady(container: any, boundPorts: any, startTime?: Date) {
@ -19,7 +21,7 @@ class MariaDBWaitStrategy extends AbstractWaitStrategy {
} }
} }
export async function mariadb(): Promise<Datasource> { export async function getDatasource(): Promise<Datasource> {
const container = await new GenericContainer("mariadb:lts") const container = await new GenericContainer("mariadb:lts")
.withName("budibase-test-mariadb") .withName("budibase-test-mariadb")
.withReuse() .withReuse()
@ -31,16 +33,23 @@ export async function mariadb(): Promise<Datasource> {
const host = container.getHost() const host = container.getHost()
const port = container.getMappedPort(3306) const port = container.getMappedPort(3306)
return { const config = {
host,
port,
user: "root",
password: "password",
database: "mysql",
}
const datasource = {
type: "datasource_plus", type: "datasource_plus",
source: SourceName.MYSQL, source: SourceName.MYSQL,
plus: true, plus: true,
config: { config,
host,
port,
user: "root",
password: "password",
database: "mysql",
},
} }
const database = generator.guid().replaceAll("-", "")
await rawQuery(datasource, `CREATE DATABASE \`${database}\``)
datasource.config.database = database
return datasource
} }

View file

@ -1,7 +1,7 @@
import { Datasource, SourceName } from "@budibase/types" import { Datasource, SourceName } from "@budibase/types"
import { GenericContainer, Wait } from "testcontainers" import { GenericContainer, Wait } from "testcontainers"
export async function mongodb(): Promise<Datasource> { export async function getDatasource(): Promise<Datasource> {
const container = await new GenericContainer("mongo:7.0-jammy") const container = await new GenericContainer("mongo:7.0-jammy")
.withName("budibase-test-mongodb") .withName("budibase-test-mongodb")
.withReuse() .withReuse()

View file

@ -1,7 +1,9 @@
import { Datasource, SourceName } from "@budibase/types" import { Datasource, SourceName } from "@budibase/types"
import { GenericContainer, Wait } from "testcontainers" import { GenericContainer, Wait } from "testcontainers"
import mssql from "mssql"
import { generator } from "@budibase/backend-core/tests"
export async function mssql(): Promise<Datasource> { export async function getDatasource(): Promise<Datasource> {
const container = await new GenericContainer( const container = await new GenericContainer(
"mcr.microsoft.com/mssql/server:2022-latest" "mcr.microsoft.com/mssql/server:2022-latest"
) )
@ -27,7 +29,7 @@ export async function mssql(): Promise<Datasource> {
const host = container.getHost() const host = container.getHost()
const port = container.getMappedPort(1433) const port = container.getMappedPort(1433)
return { const datasource: Datasource = {
type: "datasource_plus", type: "datasource_plus",
source: SourceName.SQL_SERVER, source: SourceName.SQL_SERVER,
plus: true, plus: true,
@ -41,4 +43,28 @@ export async function mssql(): Promise<Datasource> {
}, },
}, },
} }
const database = generator.guid().replaceAll("-", "")
await rawQuery(datasource, `CREATE DATABASE "${database}"`)
datasource.config!.database = database
return datasource
}
export async function rawQuery(ds: Datasource, sql: string) {
if (!ds.config) {
throw new Error("Datasource config is missing")
}
if (ds.source !== SourceName.SQL_SERVER) {
throw new Error("Datasource source is not SQL Server")
}
const pool = new mssql.ConnectionPool(ds.config! as mssql.config)
const client = await pool.connect()
try {
const { recordset } = await client.query(sql)
return recordset
} finally {
await pool.close()
}
} }

View file

@ -1,6 +1,8 @@
import { Datasource, SourceName } from "@budibase/types" import { Datasource, SourceName } from "@budibase/types"
import { GenericContainer, Wait } from "testcontainers" import { GenericContainer, Wait } from "testcontainers"
import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy" import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy"
import mysql from "mysql2/promise"
import { generator } from "@budibase/backend-core/tests"
class MySQLWaitStrategy extends AbstractWaitStrategy { class MySQLWaitStrategy extends AbstractWaitStrategy {
async waitUntilReady(container: any, boundPorts: any, startTime?: Date) { async waitUntilReady(container: any, boundPorts: any, startTime?: Date) {
@ -22,7 +24,7 @@ class MySQLWaitStrategy extends AbstractWaitStrategy {
} }
} }
export async function mysql(): Promise<Datasource> { export async function getDatasource(): Promise<Datasource> {
const container = await new GenericContainer("mysql:8.3") const container = await new GenericContainer("mysql:8.3")
.withName("budibase-test-mysql") .withName("budibase-test-mysql")
.withReuse() .withReuse()
@ -33,7 +35,7 @@ export async function mysql(): Promise<Datasource> {
const host = container.getHost() const host = container.getHost()
const port = container.getMappedPort(3306) const port = container.getMappedPort(3306)
return { const datasource: Datasource = {
type: "datasource_plus", type: "datasource_plus",
source: SourceName.MYSQL, source: SourceName.MYSQL,
plus: true, plus: true,
@ -45,4 +47,26 @@ export async function mysql(): Promise<Datasource> {
database: "mysql", database: "mysql",
}, },
} }
const database = generator.guid().replaceAll("-", "")
await rawQuery(datasource, `CREATE DATABASE \`${database}\``)
datasource.config!.database = database
return datasource
}
export async function rawQuery(ds: Datasource, sql: string) {
if (!ds.config) {
throw new Error("Datasource config is missing")
}
if (ds.source !== SourceName.MYSQL) {
throw new Error("Datasource source is not MySQL")
}
const connection = await mysql.createConnection(ds.config)
try {
const [rows] = await connection.query(sql)
return rows
} finally {
connection.end()
}
} }

View file

@ -1,7 +1,9 @@
import { Datasource, SourceName } from "@budibase/types" import { Datasource, SourceName } from "@budibase/types"
import { GenericContainer, Wait } from "testcontainers" import { GenericContainer, Wait } from "testcontainers"
import pg from "pg"
import { generator } from "@budibase/backend-core/tests"
export async function postgres(): Promise<Datasource> { export async function getDatasource(): Promise<Datasource> {
const container = await new GenericContainer("postgres:16.1-bullseye") const container = await new GenericContainer("postgres:16.1-bullseye")
.withName("budibase-test-postgres") .withName("budibase-test-postgres")
.withReuse() .withReuse()
@ -16,7 +18,7 @@ export async function postgres(): Promise<Datasource> {
const host = container.getHost() const host = container.getHost()
const port = container.getMappedPort(5432) const port = container.getMappedPort(5432)
return { const datasource: Datasource = {
type: "datasource_plus", type: "datasource_plus",
source: SourceName.POSTGRES, source: SourceName.POSTGRES,
plus: true, plus: true,
@ -32,4 +34,28 @@ export async function postgres(): Promise<Datasource> {
ca: false, ca: false,
}, },
} }
const database = generator.guid().replaceAll("-", "")
await rawQuery(datasource, `CREATE DATABASE "${database}"`)
datasource.config!.database = database
return datasource
}
export async function rawQuery(ds: Datasource, sql: string) {
if (!ds.config) {
throw new Error("Datasource config is missing")
}
if (ds.source !== SourceName.POSTGRES) {
throw new Error("Datasource source is not Postgres")
}
const client = new pg.Client(ds.config)
await client.connect()
try {
const { rows } = await client.query(sql)
return rows
} finally {
await client.end()
}
} }