1
0
Fork 0
mirror of synced 2024-08-17 02:51:55 +12:00

Merge pull request #12953 from Budibase/mongo-tests

Basic Postgres and Mongo query testcases.
This commit is contained in:
Sam Rose 2024-02-06 15:21:43 +00:00 committed by GitHub
commit df5279b96f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 729 additions and 88 deletions

View file

@ -93,7 +93,13 @@
notifications.success("Query executed successfully") notifications.success("Query executed successfully")
} catch (error) { } catch (error) {
notifications.error(`Query Error: ${error.message}`) if (typeof error.message === "string") {
notifications.error(`Query Error: ${error.message}`)
} else if (typeof error.message?.code === "string") {
notifications.error(`Query Error: ${error.message.code}`)
} else {
notifications.error(`Query Error: ${JSON.stringify(error.message)}`)
}
if (!suppressErrors) { if (!suppressErrors) {
throw error throw error

View file

@ -15,6 +15,9 @@ import {
SessionCookie, SessionCookie,
QuerySchema, QuerySchema,
FieldType, FieldType,
type ExecuteQueryRequest,
type ExecuteQueryResponse,
type Row,
} from "@budibase/types" } from "@budibase/types"
import { ValidQueryNameRegex } from "@budibase/shared-core" import { ValidQueryNameRegex } from "@budibase/shared-core"
@ -223,7 +226,7 @@ export async function preview(ctx: UserCtx) {
} }
async function execute( async function execute(
ctx: UserCtx, ctx: UserCtx<ExecuteQueryRequest, ExecuteQueryResponse | Row[]>,
opts: any = { rowsOnly: false, isAutomation: false } opts: any = { rowsOnly: false, isAutomation: false }
) { ) {
const db = context.getAppDB() const db = context.getAppDB()

View file

@ -0,0 +1,390 @@
import { Datasource, Query } from "@budibase/types"
import * as setup from "../utilities"
import { databaseTestProviders } from "../../../../integrations/tests/utils"
import { MongoClient, type Collection } from "mongodb"
jest.unmock("mongodb")
const collection = "test_collection"
describe("/queries", () => {
let config = setup.getConfig()
let datasource: Datasource
async function createQuery(query: Partial<Query>): Promise<Query> {
const defaultQuery: Query = {
datasourceId: datasource._id!,
name: "New Query",
parameters: [],
fields: {},
schema: {},
queryVerb: "read",
transformer: "return data",
readable: true,
}
const combinedQuery = { ...defaultQuery, ...query }
if (
combinedQuery.fields &&
combinedQuery.fields.extra &&
!combinedQuery.fields.extra.collection
) {
combinedQuery.fields.extra.collection = collection
}
return await config.api.query.create(combinedQuery)
}
async function withClient(
callback: (client: MongoClient) => Promise<void>
): Promise<void> {
const ds = await databaseTestProviders.mongodb.datasource()
const client = new MongoClient(ds.config!.connectionString)
await client.connect()
try {
await callback(client)
} finally {
await client.close()
}
}
async function withCollection(
callback: (collection: Collection) => Promise<void>
): Promise<void> {
await withClient(async client => {
const db = client.db(
(await databaseTestProviders.mongodb.datasource()).config!.db
)
await callback(db.collection(collection))
})
}
afterAll(async () => {
await databaseTestProviders.mongodb.stop()
setup.afterAll()
})
beforeAll(async () => {
await config.init()
datasource = await config.api.datasource.create(
await databaseTestProviders.mongodb.datasource()
)
})
beforeEach(async () => {
await withCollection(async collection => {
await collection.insertMany([
{ name: "one" },
{ name: "two" },
{ name: "three" },
{ name: "four" },
{ name: "five" },
])
})
})
afterEach(async () => {
await withCollection(async collection => {
await collection.drop()
})
})
it("should execute a count query", async () => {
const query = await createQuery({
fields: {
json: {},
extra: {
actionType: "count",
},
},
})
const result = await config.api.query.execute(query._id!)
expect(result.data).toEqual([{ value: 5 }])
})
it("should execute a count query with a transformer", async () => {
const query = await createQuery({
fields: {
json: {},
extra: {
actionType: "count",
},
},
transformer: "return data + 1",
})
const result = await config.api.query.execute(query._id!)
expect(result.data).toEqual([{ value: 6 }])
})
it("should execute a find query", async () => {
const query = await createQuery({
fields: {
json: {},
extra: {
actionType: "find",
},
},
})
const result = await config.api.query.execute(query._id!)
expect(result.data).toEqual([
{ _id: expect.anything(), name: "one" },
{ _id: expect.anything(), name: "two" },
{ _id: expect.anything(), name: "three" },
{ _id: expect.anything(), name: "four" },
{ _id: expect.anything(), name: "five" },
])
})
it("should execute a findOne query", async () => {
const query = await createQuery({
fields: {
json: {},
extra: {
actionType: "findOne",
},
},
})
const result = await config.api.query.execute(query._id!)
expect(result.data).toEqual([{ _id: expect.anything(), name: "one" }])
})
it("should execute a findOneAndUpdate query", async () => {
const query = await createQuery({
fields: {
json: {
filter: { name: { $eq: "one" } },
update: { $set: { name: "newName" } },
},
extra: {
actionType: "findOneAndUpdate",
},
},
})
const result = await config.api.query.execute(query._id!)
expect(result.data).toEqual([
{
lastErrorObject: { n: 1, updatedExisting: true },
ok: 1,
value: { _id: expect.anything(), name: "one" },
},
])
await withCollection(async collection => {
expect(await collection.countDocuments()).toBe(5)
const doc = await collection.findOne({ name: { $eq: "newName" } })
expect(doc).toEqual({
_id: expect.anything(),
name: "newName",
})
})
})
it("should execute a distinct query", async () => {
const query = await createQuery({
fields: {
json: "name",
extra: {
actionType: "distinct",
},
},
})
const result = await config.api.query.execute(query._id!)
const values = result.data.map(o => o.value).sort()
expect(values).toEqual(["five", "four", "one", "three", "two"])
})
it("should execute a create query with parameters", async () => {
const query = await createQuery({
fields: {
json: { foo: "{{ foo }}" },
extra: {
actionType: "insertOne",
},
},
queryVerb: "create",
parameters: [
{
name: "foo",
default: "default",
},
],
})
const result = await config.api.query.execute(query._id!, {
parameters: { foo: "bar" },
})
expect(result.data).toEqual([
{
acknowledged: true,
insertedId: expect.anything(),
},
])
await withCollection(async collection => {
const doc = await collection.findOne({ foo: { $eq: "bar" } })
expect(doc).toEqual({
_id: expect.anything(),
foo: "bar",
})
})
})
it("should execute a delete query with parameters", async () => {
const query = await createQuery({
fields: {
json: { name: { $eq: "{{ name }}" } },
extra: {
actionType: "deleteOne",
},
},
queryVerb: "delete",
parameters: [
{
name: "name",
default: "",
},
],
})
const result = await config.api.query.execute(query._id!, {
parameters: { name: "one" },
})
expect(result.data).toEqual([
{
acknowledged: true,
deletedCount: 1,
},
])
await withCollection(async collection => {
const doc = await collection.findOne({ name: { $eq: "one" } })
expect(doc).toBeNull()
})
})
it("should execute an update query with parameters", async () => {
const query = await createQuery({
fields: {
json: {
filter: { name: { $eq: "{{ name }}" } },
update: { $set: { name: "{{ newName }}" } },
},
extra: {
actionType: "updateOne",
},
},
queryVerb: "update",
parameters: [
{
name: "name",
default: "",
},
{
name: "newName",
default: "",
},
],
})
const result = await config.api.query.execute(query._id!, {
parameters: { name: "one", newName: "newOne" },
})
expect(result.data).toEqual([
{
acknowledged: true,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0,
upsertedId: null,
},
])
await withCollection(async collection => {
const doc = await collection.findOne({ name: { $eq: "newOne" } })
expect(doc).toEqual({
_id: expect.anything(),
name: "newOne",
})
const oldDoc = await collection.findOne({ name: { $eq: "one" } })
expect(oldDoc).toBeNull()
})
})
it("should be able to delete all records", async () => {
const query = await createQuery({
fields: {
json: {},
extra: {
actionType: "deleteMany",
},
},
queryVerb: "delete",
})
const result = await config.api.query.execute(query._id!)
expect(result.data).toEqual([
{
acknowledged: true,
deletedCount: 5,
},
])
await withCollection(async collection => {
const docs = await collection.find().toArray()
expect(docs).toHaveLength(0)
})
})
it("should be able to update all documents", async () => {
const query = await createQuery({
fields: {
json: {
filter: {},
update: { $set: { name: "newName" } },
},
extra: {
actionType: "updateMany",
},
},
queryVerb: "update",
})
const result = await config.api.query.execute(query._id!)
expect(result.data).toEqual([
{
acknowledged: true,
matchedCount: 5,
modifiedCount: 5,
upsertedCount: 0,
upsertedId: null,
},
])
await withCollection(async collection => {
const docs = await collection.find().toArray()
expect(docs).toHaveLength(5)
for (const doc of docs) {
expect(doc).toEqual({
_id: expect.anything(),
name: "newName",
})
}
})
})
})

View file

@ -0,0 +1,170 @@
import { Datasource, Query } from "@budibase/types"
import * as setup from "../utilities"
import { databaseTestProviders } from "../../../../integrations/tests/utils"
import { Client } from "pg"
jest.unmock("pg")
const createTableSQL = `
CREATE TABLE test_table (
id serial PRIMARY KEY,
name VARCHAR ( 50 ) NOT NULL
);
`
const insertSQL = `
INSERT INTO test_table (name) VALUES ('one');
INSERT INTO test_table (name) VALUES ('two');
INSERT INTO test_table (name) VALUES ('three');
INSERT INTO test_table (name) VALUES ('four');
INSERT INTO test_table (name) VALUES ('five');
`
const dropTableSQL = `
DROP TABLE test_table;
`
describe("/queries", () => {
let config = setup.getConfig()
let datasource: Datasource
async function createQuery(query: Partial<Query>): Promise<Query> {
const defaultQuery: Query = {
datasourceId: datasource._id!,
name: "New Query",
parameters: [],
fields: {},
schema: {},
queryVerb: "read",
transformer: "return data",
readable: true,
}
return await config.api.query.create({ ...defaultQuery, ...query })
}
async function withClient(
callback: (client: Client) => Promise<void>
): Promise<void> {
const ds = await databaseTestProviders.postgres.datasource()
const client = new Client(ds.config!)
await client.connect()
try {
await callback(client)
} finally {
await client.end()
}
}
afterAll(async () => {
await databaseTestProviders.postgres.stop()
setup.afterAll()
})
beforeAll(async () => {
await config.init()
datasource = await config.api.datasource.create(
await databaseTestProviders.postgres.datasource()
)
})
beforeEach(async () => {
await withClient(async client => {
await client.query(createTableSQL)
await client.query(insertSQL)
})
})
afterEach(async () => {
await withClient(async client => {
await client.query(dropTableSQL)
})
})
it("should execute a query", async () => {
const query = await createQuery({
fields: {
sql: "SELECT * FROM test_table ORDER BY id",
},
})
const result = await config.api.query.execute(query._id!)
expect(result.data).toEqual([
{
id: 1,
name: "one",
},
{
id: 2,
name: "two",
},
{
id: 3,
name: "three",
},
{
id: 4,
name: "four",
},
{
id: 5,
name: "five",
},
])
})
it("should be able to transform a query", async () => {
const query = await createQuery({
fields: {
sql: "SELECT * FROM test_table WHERE id = 1",
},
transformer: `
data[0].id = data[0].id + 1;
return data;
`,
})
const result = await config.api.query.execute(query._id!)
expect(result.data).toEqual([
{
id: 2,
name: "one",
},
])
})
it("should be able to insert with bindings", async () => {
const query = await createQuery({
fields: {
sql: "INSERT INTO test_table (name) VALUES ({{ foo }})",
},
parameters: [
{
name: "foo",
default: "bar",
},
],
queryVerb: "create",
})
const result = await config.api.query.execute(query._id!, {
parameters: {
foo: "baz",
},
})
expect(result.data).toEqual([
{
created: true,
},
])
await withClient(async client => {
const { rows } = await client.query(
"SELECT * FROM test_table WHERE name = 'baz'"
)
expect(rows).toHaveLength(1)
})
})
})

View file

@ -16,9 +16,9 @@ jest.mock("@budibase/backend-core", () => {
}, },
} }
}) })
import * as setup from "./utilities" import * as setup from "../utilities"
import { checkBuilderEndpoint } from "./utilities/TestFunctions" import { checkBuilderEndpoint } from "../utilities/TestFunctions"
import { checkCacheForDynamicVariable } from "../../../threads/utils" import { checkCacheForDynamicVariable } from "../../../../threads/utils"
const { basicQuery, basicDatasource } = setup.structures const { basicQuery, basicDatasource } = setup.structures
import { events, db as dbCore } from "@budibase/backend-core" import { events, db as dbCore } from "@budibase/backend-core"

