Merge pull request #1963 from faroutchris/feature/query-mongo-collection

Feature/query mongo collection
This commit is contained in:
Martin McKeaveney 2021-07-29 10:10:52 +01:00 committed by GitHub
commit 44cdcdf38e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 252 additions and 13 deletions

View File

@ -0,0 +1,48 @@
<script>
import { Select, Label, Input } from "@budibase/bbui"
/**
* This component takes the query object and populates the 'extra' property
* when a datasource has specified a configuration for these fields in SCHEMA.extra
*/
export let populateExtraQuery
export let config
export let query
$: extraFields = Object.keys(config).map(key => ({
...config[key],
key,
}))
$: extraQueryFields = query.fields.extra || {}
</script>
{#each extraFields as { key, displayName, type }}
<div class="config-field">
<Label>{displayName}</Label>
{#if type === "string"}
<Input
on:change={() => populateExtraQuery(extraQueryFields)}
bind:value={extraQueryFields[key]}
/>
{/if}
{#if type === "list"}
<Select
on:change={() => populateExtraQuery(extraQueryFields)}
bind:value={extraQueryFields[key]}
options={config[key].data[query.queryVerb]}
getOptionLabel={current => current}
/>
{/if}
</div>
{/each}
<style>
.config-field {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
</style>

View File

@ -15,6 +15,7 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { notifications, Divider } from "@budibase/bbui" import { notifications, Divider } from "@budibase/bbui"
import api from "builderStore/api" import api from "builderStore/api"
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte" import IntegrationQueryEditor from "components/integration/index.svelte"
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte" import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte" import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
@ -60,6 +61,14 @@
fields = fields fields = fields
} }
function resetDependentFields() {
if (query.fields.extra) query.fields.extra = {}
}
function populateExtraQuery(extraQueryFields) {
query.fields.extra = extraQueryFields
}
async function previewQuery() { async function previewQuery() {
try { try {
const response = await api.post(`/api/queries/preview`, { const response = await api.post(`/api/queries/preview`, {
@ -127,11 +136,19 @@
<Label>Function</Label> <Label>Function</Label>
<Select <Select
bind:value={query.queryVerb} bind:value={query.queryVerb}
on:change={resetDependentFields}
options={Object.keys(queryConfig)} options={Object.keys(queryConfig)}
getOptionLabel={verb => getOptionLabel={verb =>
queryConfig[verb]?.displayName || capitalise(verb)} queryConfig[verb]?.displayName || capitalise(verb)}
/> />
</div> </div>
{#if integrationInfo?.extra && query.queryVerb}
<ExtraQueryConfig
{query}
{populateExtraQuery}
config={integrationInfo.extra}
/>
{/if}
<ParameterBuilder bind:parameters={query.parameters} bindable={false} /> <ParameterBuilder bind:parameters={query.parameters} bindable={false} />
{/if} {/if}
</div> </div>

View File

@ -5,11 +5,26 @@ module MongoMock {
this.connect = jest.fn() this.connect = jest.fn()
this.close = jest.fn() this.close = jest.fn()
this.insertOne = jest.fn() this.insertOne = jest.fn()
this.insertMany = jest.fn(() => ({toArray: () => []}))
this.find = jest.fn(() => ({toArray: () => []})) this.find = jest.fn(() => ({toArray: () => []}))
this.findOne = jest.fn()
this.count = jest.fn()
this.deleteOne = jest.fn()
this.deleteMany = jest.fn(() => ({toArray: () => []}))
this.updateOne = jest.fn()
this.updateMany = jest.fn(() => ({toArray: () => []}))
this.collection = jest.fn(() => ({ this.collection = jest.fn(() => ({
insertOne: this.insertOne, insertOne: this.insertOne,
find: this.find, find: this.find,
insertMany: this.insertMany,
findOne: this.findOne,
count: this.count,
deleteOne: this.deleteOne,
deleteMany: this.deleteMany,
updateOne: this.updateOne,
updateMany: this.updateMany,
})) }))
this.db = () => ({ this.db = () => ({

View File

@ -30,6 +30,7 @@ function generateQueryValidation() {
default: Joi.string().allow(""), default: Joi.string().allow(""),
})), })),
queryVerb: Joi.string().allow().required(), queryVerb: Joi.string().allow().required(),
extra: Joi.object().optional(),
schema: Joi.object({}).required().unknown(true) schema: Joi.object({}).required().unknown(true)
})) }))
} }
@ -39,6 +40,7 @@ function generateQueryPreviewValidation() {
return joiValidator.body(Joi.object({ return joiValidator.body(Joi.object({
fields: Joi.object().required(), fields: Joi.object().required(),
queryVerb: Joi.string().allow().required(), queryVerb: Joi.string().allow().required(),
extra: Joi.object().optional(),
datasourceId: Joi.string().required(), datasourceId: Joi.string().required(),
parameters: Joi.object({}).required().unknown(true) parameters: Joi.object({}).required().unknown(true)
})) }))

View File

@ -49,6 +49,15 @@ export interface QueryDefinition {
urlDisplay?: boolean urlDisplay?: boolean
} }
export interface ExtraQueryConfig {
[key: string]: {
displayName: string,
type: string,
required: boolean
data?: object
}
}
export interface Integration { export interface Integration {
docs: string docs: string
plus?: boolean plus?: boolean
@ -58,6 +67,7 @@ export interface Integration {
query: { query: {
[key: string]: QueryDefinition [key: string]: QueryDefinition
} }
extra?: ExtraQueryConfig
} }
export interface SearchFilters { export interface SearchFilters {

View File

@ -10,7 +10,7 @@ module MongoDBModule {
interface MongoDBConfig { interface MongoDBConfig {
connectionString: string connectionString: string
db: string db: string
collection: string // collection: string
} }
const SCHEMA: Integration = { const SCHEMA: Integration = {
@ -28,10 +28,6 @@ module MongoDBModule {
type: DatasourceFieldTypes.STRING, type: DatasourceFieldTypes.STRING,
required: true, required: true,
}, },
collection: {
type: DatasourceFieldTypes.STRING,
required: true,
},
}, },
query: { query: {
create: { create: {
@ -40,7 +36,31 @@ module MongoDBModule {
read: { read: {
type: QueryTypes.JSON, type: QueryTypes.JSON,
}, },
update: {
type: QueryTypes.JSON,
},
delete: {
type: QueryTypes.JSON,
}
}, },
extra: {
collection: {
displayName: "Collection",
type: DatasourceFieldTypes.STRING,
required: true,
},
actionTypes: {
displayName: "Action Types",
type: DatasourceFieldTypes.LIST,
required: true,
data: {
read: ['find', 'findOne', 'findOneAndUpdate', "count", "distinct"],
create: ['insertOne', 'insertMany'],
update: ['updateOne', 'updateMany'],
delete: ['deleteOne', 'deleteMany']
}
}
}
} }
class MongoIntegration { class MongoIntegration {
@ -56,12 +76,25 @@ module MongoDBModule {
return this.client.connect() return this.client.connect()
} }
async create(query: { json: object }) { async create(query: { json: object, extra: { [key: string]: string } }) {
try { try {
await this.connect() await this.connect()
const db = this.client.db(this.config.db) const db = this.client.db(this.config.db)
const collection = db.collection(this.config.collection) const collection = db.collection(query.extra.collection)
return collection.insertOne(query.json)
// For mongodb we add an extra actionType to specify
// which method we want to call on the collection
switch(query.extra.actionTypes) {
case 'insertOne': {
return collection.insertOne(query.json)
}
case 'insertMany': {
return collection.insertOne(query.json).toArray()
}
default: {
throw new Error(`actionType ${query.extra.actionTypes} does not exist on DB for create`)
}
}
} catch (err) { } catch (err) {
console.error("Error writing to mongodb", err) console.error("Error writing to mongodb", err)
throw err throw err
@ -70,12 +103,32 @@ module MongoDBModule {
} }
} }
async read(query: { json: object }) { async read(query: { json: object, extra: { [key: string]: string } }) {
try { try {
await this.connect() await this.connect()
const db = this.client.db(this.config.db) const db = this.client.db(this.config.db)
const collection = db.collection(this.config.collection) const collection = db.collection(query.extra.collection)
return collection.find(query.json).toArray()
switch(query.extra.actionTypes) {
case 'find': {
return collection.find(query.json).toArray()
}
case 'findOne': {
return collection.findOne(query.json)
}
case 'findOneAndUpdate': {
return collection.findOneAndUpdate(query.json)
}
case 'count': {
return collection.countDocuments(query.json)
}
case 'distinct': {
return collection.distinct(query.json)
}
default: {
throw new Error(`actionType ${query.extra.actionTypes} does not exist on DB for read`)
}
}
} catch (err) { } catch (err) {
console.error("Error querying mongodb", err) console.error("Error querying mongodb", err)
throw err throw err
@ -83,6 +136,56 @@ module MongoDBModule {
await this.client.close() await this.client.close()
} }
} }
async update(query: { json: object, extra: { [key: string]: string } }) {
try {
await this.connect()
const db = this.client.db(this.config.db)
const collection = db.collection(query.extra.collection)
switch(query.extra.actionTypes) {
case 'updateOne': {
return collection.updateOne(query.json)
}
case 'updateMany': {
return collection.updateMany(query.json).toArray()
}
default: {
throw new Error(`actionType ${query.extra.actionTypes} does not exist on DB for update`)
}
}
} catch (err) {
console.error("Error writing to mongodb", err)
throw err
} finally {
await this.client.close()
}
}
async delete(query: { json: object, extra: { [key: string]: string } }) {
try {
await this.connect()
const db = this.client.db(this.config.db)
const collection = db.collection(query.extra.collection)
switch(query.extra.actionTypes) {
case 'deleteOne': {
return collection.deleteOne(query.json)
}
case 'deleteMany': {
return collection.deleteMany(query.json).toArray()
}
default: {
throw new Error(`actionType ${query.extra.actionTypes} does not exist on DB for delete`)
}
}
} catch (err) {
console.error("Error writing to mongodb", err)
throw err
} finally {
await this.client.close()
}
}
} }
module.exports = { module.exports = {

View File

@ -8,6 +8,13 @@ class TestConfiguration {
} }
} }
function disableConsole() {
jest.spyOn(console, 'error');
console.error.mockImplementation(() => {});
return console.error.mockRestore;
}
describe("MongoDB Integration", () => { describe("MongoDB Integration", () => {
let config let config
let indexName = "Users" let indexName = "Users"
@ -22,7 +29,8 @@ describe("MongoDB Integration", () => {
} }
const response = await config.integration.create({ const response = await config.integration.create({
index: indexName, index: indexName,
json: body json: body,
extra: { collection: 'testCollection', actionTypes: 'insertOne'}
}) })
expect(config.integration.client.insertOne).toHaveBeenCalledWith(body) expect(config.integration.client.insertOne).toHaveBeenCalledWith(body)
}) })
@ -31,10 +39,46 @@ describe("MongoDB Integration", () => {
const query = { const query = {
json: { json: {
address: "test" address: "test"
} },
extra: { collection: 'testCollection', actionTypes: 'find'}
} }
const response = await config.integration.read(query) const response = await config.integration.read(query)
expect(config.integration.client.find).toHaveBeenCalledWith(query.json) expect(config.integration.client.find).toHaveBeenCalledWith(query.json)
expect(response).toEqual(expect.any(Array)) expect(response).toEqual(expect.any(Array))
}) })
it("calls the delete method with the correct params", async () => {
const query = {
json: {
id: "test"
},
extra: { collection: 'testCollection', actionTypes: 'deleteOne'}
}
const response = await config.integration.delete(query)
expect(config.integration.client.deleteOne).toHaveBeenCalledWith(query.json)
})
it("calls the update method with the correct params", async () => {
const query = {
json: {
id: "test"
},
extra: { collection: 'testCollection', actionTypes: 'updateOne'}
}
const response = await config.integration.update(query)
expect(config.integration.client.updateOne).toHaveBeenCalledWith(query.json)
})
it("throws an error when an invalid query.extra.actionType is passed for each method", async () => {
const restore = disableConsole()
const query = {
extra: { collection: 'testCollection', actionTypes: 'deleteOne'}
}
// Weird, need to do an IIFE for jest to recognize that it throws
expect(() => config.integration.read(query)()).toThrow(expect.any(Object))
restore()
})
}) })