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"
|
} 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>
|
||||||
|
|
|
@ -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 = () => ({
|
||||||
|
|
|
@ -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)
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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()
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
Loading…
Reference in New Issue