View file

@ -12,7 +12,6 @@ import {
FieldTypeSubtypes, FieldTypeSubtypes,
FormulaType, FormulaType,
INTERNAL_TABLE_SOURCE_ID, INTERNAL_TABLE_SOURCE_ID,
MonthlyQuotaName,
PermissionLevel, PermissionLevel,
QuotaUsageType, QuotaUsageType,
RelationshipType, RelationshipType,
@ -53,7 +52,7 @@ describe.each([
afterAll(async () => { afterAll(async () => {
if (dsProvider) { if (dsProvider) {
await dsProvider.stopContainer() await dsProvider.stop()
} }
setup.afterAll() setup.afterAll()
}) })
@ -63,7 +62,7 @@ describe.each([
if (dsProvider) { if (dsProvider) {
await config.createDatasource({ await config.createDatasource({
datasource: await dsProvider.getDsConfig(), datasource: await dsProvider.datasource(),
}) })
} }
}) })
@ -117,16 +116,6 @@ describe.each([
return total return total
} }
const getQueryUsage = async () => {
const { total } = await config.doInContext(null, () =>
quotas.getCurrentUsageValues(
QuotaUsageType.MONTHLY,
MonthlyQuotaName.QUERIES
)
)
return total
}
const assertRowUsage = async (expected: number) => { const assertRowUsage = async (expected: number) => {
const usage = await getRowUsage() const usage = await getRowUsage()
expect(usage).toBe(expected) expect(usage).toBe(expected)
@ -162,7 +151,6 @@ describe.each([
describe("save, load, update", () => { describe("save, load, update", () => {
it("returns a success message when the row is created", async () => { it("returns a success message when the row is created", async () => {
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await request const res = await request
.post(`/api/${tableId}/rows`) .post(`/api/${tableId}/rows`)
@ -180,7 +168,6 @@ describe.each([
it("Increment row autoId per create row request", async () => { it("Increment row autoId per create row request", async () => {
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const tableConfig = generateTableConfig() const tableConfig = generateTableConfig()
const newTable = await createTable( const newTable = await createTable(
@ -231,7 +218,6 @@ describe.each([
it("updates a row successfully", async () => { it("updates a row successfully", async () => {
const existing = await config.createRow() const existing = await config.createRow()
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await config.api.row.save(tableId, { const res = await config.api.row.save(tableId, {
_id: existing._id, _id: existing._id,
@ -246,7 +232,6 @@ describe.each([
it("should load a row", async () => { it("should load a row", async () => {
const existing = await config.createRow() const existing = await config.createRow()
const queryUsage = await getQueryUsage()
const res = await config.api.row.get(tableId, existing._id!) const res = await config.api.row.get(tableId, existing._id!)
@ -268,7 +253,6 @@ describe.each([
} }
const firstRow = await config.createRow({ tableId }) const firstRow = await config.createRow({ tableId })
await config.createRow(newRow) await config.createRow(newRow)
const queryUsage = await getQueryUsage()
const res = await config.api.row.fetch(tableId) const res = await config.api.row.fetch(tableId)
@ -279,7 +263,6 @@ describe.each([
it("load should return 404 when row does not exist", async () => { it("load should return 404 when row does not exist", async () => {
await config.createRow() await config.createRow()
const queryUsage = await getQueryUsage()
await config.api.row.get(tableId, "1234567", { await config.api.row.get(tableId, "1234567", {
expectStatus: 404, expectStatus: 404,
@ -530,7 +513,6 @@ describe.each([
const existing = await config.createRow() const existing = await config.createRow()
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const row = await config.api.row.patch(table._id!, { const row = await config.api.row.patch(table._id!, {
_id: existing._id!, _id: existing._id!,
@ -552,7 +534,6 @@ describe.each([
it("should throw an error when given improper types", async () => { it("should throw an error when given improper types", async () => {
const existing = await config.createRow() const existing = await config.createRow()
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
await config.api.row.patch( await config.api.row.patch(
table._id!, table._id!,
@ -650,7 +631,6 @@ describe.each([
it("should be able to delete a row", async () => { it("should be able to delete a row", async () => {
const createdRow = await config.createRow() const createdRow = await config.createRow()
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await config.api.row.delete(table._id!, [createdRow]) const res = await config.api.row.delete(table._id!, [createdRow])
expect(res.body[0]._id).toEqual(createdRow._id) expect(res.body[0]._id).toEqual(createdRow._id)
@ -666,7 +646,6 @@ describe.each([
it("should return no errors on valid row", async () => { it("should return no errors on valid row", async () => {
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await config.api.row.validate(table._id!, { name: "ivan" }) const res = await config.api.row.validate(table._id!, { name: "ivan" })
@ -677,7 +656,6 @@ describe.each([
it("should errors on invalid row", async () => { it("should errors on invalid row", async () => {
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await config.api.row.validate(table._id!, { name: 1 }) const res = await config.api.row.validate(table._id!, { name: 1 })
@ -703,7 +681,6 @@ describe.each([
const row1 = await config.createRow() const row1 = await config.createRow()
const row2 = await config.createRow() const row2 = await config.createRow()
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await config.api.row.delete(table._id!, [row1, row2]) const res = await config.api.row.delete(table._id!, [row1, row2])
@ -719,7 +696,6 @@ describe.each([
config.createRow(), config.createRow(),
]) ])
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await config.api.row.delete(table._id!, [ const res = await config.api.row.delete(table._id!, [
row1, row1,
@ -735,7 +711,6 @@ describe.each([
it("should accept a valid row object and delete the row", async () => { it("should accept a valid row object and delete the row", async () => {
const row1 = await config.createRow() const row1 = await config.createRow()
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await config.api.row.delete(table._id!, row1) const res = await config.api.row.delete(table._id!, row1)
@ -746,7 +721,6 @@ describe.each([
it("Should ignore malformed/invalid delete requests", async () => { it("Should ignore malformed/invalid delete requests", async () => {
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await config.api.row.delete( const res = await config.api.row.delete(
table._id!, table._id!,
@ -782,7 +756,6 @@ describe.each([
it("should be able to fetch tables contents via 'view'", async () => { it("should be able to fetch tables contents via 'view'", async () => {
const row = await config.createRow() const row = await config.createRow()
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await config.api.legacyView.get(table._id!) const res = await config.api.legacyView.get(table._id!)
expect(res.body.length).toEqual(1) expect(res.body.length).toEqual(1)
@ -792,7 +765,6 @@ describe.each([
it("should throw an error if view doesn't exist", async () => { it("should throw an error if view doesn't exist", async () => {
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
await config.api.legacyView.get("derp", { expectStatus: 404 }) await config.api.legacyView.get("derp", { expectStatus: 404 })
@ -808,7 +780,6 @@ describe.each([
}) })
const row = await config.createRow() const row = await config.createRow()
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await config.api.legacyView.get(view.name) const res = await config.api.legacyView.get(view.name)
expect(res.body.length).toEqual(1) expect(res.body.length).toEqual(1)
@ -864,7 +835,6 @@ describe.each([
} }
) )
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
// test basic enrichment // test basic enrichment
const resBasic = await config.api.row.get( const resBasic = await config.api.row.get(
@ -1100,7 +1070,6 @@ describe.each([
const createdRow = await config.createRow() const createdRow = await config.createRow()
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
await config.api.row.delete(view.id, [createdRow]) await config.api.row.delete(view.id, [createdRow])
@ -1127,7 +1096,6 @@ describe.each([
config.createRow(), config.createRow(),
]) ])
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
await config.api.row.delete(view.id, [rows[0], rows[2]]) await config.api.row.delete(view.id, [rows[0], rows[2]])

View file

@ -41,12 +41,12 @@ describe("postgres integrations", () => {
makeRequest = generateMakeRequest(apiKey, true) makeRequest = generateMakeRequest(apiKey, true)
postgresDatasource = await config.api.datasource.create( postgresDatasource = await config.api.datasource.create(
await databaseTestProviders.postgres.getDsConfig() await databaseTestProviders.postgres.datasource()
) )
}) })
afterAll(async () => { afterAll(async () => {
await databaseTestProviders.postgres.stopContainer() await databaseTestProviders.postgres.stop()
}) })
beforeEach(async () => { beforeEach(async () => {
@ -1041,14 +1041,14 @@ describe("postgres integrations", () => {
describe("POST /api/datasources/verify", () => { describe("POST /api/datasources/verify", () => {
it("should be able to verify the connection", async () => { it("should be able to verify the connection", async () => {
const response = await config.api.datasource.verify({ const response = await config.api.datasource.verify({
datasource: await databaseTestProviders.postgres.getDsConfig(), datasource: await databaseTestProviders.postgres.datasource(),
}) })
expect(response.status).toBe(200) expect(response.status).toBe(200)
expect(response.body.connected).toBe(true) expect(response.body.connected).toBe(true)
}) })
it("should state an invalid datasource cannot connect", async () => { it("should state an invalid datasource cannot connect", async () => {
const dbConfig = await databaseTestProviders.postgres.getDsConfig() const dbConfig = await databaseTestProviders.postgres.datasource()
const response = await config.api.datasource.verify({ const response = await config.api.datasource.verify({
datasource: { datasource: {
...dbConfig, ...dbConfig,
@ -1082,7 +1082,7 @@ describe("postgres integrations", () => {
beforeEach(async () => { beforeEach(async () => {
client = new Client( client = new Client(
(await databaseTestProviders.postgres.getDsConfig()).config! (await databaseTestProviders.postgres.datasource()).config!
) )
await client.connect() await client.connect()
}) })
@ -1125,7 +1125,7 @@ describe("postgres integrations", () => {
schema2 = "test-2" schema2 = "test-2"
beforeAll(async () => { beforeAll(async () => {
const dsConfig = await databaseTestProviders.postgres.getDsConfig() const dsConfig = await databaseTestProviders.postgres.datasource()
const dbConfig = dsConfig.config! const dbConfig = dsConfig.config!
client = new Client(dbConfig) client = new Client(dbConfig)

View file

@ -202,8 +202,13 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
await this.openConnection() await this.openConnection()
response.connected = true response.connected = true
} catch (e: any) { } catch (e: any) {
console.log(e) if (typeof e.message === "string" && e.message !== "") {
response.error = e.message as string response.error = e.message as string
} else if (typeof e.code === "string" && e.code !== "") {
response.error = e.code
} else {
response.error = "Unknown error"
}
} finally { } finally {
await this.closeConnection() await this.closeConnection()
} }

View file

@ -1,14 +1,16 @@
jest.unmock("pg") jest.unmock("pg")
import { Datasource } from "@budibase/types" import { Datasource } from "@budibase/types"
import * as pg from "./postgres" import * as postgres from "./postgres"
import * as mongodb from "./mongodb"
import { StartedTestContainer } from "testcontainers"
jest.setTimeout(30000) jest.setTimeout(30000)
export interface DatabasePlusTestProvider { export interface DatabaseProvider {
getDsConfig(): Promise<Datasource> start(): Promise<StartedTestContainer>
stop(): Promise<void>
datasource(): Promise<Datasource>
} }
export const databaseTestProviders = { export const databaseTestProviders = { postgres, mongodb }
postgres: pg,
}

View file

@ -0,0 +1,41 @@
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("mongo:7.0-jammy")
.withExposedPorts(27017)
.withEnvironment({
MONGO_INITDB_ROOT_USERNAME: "mongo",
MONGO_INITDB_ROOT_PASSWORD: "password",
})
.withWaitStrategy(
Wait.forSuccessfulCommand(`mongosh --eval "db.version()"`)
)
.start()
}
export async function datasource(): Promise<Datasource> {
if (!container) {
container = await start()
}
const host = container.getHost()
const port = container.getMappedPort(27017)
return {
type: "datasource",
source: SourceName.MONGODB,
plus: false,
config: {
connectionString: `mongodb://mongo:password@${host}:${port}`,
db: "mongo",
},
}
}
export async function stop() {
if (container) {
await container.stop()
container = undefined
}
}

View file

@ -3,45 +3,44 @@ import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
let container: StartedTestContainer | undefined let container: StartedTestContainer | undefined
export async function getDsConfig(): Promise<Datasource> { export async function start(): Promise<StartedTestContainer> {
try { return await new GenericContainer("postgres:16.1-bullseye")
if (!container) { .withExposedPorts(5432)
container = await new GenericContainer("postgres:16.1-bullseye") .withEnvironment({ POSTGRES_PASSWORD: "password" })
.withExposedPorts(5432) .withWaitStrategy(
.withEnvironment({ POSTGRES_PASSWORD: "password" }) Wait.forSuccessfulCommand(
.withWaitStrategy( "pg_isready -h localhost -p 5432"
Wait.forLogMessage( ).withStartupTimeout(10000)
"database system is ready to accept connections", )
2 .start()
) }
)
.start()
}
const host = container.getHost()
const port = container.getMappedPort(5432)
return { export async function datasource(): Promise<Datasource> {
type: "datasource_plus", if (!container) {
source: SourceName.POSTGRES, container = await start()
plus: true, }
config: { const host = container.getHost()
host, const port = container.getMappedPort(5432)
port,
database: "postgres", return {
user: "postgres", type: "datasource_plus",
password: "password", source: SourceName.POSTGRES,
schema: "public", plus: true,
ssl: false, config: {
rejectUnauthorized: false, host,
ca: false, port,
}, database: "postgres",
} user: "postgres",
} catch (err) { password: "password",
throw new Error("**UNABLE TO CREATE TO POSTGRES CONTAINER**") schema: "public",
ssl: false,
rejectUnauthorized: false,
ca: false,
},
} }
} }
export async function stopContainer() { export async function stop() {
if (container) { if (container) {
await container.stop() await container.stop()
container = undefined container = undefined

View file

@ -10,6 +10,7 @@ import { ApplicationAPI } from "./application"
import { BackupAPI } from "./backup" import { BackupAPI } from "./backup"
import { AttachmentAPI } from "./attachment" import { AttachmentAPI } from "./attachment"
import { UserAPI } from "./user" import { UserAPI } from "./user"
import { QueryAPI } from "./query"
export default class API { export default class API {
table: TableAPI table: TableAPI
@ -23,6 +24,7 @@ export default class API {
backup: BackupAPI backup: BackupAPI
attachment: AttachmentAPI attachment: AttachmentAPI
user: UserAPI user: UserAPI
query: QueryAPI
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {
this.table = new TableAPI(config) this.table = new TableAPI(config)
@ -36,5 +38,6 @@ export default class API {
this.backup = new BackupAPI(config) this.backup = new BackupAPI(config)
this.attachment = new AttachmentAPI(config) this.attachment = new AttachmentAPI(config)
this.user = new UserAPI(config) this.user = new UserAPI(config)
this.query = new QueryAPI(config)
} }
} }

View file

@ -0,0 +1,44 @@
import TestConfiguration from "../TestConfiguration"
import {
Query,
type ExecuteQueryRequest,
type ExecuteQueryResponse,
} from "@budibase/types"
import { TestAPI } from "./base"
export class QueryAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
create = async (body: Query): Promise<Query> => {
const res = await this.request
.post(`/api/queries`)
.set(this.config.defaultHeaders())
.send(body)
.expect("Content-Type", /json/)
if (res.status !== 200) {
throw new Error(JSON.stringify(res.body))
}
return res.body as Query
}
execute = async (
queryId: string,
body?: ExecuteQueryRequest
): Promise<ExecuteQueryResponse> => {
const res = await this.request
.post(`/api/v2/queries/${queryId}`)
.set(this.config.defaultHeaders())
.send(body)
.expect("Content-Type", /json/)
if (res.status !== 200) {
throw new Error(JSON.stringify(res.body))
}
return res.body
}
}

View file

@ -1,4 +1,5 @@
import { Document } from "../document" import { Document } from "../document"
import type { Row } from "./row"
export interface QuerySchema { export interface QuerySchema {
name?: string name?: string
@ -54,3 +55,12 @@ export interface PreviewQueryRequest extends Omit<Query, "parameters"> {
urlName?: boolean urlName?: boolean
} }
} }
export interface ExecuteQueryRequest {
parameters?: { [key: string]: string }
pagination?: any
}
export interface ExecuteQueryResponse {
data: Row[]
}