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

View File

@ -5,11 +5,26 @@ module MongoMock {
this.connect = jest.fn()
this.close = jest.fn()
this.insertOne = jest.fn()
this.insertMany = 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(() => ({
insertOne: this.insertOne,
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 = () => ({

View File

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

View File

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

View File

@ -10,7 +10,7 @@ module MongoDBModule {
interface MongoDBConfig {
connectionString: string
db: string
collection: string
// collection: string
}
const SCHEMA: Integration = {
@ -28,10 +28,6 @@ module MongoDBModule {
type: DatasourceFieldTypes.STRING,
required: true,
},
collection: {
type: DatasourceFieldTypes.STRING,
required: true,
},
},
query: {
create: {
@ -40,7 +36,31 @@ module MongoDBModule {
read: {
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 {
@ -56,12 +76,25 @@ module MongoDBModule {
return this.client.connect()
}
async create(query: { json: object }) {
async create(query: { json: object, extra: { [key: string]: string } }) {
try {
await this.connect()
const db = this.client.db(this.config.db)
const collection = db.collection(this.config.collection)
return collection.insertOne(query.json)
const collection = db.collection(query.extra.collection)
// 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) {
console.error("Error writing to mongodb", err)
throw err
@ -70,12 +103,32 @@ module MongoDBModule {
}
}
async read(query: { json: object }) {
async read(query: { json: object, extra: { [key: string]: string } }) {
try {
await this.connect()
const db = this.client.db(this.config.db)
const collection = db.collection(this.config.collection)
return collection.find(query.json).toArray()
const collection = db.collection(query.extra.collection)
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) {
console.error("Error querying mongodb", err)
throw err
@ -83,6 +136,56 @@ module MongoDBModule {
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 = {

View File

@ -8,6 +8,13 @@ class TestConfiguration {
}
}
function disableConsole() {
jest.spyOn(console, 'error');
console.error.mockImplementation(() => {});
return console.error.mockRestore;
}
describe("MongoDB Integration", () => {
let config
let indexName = "Users"
@ -22,7 +29,8 @@ describe("MongoDB Integration", () => {
}
const response = await config.integration.create({
index: indexName,
json: body
json: body,
extra: { collection: 'testCollection', actionTypes: 'insertOne'}
})
expect(config.integration.client.insertOne).toHaveBeenCalledWith(body)
})
@ -31,10 +39,46 @@ describe("MongoDB Integration", () => {
const query = {
json: {
address: "test"
}
},
extra: { collection: 'testCollection', actionTypes: 'find'}
}
const response = await config.integration.read(query)
expect(config.integration.client.find).toHaveBeenCalledWith(query.json)
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()
})
})