Merge pull request #1963 from faroutchris/feature/query-mongo-collection
Feature/query mongo collection
This commit is contained in:
commit
44cdcdf38e
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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 = () => ({
|
||||
|
|
|
@ -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)
|
||||
}))
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
||||
})
|
Loading…
Reference in New Issue