Making really good progress removing the pg mocks. More to do, though.
This commit is contained in:
parent
08cf877565
commit
1c13565459
|
@ -1,25 +0,0 @@
|
|||
const query = jest.fn(() => ({
|
||||
rows: [
|
||||
{
|
||||
a: "string",
|
||||
b: 1,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
class Client {
|
||||
query = query
|
||||
end = jest.fn(cb => {
|
||||
if (cb) cb()
|
||||
})
|
||||
connect = jest.fn()
|
||||
release = jest.fn()
|
||||
}
|
||||
|
||||
const on = jest.fn()
|
||||
|
||||
module.exports = {
|
||||
Client,
|
||||
queryMock: query,
|
||||
on,
|
||||
}
|
|
@ -60,6 +60,8 @@ const config: Config.InitialOptions = {
|
|||
"!src/db/views/staticViews.*",
|
||||
"!src/**/*.spec.{js,ts}",
|
||||
"!src/tests/**/*.{js,ts}",
|
||||
// The use of coverage in the JS runner bundles breaks tests
|
||||
"!src/jsRunner/bundles/**/*.{js,ts}",
|
||||
],
|
||||
coverageReporters: ["lcov", "json", "clover"],
|
||||
}
|
||||
|
|
|
@ -10,5 +10,5 @@ else
|
|||
# --maxWorkers performs better in development
|
||||
export NODE_OPTIONS="--no-node-snapshot $NODE_OPTIONS"
|
||||
echo "jest --coverage --maxWorkers=2 --forceExit $@"
|
||||
jest --coverage --maxWorkers=2 --forceExit $@
|
||||
jest --maxWorkers=2 --forceExit $@
|
||||
fi
|
|
@ -1,9 +1,11 @@
|
|||
import { Datasource, Query, SourceName } from "@budibase/types"
|
||||
import { Datasource, Query, QueryPreview, SourceName } from "@budibase/types"
|
||||
import * as setup from "../utilities"
|
||||
import { databaseTestProviders } from "../../../../integrations/tests/utils"
|
||||
import pg from "pg"
|
||||
import mysql from "mysql2/promise"
|
||||
import mssql from "mssql"
|
||||
import { Expectations } from "src/tests/utilities/api/base"
|
||||
import { events } from "@budibase/backend-core"
|
||||
|
||||
jest.unmock("pg")
|
||||
|
||||
|
@ -40,7 +42,10 @@ describe.each([
|
|||
const config = setup.getConfig()
|
||||
let datasource: Datasource
|
||||
|
||||
async function createQuery(query: Partial<Query>): Promise<Query> {
|
||||
async function createQuery(
|
||||
query: Partial<Query>,
|
||||
expectations?: Expectations
|
||||
): Promise<Query> {
|
||||
const defaultQuery: Query = {
|
||||
datasourceId: datasource._id!,
|
||||
name: "New Query",
|
||||
|
@ -51,17 +56,16 @@ describe.each([
|
|||
transformer: "return data",
|
||||
readable: true,
|
||||
}
|
||||
return await config.api.query.create({ ...defaultQuery, ...query })
|
||||
return await config.api.query.save(
|
||||
{ ...defaultQuery, ...query },
|
||||
expectations
|
||||
)
|
||||
}
|
||||
|
||||
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.datasource()
|
||||
switch (ds.source) {
|
||||
switch (datasource.source) {
|
||||
case SourceName.POSTGRES: {
|
||||
const client = new pg.Client(ds.config!)
|
||||
const client = new pg.Client(datasource.config!)
|
||||
await client.connect()
|
||||
try {
|
||||
const { rows } = await client.query(sql)
|
||||
|
@ -71,7 +75,7 @@ describe.each([
|
|||
}
|
||||
}
|
||||
case SourceName.MYSQL: {
|
||||
const con = await mysql.createConnection(ds.config!)
|
||||
const con = await mysql.createConnection(datasource.config!)
|
||||
try {
|
||||
const [rows] = await con.query(sql)
|
||||
return rows
|
||||
|
@ -80,7 +84,9 @@ describe.each([
|
|||
}
|
||||
}
|
||||
case SourceName.SQL_SERVER: {
|
||||
const pool = new mssql.ConnectionPool(ds.config! as mssql.config)
|
||||
const pool = new mssql.ConnectionPool(
|
||||
datasource.config! as mssql.config
|
||||
)
|
||||
const client = await pool.connect()
|
||||
try {
|
||||
const { recordset } = await client.query(sql)
|
||||
|
@ -94,17 +100,26 @@ describe.each([
|
|||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
datasource = await config.api.datasource.create(
|
||||
await dsProvider.datasource()
|
||||
)
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
const datasourceRequest = await dsProvider.datasource()
|
||||
datasource = await config.api.datasource.create(datasourceRequest)
|
||||
|
||||
// The Datasource API does not return the password, but we need
|
||||
// it later to connect to the underlying database, so we fill it
|
||||
// back in here.
|
||||
datasource.config!.password = datasourceRequest.config!.password
|
||||
|
||||
await rawQuery(createTableSQL[datasource.source])
|
||||
await rawQuery(insertSQL)
|
||||
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const ds = await config.api.datasource.get(datasource._id!)
|
||||
config.api.datasource.delete(ds)
|
||||
await rawQuery(dropTableSQL)
|
||||
})
|
||||
|
||||
|
@ -113,6 +128,309 @@ describe.each([
|
|||
setup.afterAll()
|
||||
})
|
||||
|
||||
describe("query admin", () => {
|
||||
describe("create", () => {
|
||||
it("should be able to create a query", async () => {
|
||||
const query = await createQuery({
|
||||
name: "New Query",
|
||||
fields: {
|
||||
sql: "SELECT * FROM test_table",
|
||||
},
|
||||
})
|
||||
|
||||
expect(query).toMatchObject({
|
||||
datasourceId: datasource._id!,
|
||||
name: "New Query",
|
||||
parameters: [],
|
||||
fields: {
|
||||
sql: "SELECT * FROM test_table",
|
||||
},
|
||||
schema: {},
|
||||
queryVerb: "read",
|
||||
transformer: "return data",
|
||||
readable: true,
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
})
|
||||
|
||||
expect(events.query.created).toHaveBeenCalledTimes(1)
|
||||
expect(events.query.updated).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
it("should be able to update a query", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: "SELECT * FROM test_table",
|
||||
},
|
||||
})
|
||||
|
||||
jest.clearAllMocks()
|
||||
|
||||
const updatedQuery = await config.api.query.save({
|
||||
...query,
|
||||
name: "Updated Query",
|
||||
fields: {
|
||||
sql: "SELECT * FROM test_table WHERE id = 1",
|
||||
},
|
||||
})
|
||||
|
||||
expect(updatedQuery).toMatchObject({
|
||||
datasourceId: datasource._id!,
|
||||
name: "Updated Query",
|
||||
parameters: [],
|
||||
fields: {
|
||||
sql: "SELECT * FROM test_table WHERE id = 1",
|
||||
},
|
||||
schema: {},
|
||||
queryVerb: "read",
|
||||
transformer: "return data",
|
||||
readable: true,
|
||||
})
|
||||
|
||||
expect(events.query.created).not.toHaveBeenCalled()
|
||||
expect(events.query.updated).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete", () => {
|
||||
it("should be able to delete a query", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: "SELECT * FROM test_table",
|
||||
},
|
||||
})
|
||||
|
||||
await config.api.query.delete(query)
|
||||
await config.api.query.get(query._id!, { status: 404 })
|
||||
|
||||
const queries = await config.api.query.fetch()
|
||||
expect(queries).not.toContainEqual(query)
|
||||
|
||||
expect(events.query.deleted).toHaveBeenCalledTimes(1)
|
||||
expect(events.query.deleted).toHaveBeenCalledWith(datasource, query)
|
||||
})
|
||||
})
|
||||
|
||||
describe("read", () => {
|
||||
it("should be able to list queries", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: "SELECT * FROM test_table",
|
||||
},
|
||||
})
|
||||
|
||||
const queries = await config.api.query.fetch()
|
||||
expect(queries).toContainEqual(query)
|
||||
})
|
||||
|
||||
it("should strip sensitive fields for prod apps", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: "SELECT * FROM test_table",
|
||||
},
|
||||
})
|
||||
|
||||
await config.publish()
|
||||
const prodQuery = await config.api.query.getProd(query._id!)
|
||||
|
||||
expect(prodQuery._id).toEqual(query._id)
|
||||
expect(prodQuery.fields).toBeUndefined()
|
||||
expect(prodQuery.parameters).toBeUndefined()
|
||||
expect(prodQuery.schema).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("preview", () => {
|
||||
it("should be able to preview a query", async () => {
|
||||
const request: QueryPreview = {
|
||||
datasourceId: datasource._id!,
|
||||
queryVerb: "read",
|
||||
fields: {
|
||||
sql: `SELECT * FROM test_table WHERE id = 1`,
|
||||
},
|
||||
parameters: [],
|
||||
transformer: "return data",
|
||||
name: datasource.name!,
|
||||
schema: {},
|
||||
readable: true,
|
||||
}
|
||||
const response = await config.api.query.previewQuery(request)
|
||||
expect(response.schema).toEqual({
|
||||
birthday: {
|
||||
name: "birthday",
|
||||
type: "string",
|
||||
},
|
||||
id: {
|
||||
name: "id",
|
||||
type: "number",
|
||||
},
|
||||
name: {
|
||||
name: "name",
|
||||
type: "string",
|
||||
},
|
||||
})
|
||||
expect(response.rows).toEqual([
|
||||
{
|
||||
birthday: null,
|
||||
id: 1,
|
||||
name: "one",
|
||||
},
|
||||
])
|
||||
expect(events.query.previewed).toHaveBeenCalledTimes(1)
|
||||
|
||||
const dsWithoutConfig = { ...datasource }
|
||||
delete dsWithoutConfig.config
|
||||
expect(events.query.previewed).toHaveBeenCalledWith(
|
||||
dsWithoutConfig,
|
||||
request
|
||||
)
|
||||
})
|
||||
|
||||
it("should work with static variables", async () => {
|
||||
await config.api.datasource.update({
|
||||
...datasource,
|
||||
config: {
|
||||
...datasource.config,
|
||||
staticVariables: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const request: QueryPreview = {
|
||||
datasourceId: datasource._id!,
|
||||
queryVerb: "read",
|
||||
fields: {
|
||||
sql: `SELECT '{{ foo }}' as foo`,
|
||||
},
|
||||
parameters: [],
|
||||
transformer: "return data",
|
||||
name: datasource.name!,
|
||||
schema: {},
|
||||
readable: true,
|
||||
}
|
||||
|
||||
const response = await config.api.query.previewQuery(request)
|
||||
|
||||
expect(response.schema).toEqual({
|
||||
foo: {
|
||||
name: "foo",
|
||||
type: "string",
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.rows).toEqual([
|
||||
{
|
||||
foo: "bar",
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should work with dynamic variables", async () => {
|
||||
const basedOnQuery = await createQuery({
|
||||
fields: {
|
||||
sql: "SELECT name FROM test_table WHERE id = 1",
|
||||
},
|
||||
})
|
||||
|
||||
await config.api.datasource.update({
|
||||
...datasource,
|
||||
config: {
|
||||
...datasource.config,
|
||||
dynamicVariables: [
|
||||
{
|
||||
queryId: basedOnQuery._id!,
|
||||
name: "foo",
|
||||
value: "{{ data[0].name }}",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const preview = await config.api.query.previewQuery({
|
||||
datasourceId: datasource._id!,
|
||||
queryVerb: "read",
|
||||
fields: {
|
||||
sql: `SELECT '{{ foo }}' as foo`,
|
||||
},
|
||||
parameters: [],
|
||||
transformer: "return data",
|
||||
name: datasource.name!,
|
||||
schema: {},
|
||||
readable: true,
|
||||
})
|
||||
|
||||
expect(preview.schema).toEqual({
|
||||
foo: {
|
||||
name: "foo",
|
||||
type: "string",
|
||||
},
|
||||
})
|
||||
|
||||
expect(preview.rows).toEqual([
|
||||
{
|
||||
foo: "one",
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should handle the dynamic base query being deleted", async () => {
|
||||
const basedOnQuery = await createQuery({
|
||||
fields: {
|
||||
sql: "SELECT name FROM test_table WHERE id = 1",
|
||||
},
|
||||
})
|
||||
|
||||
await config.api.datasource.update({
|
||||
...datasource,
|
||||
config: {
|
||||
...datasource.config,
|
||||
dynamicVariables: [
|
||||
{
|
||||
queryId: basedOnQuery._id!,
|
||||
name: "foo",
|
||||
value: "{{ data[0].name }}",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
await config.api.query.delete(basedOnQuery)
|
||||
|
||||
const preview = await config.api.query.previewQuery({
|
||||
datasourceId: datasource._id!,
|
||||
queryVerb: "read",
|
||||
fields: {
|
||||
sql: `SELECT '{{ foo }}' as foo`,
|
||||
},
|
||||
parameters: [],
|
||||
transformer: "return data",
|
||||
name: datasource.name!,
|
||||
schema: {},
|
||||
readable: true,
|
||||
})
|
||||
|
||||
expect(preview.schema).toEqual({
|
||||
foo: {
|
||||
name: "foo",
|
||||
type: "string",
|
||||
},
|
||||
})
|
||||
|
||||
// TODO: is this the correct behaviour? To return an empty string when the
|
||||
// underlying query has been deleted?
|
||||
expect(preview.rows).toEqual([
|
||||
{
|
||||
foo: "",
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("query verbs", () => {
|
||||
describe("create", () => {
|
||||
it("should be able to insert with bindings", async () => {
|
||||
const query = await createQuery({
|
||||
|
@ -140,10 +458,43 @@ describe.each([
|
|||
},
|
||||
])
|
||||
|
||||
const rows = await rawQuery("SELECT * FROM test_table WHERE name = 'baz'")
|
||||
const rows = await rawQuery(
|
||||
"SELECT * FROM test_table WHERE name = 'baz'"
|
||||
)
|
||||
expect(rows).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("should not allow handlebars as parameters", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: "INSERT INTO test_table (name) VALUES ({{ foo }})",
|
||||
},
|
||||
parameters: [
|
||||
{
|
||||
name: "foo",
|
||||
default: "bar",
|
||||
},
|
||||
],
|
||||
queryVerb: "create",
|
||||
})
|
||||
|
||||
await config.api.query.execute(
|
||||
query._id!,
|
||||
{
|
||||
parameters: {
|
||||
foo: "{{ 'test' }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
body: {
|
||||
message:
|
||||
"Parameter 'foo' input contains a handlebars binding - this is not allowed.",
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it.each(["2021-02-05T12:01:00.000Z", "2021-02-05"])(
|
||||
"should coerce %s into a date",
|
||||
async datetimeStr => {
|
||||
|
@ -399,3 +750,4 @@ describe.each([
|
|||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Datasource, Query } from "@budibase/types"
|
|||
import * as setup from "../utilities"
|
||||
import { databaseTestProviders } from "../../../../integrations/tests/utils"
|
||||
import { MongoClient, type Collection, BSON } from "mongodb"
|
||||
import { generator } from "@budibase/backend-core/tests"
|
||||
|
||||
jest.unmock("mongodb")
|
||||
|
||||
|
@ -33,30 +34,30 @@ describe("/queries", () => {
|
|||
) {
|
||||
combinedQuery.fields.extra.collection = collection
|
||||
}
|
||||
return await config.api.query.create(combinedQuery)
|
||||
return await config.api.query.save(combinedQuery)
|
||||
}
|
||||
|
||||
async function withClient(
|
||||
callback: (client: MongoClient) => Promise<void>
|
||||
): Promise<void> {
|
||||
async function withClient<T>(
|
||||
callback: (client: MongoClient) => Promise<T>
|
||||
): Promise<T> {
|
||||
const ds = await databaseTestProviders.mongodb.datasource()
|
||||
const client = new MongoClient(ds.config!.connectionString)
|
||||
await client.connect()
|
||||
try {
|
||||
await callback(client)
|
||||
return await callback(client)
|
||||
} finally {
|
||||
await client.close()
|
||||
}
|
||||
}
|
||||
|
||||
async function withCollection(
|
||||
callback: (collection: Collection) => Promise<void>
|
||||
): Promise<void> {
|
||||
await withClient(async client => {
|
||||
async function withCollection<T>(
|
||||
callback: (collection: Collection) => Promise<T>
|
||||
): Promise<T> {
|
||||
return await withClient(async client => {
|
||||
const db = client.db(
|
||||
(await databaseTestProviders.mongodb.datasource()).config!.db
|
||||
)
|
||||
await callback(db.collection(collection))
|
||||
return await callback(db.collection(collection))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -85,12 +86,155 @@ describe("/queries", () => {
|
|||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await withCollection(async collection => {
|
||||
await collection.drop()
|
||||
await withCollection(collection => collection.drop())
|
||||
})
|
||||
|
||||
describe.only("preview", () => {
|
||||
it("should generate a nested schema with an empty array", async () => {
|
||||
const name = generator.guid()
|
||||
await withCollection(
|
||||
async collection => await collection.insertOne({ name, nested: [] })
|
||||
)
|
||||
|
||||
const preview = await config.api.query.previewQuery({
|
||||
name: "New Query",
|
||||
datasourceId: datasource._id!,
|
||||
fields: {
|
||||
json: {
|
||||
name: { $eq: name },
|
||||
},
|
||||
extra: {
|
||||
collection,
|
||||
actionType: "findOne",
|
||||
},
|
||||
},
|
||||
schema: {},
|
||||
queryVerb: "read",
|
||||
parameters: [],
|
||||
transformer: "return data",
|
||||
readable: true,
|
||||
})
|
||||
|
||||
expect(preview).toEqual({
|
||||
nestedSchemaFields: {},
|
||||
rows: [{ _id: expect.any(String), name, nested: [] }],
|
||||
schema: {
|
||||
_id: {
|
||||
type: "string",
|
||||
name: "_id",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
name: "name",
|
||||
},
|
||||
nested: {
|
||||
type: "array",
|
||||
name: "nested",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should execute a count query", async () => {
|
||||
it("should generate a nested schema based on all of the nested items", async () => {
|
||||
const name = generator.guid()
|
||||
const item = {
|
||||
name,
|
||||
contacts: [
|
||||
{
|
||||
address: "123 Lane",
|
||||
},
|
||||
{
|
||||
address: "456 Drive",
|
||||
},
|
||||
{
|
||||
postcode: "BT1 12N",
|
||||
lat: 54.59,
|
||||
long: -5.92,
|
||||
},
|
||||
{
|
||||
city: "Belfast",
|
||||
},
|
||||
{
|
||||
address: "789 Avenue",
|
||||
phoneNumber: "0800-999-5555",
|
||||
},
|
||||
{
|
||||
name: "Name",
|
||||
isActive: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await withCollection(collection => collection.insertOne(item))
|
||||
|
||||
const preview = await config.api.query.previewQuery({
|
||||
name: "New Query",
|
||||
datasourceId: datasource._id!,
|
||||
fields: {
|
||||
json: {
|
||||
name: { $eq: name },
|
||||
},
|
||||
extra: {
|
||||
collection,
|
||||
actionType: "findOne",
|
||||
},
|
||||
},
|
||||
schema: {},
|
||||
queryVerb: "read",
|
||||
parameters: [],
|
||||
transformer: "return data",
|
||||
readable: true,
|
||||
})
|
||||
|
||||
expect(preview).toEqual({
|
||||
nestedSchemaFields: {
|
||||
contacts: {
|
||||
address: {
|
||||
type: "string",
|
||||
name: "address",
|
||||
},
|
||||
postcode: {
|
||||
type: "string",
|
||||
name: "postcode",
|
||||
},
|
||||
lat: {
|
||||
type: "number",
|
||||
name: "lat",
|
||||
},
|
||||
long: {
|
||||
type: "number",
|
||||
name: "long",
|
||||
},
|
||||
city: {
|
||||
type: "string",
|
||||
name: "city",
|
||||
},
|
||||
phoneNumber: {
|
||||
type: "string",
|
||||
name: "phoneNumber",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
name: "name",
|
||||
},
|
||||
isActive: {
|
||||
type: "boolean",
|
||||
name: "isActive",
|
||||
},
|
||||
},
|
||||
},
|
||||
rows: [{ ...item, _id: expect.any(String) }],
|
||||
schema: {
|
||||
_id: { type: "string", name: "_id" },
|
||||
name: { type: "string", name: "name" },
|
||||
contacts: { type: "json", name: "contacts", subtype: "array" },
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("execute", () => {
|
||||
it("a count query", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
json: {},
|
||||
|
@ -105,7 +249,7 @@ describe("/queries", () => {
|
|||
expect(result.data).toEqual([{ value: 5 }])
|
||||
})
|
||||
|
||||
it("should execute a count query with a transformer", async () => {
|
||||
it("a count query with a transformer", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
json: {},
|
||||
|
@ -121,7 +265,7 @@ describe("/queries", () => {
|
|||
expect(result.data).toEqual([{ value: 6 }])
|
||||
})
|
||||
|
||||
it("should execute a find query", async () => {
|
||||
it("a find query", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
json: {},
|
||||
|
@ -142,7 +286,7 @@ describe("/queries", () => {
|
|||
])
|
||||
})
|
||||
|
||||
it("should execute a findOne query", async () => {
|
||||
it("a findOne query", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
json: {},
|
||||
|
@ -157,7 +301,7 @@ describe("/queries", () => {
|
|||
expect(result.data).toEqual([{ _id: expectValidId, name: "one" }])
|
||||
})
|
||||
|
||||
it("should execute a findOneAndUpdate query", async () => {
|
||||
it("a findOneAndUpdate query", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
json: {
|
||||
|
@ -191,7 +335,7 @@ describe("/queries", () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("should execute a distinct query", async () => {
|
||||
it("a distinct query", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
json: "name",
|
||||
|
@ -206,7 +350,7 @@ describe("/queries", () => {
|
|||
expect(values).toEqual(["five", "four", "one", "three", "two"])
|
||||
})
|
||||
|
||||
it("should execute a create query with parameters", async () => {
|
||||
it("a create query with parameters", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
json: { foo: "{{ foo }}" },
|
||||
|
@ -243,7 +387,7 @@ describe("/queries", () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("should execute a delete query with parameters", async () => {
|
||||
it("a delete query with parameters", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
json: { name: { $eq: "{{ name }}" } },
|
||||
|
@ -277,7 +421,7 @@ describe("/queries", () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("should execute an update query with parameters", async () => {
|
||||
it("an update query with parameters", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
json: {
|
||||
|
@ -391,3 +535,4 @@ describe("/queries", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -55,87 +55,6 @@ describe("/queries", () => {
|
|||
await setupTest()
|
||||
})
|
||||
|
||||
const createQuery = async (query: Query) => {
|
||||
return request
|
||||
.post(`/api/queries`)
|
||||
.send(query)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
}
|
||||
|
||||
describe("create", () => {
|
||||
it("should create a new query", async () => {
|
||||
const { _id } = await config.createDatasource()
|
||||
const query = basicQuery(_id)
|
||||
jest.clearAllMocks()
|
||||
const res = await createQuery(query)
|
||||
|
||||
expect((res as any).res.statusMessage).toEqual(
|
||||
`Query ${query.name} saved successfully.`
|
||||
)
|
||||
expect(res.body).toEqual({
|
||||
_rev: res.body._rev,
|
||||
_id: res.body._id,
|
||||
...query,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
expect(events.query.created).toHaveBeenCalledTimes(1)
|
||||
expect(events.query.updated).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
it("should update query", async () => {
|
||||
const { _id } = await config.createDatasource()
|
||||
const query = basicQuery(_id)
|
||||
const res = await createQuery(query)
|
||||
jest.clearAllMocks()
|
||||
query._id = res.body._id
|
||||
query._rev = res.body._rev
|
||||
await createQuery(query)
|
||||
|
||||
expect((res as any).res.statusMessage).toEqual(
|
||||
`Query ${query.name} saved successfully.`
|
||||
)
|
||||
expect(res.body).toEqual({
|
||||
_rev: res.body._rev,
|
||||
_id: res.body._id,
|
||||
...query,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
expect(events.query.created).not.toHaveBeenCalled()
|
||||
expect(events.query.updated).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetch", () => {
|
||||
beforeEach(async () => {
|
||||
await setupTest()
|
||||
})
|
||||
|
||||
it("returns all the queries from the server", async () => {
|
||||
const res = await request
|
||||
.get(`/api/queries`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
const queries = res.body
|
||||
expect(queries).toEqual([
|
||||
{
|
||||
_rev: query._rev,
|
||||
_id: query._id,
|
||||
createdAt: new Date().toISOString(),
|
||||
...basicQuery(datasource._id),
|
||||
updatedAt: new Date().toISOString(),
|
||||
readable: true,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await checkBuilderEndpoint({
|
||||
config,
|
||||
|
@ -143,77 +62,8 @@ describe("/queries", () => {
|
|||
url: `/api/datasources`,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("find", () => {
|
||||
it("should find a query in builder", async () => {
|
||||
const query = await config.createQuery()
|
||||
const res = await request
|
||||
.get(`/api/queries/${query._id}`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body._id).toEqual(query._id)
|
||||
})
|
||||
|
||||
it("should find a query in cloud", async () => {
|
||||
await config.withEnv({ SELF_HOSTED: "true" }, async () => {
|
||||
const query = await config.createQuery()
|
||||
const res = await request
|
||||
.get(`/api/queries/${query._id}`)
|
||||
.set(await config.defaultHeaders())
|
||||
.expect(200)
|
||||
.expect("Content-Type", /json/)
|
||||
expect(res.body.fields).toBeDefined()
|
||||
expect(res.body.parameters).toBeDefined()
|
||||
expect(res.body.schema).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
it("should remove sensitive info for prod apps", async () => {
|
||||
// Mock isProdAppID to pretend we are using a prod app
|
||||
mockIsProdAppID.mockClear()
|
||||
mockIsProdAppID.mockImplementation(() => true)
|
||||
|
||||
const query = await config.createQuery()
|
||||
const res = await request
|
||||
.get(`/api/queries/${query._id}`)
|
||||
.set(await config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body._id).toEqual(query._id)
|
||||
expect(res.body.fields).toBeUndefined()
|
||||
expect(res.body.parameters).toBeUndefined()
|
||||
expect(res.body.schema).toBeDefined()
|
||||
|
||||
// Reset isProdAppID mock
|
||||
expect(dbCore.isProdAppID).toHaveBeenCalledTimes(1)
|
||||
mockIsProdAppID.mockImplementation(() => false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("destroy", () => {
|
||||
beforeEach(async () => {
|
||||
await setupTest()
|
||||
})
|
||||
|
||||
it("deletes a query and returns a success message", async () => {
|
||||
await request
|
||||
.delete(`/api/queries/${query._id}/${query._rev}`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect(200)
|
||||
|
||||
const res = await request
|
||||
.get(`/api/queries`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toEqual([])
|
||||
expect(events.query.deleted).toHaveBeenCalledTimes(1)
|
||||
expect(events.query.deleted).toHaveBeenCalledWith(datasource, query)
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
const query = await config.createQuery()
|
||||
await checkBuilderEndpoint({
|
||||
|
@ -225,32 +75,6 @@ describe("/queries", () => {
|
|||
})
|
||||
|
||||
describe("preview", () => {
|
||||
it("should be able to preview the query", async () => {
|
||||
const queryPreview: QueryPreview = {
|
||||
datasourceId: datasource._id,
|
||||
queryVerb: "read",
|
||||
fields: {},
|
||||
parameters: [],
|
||||
transformer: "return data",
|
||||
name: datasource.name!,
|
||||
schema: {},
|
||||
readable: true,
|
||||
}
|
||||
const responseBody = await config.api.query.previewQuery(queryPreview)
|
||||
// these responses come from the mock
|
||||
expect(responseBody.schema).toEqual({
|
||||
a: { type: "string", name: "a" },
|
||||
b: { type: "number", name: "b" },
|
||||
})
|
||||
expect(responseBody.rows.length).toEqual(1)
|
||||
expect(events.query.previewed).toHaveBeenCalledTimes(1)
|
||||
delete datasource.config
|
||||
expect(events.query.previewed).toHaveBeenCalledWith(
|
||||
datasource,
|
||||
queryPreview
|
||||
)
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await checkBuilderEndpoint({
|
||||
config,
|
||||
|
@ -258,129 +82,6 @@ describe("/queries", () => {
|
|||
url: `/api/queries/preview`,
|
||||
})
|
||||
})
|
||||
|
||||
it("should not error when trying to generate a nested schema for an empty array", async () => {
|
||||
const queryPreview: QueryPreview = {
|
||||
datasourceId: datasource._id,
|
||||
parameters: [],
|
||||
fields: {},
|
||||
queryVerb: "read",
|
||||
name: datasource.name!,
|
||||
transformer: "return data",
|
||||
schema: {},
|
||||
readable: true,
|
||||
}
|
||||
const rows = [
|
||||
{
|
||||
contacts: [],
|
||||
},
|
||||
]
|
||||
pg.queryMock.mockImplementation(() => ({
|
||||
rows,
|
||||
}))
|
||||
|
||||
const responseBody = await config.api.query.previewQuery(queryPreview)
|
||||
expect(responseBody).toEqual({
|
||||
nestedSchemaFields: {},
|
||||
rows,
|
||||
schema: {
|
||||
contacts: { type: "array", name: "contacts" },
|
||||
},
|
||||
})
|
||||
expect(responseBody.rows.length).toEqual(1)
|
||||
delete datasource.config
|
||||
})
|
||||
|
||||
it("should generate a nested schema based on all the nested items", async () => {
|
||||
const queryPreview: QueryPreview = {
|
||||
datasourceId: datasource._id,
|
||||
parameters: [],
|
||||
fields: {},
|
||||
queryVerb: "read",
|
||||
name: datasource.name!,
|
||||
transformer: "return data",
|
||||
schema: {},
|
||||
readable: true,
|
||||
}
|
||||
const rows = [
|
||||
{
|
||||
contacts: [
|
||||
{
|
||||
address: "123 Lane",
|
||||
},
|
||||
{
|
||||
address: "456 Drive",
|
||||
},
|
||||
{
|
||||
postcode: "BT1 12N",
|
||||
lat: 54.59,
|
||||
long: -5.92,
|
||||
},
|
||||
{
|
||||
city: "Belfast",
|
||||
},
|
||||
{
|
||||
address: "789 Avenue",
|
||||
phoneNumber: "0800-999-5555",
|
||||
},
|
||||
{
|
||||
name: "Name",
|
||||
isActive: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
pg.queryMock.mockImplementation(() => ({
|
||||
rows,
|
||||
}))
|
||||
|
||||
const responseBody = await config.api.query.previewQuery(queryPreview)
|
||||
expect(responseBody).toEqual({
|
||||
nestedSchemaFields: {
|
||||
contacts: {
|
||||
address: {
|
||||
type: "string",
|
||||
name: "address",
|
||||
},
|
||||
postcode: {
|
||||
type: "string",
|
||||
name: "postcode",
|
||||
},
|
||||
lat: {
|
||||
type: "number",
|
||||
name: "lat",
|
||||
},
|
||||
long: {
|
||||
type: "number",
|
||||
name: "long",
|
||||
},
|
||||
city: {
|
||||
type: "string",
|
||||
name: "city",
|
||||
},
|
||||
phoneNumber: {
|
||||
type: "string",
|
||||
name: "phoneNumber",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
name: "name",
|
||||
},
|
||||
isActive: {
|
||||
type: "boolean",
|
||||
name: "isActive",
|
||||
},
|
||||
},
|
||||
},
|
||||
rows,
|
||||
schema: {
|
||||
contacts: { type: "json", name: "contacts", subtype: "array" },
|
||||
},
|
||||
})
|
||||
expect(responseBody.rows.length).toEqual(1)
|
||||
delete datasource.config
|
||||
})
|
||||
})
|
||||
|
||||
describe("execute", () => {
|
||||
|
@ -412,21 +113,6 @@ describe("/queries", () => {
|
|||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("shouldn't allow handlebars to be passed as parameters", async () => {
|
||||
const res = await request
|
||||
.post(`/api/queries/${query._id}`)
|
||||
.send({
|
||||
parameters: {
|
||||
a: "{{ 'test' }}",
|
||||
},
|
||||
})
|
||||
.set(config.defaultHeaders())
|
||||
.expect(400)
|
||||
expect(res.body.message).toEqual(
|
||||
"Parameter 'a' input contains a handlebars binding - this is not allowed."
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("variables", () => {
|
||||
|
@ -444,40 +130,6 @@ describe("/queries", () => {
|
|||
return await config.api.query.previewQuery(queryPreview)
|
||||
}
|
||||
|
||||
it("should work with static variables", async () => {
|
||||
const datasource = await config.restDatasource({
|
||||
staticVariables: {
|
||||
variable: "google",
|
||||
variable2: "1",
|
||||
},
|
||||
})
|
||||
const responseBody = await preview(datasource, {
|
||||
path: "www.{{ variable }}.com",
|
||||
queryString: "test={{ variable2 }}",
|
||||
})
|
||||
// these responses come from the mock
|
||||
expect(responseBody.schema).toEqual({
|
||||
opts: { type: "json", name: "opts" },
|
||||
url: { type: "string", name: "url" },
|
||||
value: { type: "string", name: "value" },
|
||||
})
|
||||
expect(responseBody.rows[0].url).toEqual("http://www.google.com?test=1")
|
||||
})
|
||||
|
||||
it("should work with dynamic variables", async () => {
|
||||
const { datasource } = await config.dynamicVariableDatasource()
|
||||
const responseBody = await preview(datasource, {
|
||||
path: "www.google.com",
|
||||
queryString: "test={{ variable3 }}",
|
||||
})
|
||||
expect(responseBody.schema).toEqual({
|
||||
opts: { type: "json", name: "opts" },
|
||||
url: { type: "string", name: "url" },
|
||||
value: { type: "string", name: "value" },
|
||||
})
|
||||
expect(responseBody.rows[0].url).toContain("doctype%20html")
|
||||
})
|
||||
|
||||
it("check that it automatically retries on fail with cached dynamics", async () => {
|
||||
const { datasource, query: base } =
|
||||
await config.dynamicVariableDatasource()
|
||||
|
@ -503,29 +155,6 @@ describe("/queries", () => {
|
|||
})
|
||||
expect(responseBody.rows[0].fails).toEqual(1)
|
||||
})
|
||||
|
||||
it("deletes variables when linked query is deleted", async () => {
|
||||
const { datasource, query: base } =
|
||||
await config.dynamicVariableDatasource()
|
||||
// preview once to cache
|
||||
await preview(datasource, {
|
||||
path: "www.google.com",
|
||||
queryString: "test={{ variable3 }}",
|
||||
})
|
||||
// check its in cache
|
||||
let contents = await checkCacheForDynamicVariable(base._id!, "variable3")
|
||||
expect(contents.rows.length).toEqual(1)
|
||||
|
||||
// delete the query
|
||||
await request
|
||||
.delete(`/api/queries/${base._id}/${base._rev}`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect(200)
|
||||
|
||||
// check variables no longer in cache
|
||||
contents = await checkCacheForDynamicVariable(base._id!, "variable3")
|
||||
expect(contents).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Current User Request Mapping", () => {
|
||||
|
|
|
@ -45,4 +45,17 @@ export class DatasourceAPI extends TestAPI {
|
|||
expectations,
|
||||
})
|
||||
}
|
||||
|
||||
delete = async (datasource: Datasource, expectations?: Expectations) => {
|
||||
return await this._delete(
|
||||
`/api/datasources/${datasource._id!}/${datasource._rev!}`,
|
||||
{ expectations }
|
||||
)
|
||||
}
|
||||
|
||||
get = async (id: string, expectations?: Expectations) => {
|
||||
return await this._get<Datasource>(`/api/datasources/${id}`, {
|
||||
expectations,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,28 +5,58 @@ import {
|
|||
PreviewQueryRequest,
|
||||
PreviewQueryResponse,
|
||||
} from "@budibase/types"
|
||||
import { TestAPI } from "./base"
|
||||
import { Expectations, TestAPI } from "./base"
|
||||
import { constants } from "@budibase/backend-core"
|
||||
|
||||
export class QueryAPI extends TestAPI {
|
||||
create = async (body: Query): Promise<Query> => {
|
||||
return await this._post<Query>(`/api/queries`, { body })
|
||||
save = async (body: Query, expectations?: Expectations): Promise<Query> => {
|
||||
return await this._post<Query>(`/api/queries`, { body, expectations })
|
||||
}
|
||||
|
||||
execute = async (
|
||||
queryId: string,
|
||||
body?: ExecuteQueryRequest
|
||||
body?: ExecuteQueryRequest,
|
||||
expectations?: Expectations
|
||||
): Promise<ExecuteQueryResponse> => {
|
||||
return await this._post<ExecuteQueryResponse>(
|
||||
`/api/v2/queries/${queryId}`,
|
||||
{
|
||||
body,
|
||||
expectations,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
previewQuery = async (queryPreview: PreviewQueryRequest) => {
|
||||
previewQuery = async (
|
||||
queryPreview: PreviewQueryRequest,
|
||||
expectations?: Expectations
|
||||
) => {
|
||||
return await this._post<PreviewQueryResponse>(`/api/queries/preview`, {
|
||||
body: queryPreview,
|
||||
expectations,
|
||||
})
|
||||
}
|
||||
|
||||
delete = async (query: Query, expectations?: Expectations) => {
|
||||
return await this._delete(`/api/queries/${query._id!}/${query._rev!}`, {
|
||||
expectations,
|
||||
})
|
||||
}
|
||||
|
||||
get = async (queryId: string, expectations?: Expectations) => {
|
||||
return await this._get<Query>(`/api/queries/${queryId}`, { expectations })
|
||||
}
|
||||
|
||||
getProd = async (queryId: string, expectations?: Expectations) => {
|
||||
return await this._get<Query>(`/api/queries/${queryId}`, {
|
||||
expectations,
|
||||
headers: {
|
||||
[constants.Header.APP_ID]: this.config.getProdAppId(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fetch = async (expectations?: Expectations) => {
|
||||
return await this._get<Query[]>(`/api/queries`, { expectations })
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue