switching between queries

This commit is contained in:
Martin McKeaveney 2021-01-06 12:28:51 +00:00
parent 755fa0ac4a
commit d7a0d29b03
20 changed files with 339 additions and 238 deletions

View File

@ -8,6 +8,7 @@ const INITIAL_BACKEND_UI_STATE = {
users: [],
roles: [],
datasources: [],
queries: [],
selectedDatabase: {},
selectedTable: {},
draftTable: {},
@ -24,10 +25,13 @@ export const getBackendUiStore = () => {
const tables = await tablesResponse.json()
const datasourcesResponse = await api.get(`/api/datasources`)
const datasources = await datasourcesResponse.json()
const queriesResponse = await api.get(`/api/queries`)
const queries = await queriesResponse.json()
store.update(state => {
state.selectedDatabase = db
state.tables = tables
state.datasources = datasources
state.queries = queries
return state
})
},
@ -98,35 +102,56 @@ export const getBackendUiStore = () => {
return state
})
},
saveQuery: async (datasourceId, query) => {
const response = await api.post(
`/api/datasources/${datasourceId}/queries`,
query
)
},
queries: {
fetch: async () => {
const response = await api.get(`/api/queries`)
const json = await response.json()
store.update(state => {
const currentIdx = state.datasources.findIndex(
ds => ds._id === json._id
state.queries = json
return state
})
return json
},
save: async (datasourceId, query) => {
query.datasourceId = datasourceId
const response = await api.post(`/api/queries`, query)
const json = await response.json()
store.update(state => {
const currentIdx = state.queries.findIndex(
query => query._id === json._id
)
if (currentIdx >= 0) {
state.datasources.splice(currentIdx, 1, json)
state.queries.splice(currentIdx, 1, json)
} else {
state.datasources.push(json)
state.queries.push(json)
}
state.datasources = state.datasources
state.queries = state.queries
state.selectedQueryId = json._id
return state
})
},
},
queries: {
select: queryId =>
store.update(state => {
state.selectedDatasourceId = null
state.selectedQueryId = queryId
return state
}),
delete: async queryId => {
await api.delete(`/api/queries/${queryId}`)
store.update(state => {
state.datasources = state.queries.filter(
existing => existing._id !== queryId
)
if (state.selectedQueryId === queryId) {
state.selectedQueryId = null
}
return state
})
},
},
tables: {
fetch: async () => {

View File

@ -6,7 +6,6 @@
import Table from "./Table.svelte"
import CreateQueryButton from "components/backend/DataTable/buttons/CreateQueryButton.svelte"
export let datasource
export let query = {}
let data = []
@ -23,7 +22,6 @@
data = response.rows || []
error = false
} catch (err) {
console.log(err)
error = `${query}: Query error. (${err.message}). This could be a problem with your datasource configuration.`
notifier.danger(error)
} finally {
@ -32,14 +30,14 @@
}
// Fetch rows for specified query
$: fetchData()
$: query && fetchData()
</script>
{#if error}
<div class="errors">{error}</div>
{/if}
<Table title={query.name} schema={query.schema} {data} {loading}>
<CreateQueryButton {query} {datasource} />
<CreateQueryButton {query} edit />
</Table>
<style>

View File

@ -32,7 +32,7 @@ export async function fetchDataForView(view) {
}
export async function fetchDataForQuery(datasourceId, queryId) {
const FETCH_QUERY_URL = `/api/datasources/${datasourceId}/queries/${queryId}`
const FETCH_QUERY_URL = `/api/queries/${queryId}`
const response = await api.get(FETCH_QUERY_URL)
const json = await response.json()

View File

@ -15,15 +15,15 @@
import EditIntegrationConfig from "../modals/EditIntegrationConfig.svelte"
import CreateEditQuery from "components/backend/DataTable/modals/CreateEditQuery.svelte"
export let datasource
export let query = {}
export let edit
let modal
let fields = []
async function saveQuery() {
try {
await backendUiStore.actions.datasources.saveQuery(datasource._id, query)
await backendUiStore.actions.queries.save(query.datasourceId, query)
notifier.success(`Query created successfully.`)
} catch (err) {
console.error(err)
@ -35,7 +35,7 @@
<div>
<Button text small on:click={modal.show}>
<Icon name="filter" />
{$backendUiStore.selectedQueryId ? 'Edit' : 'Create'} Query
{edit ? 'Edit' : 'Create'} Query
</Button>
</div>
<Modal bind:this={modal}>
@ -43,7 +43,7 @@
confirmText="Save"
cancelText="Cancel"
onConfirm={saveQuery}
title={query ? 'Edit Query' : 'Create New Query'}>
<CreateEditQuery {datasource} bind:query />
title={edit ? 'Edit Query' : 'Create New Query'}>
<CreateEditQuery bind:query />
</ModalContent>
</Modal>

View File

@ -27,17 +27,20 @@
},
]
export let datasource
export let query
export let fields = []
console.log(query)
let config = {}
let queryType
let previewTab = "PREVIEW"
let preview
$: datasource = $backendUiStore.datasources.find(
ds => ds._id === query.datasourceId
)
$: query.datasourceId =
query.datasourceId || $backendUiStore.selectedDatasourceId
$: query.schema = fields.reduce(
(acc, next) => ({
...acc,
@ -72,9 +75,8 @@
async function previewQuery() {
try {
const response = await api.post(`/api/datasources/queries/preview`, {
type: datasource.source,
config: datasource.config,
const response = await api.post(`/api/queries/preview`, {
datasourceId: datasource._id,
query: query.queryString,
})
const json = await response.json()
@ -101,18 +103,13 @@
<section>
<div class="config">
<h6>Datasource Type</h6>
<span>{datasource.source}</span>
<Spacer medium />
<Label extraSmall grey>Query Name</Label>
<Input type="text" thin bind:value={query.name} />
<Spacer medium />
<Label extraSmall grey>Query Type</Label>
<Select secondary bind:value={queryType}>
<Select secondary bind:value={query.queryType}>
<option value={''}>Select an option</option>
{#each Object.keys(config) as queryType}
<option value={queryType}>{queryType}</option>
@ -121,7 +118,9 @@
<Spacer medium />
<IntegrationQueryEditor {queryType} bind:query={query.queryString} />
<IntegrationQueryEditor
type={query.queryType}
bind:query={query.queryString} />
<Spacer small />

View File

@ -5,6 +5,7 @@
import { TableNames } from "constants"
import CreateDatasourceModal from "./modals/CreateDatasourceModal.svelte"
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte"
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
import { Modal, Switcher } from "@budibase/bbui"
import NavItem from "components/common/NavItem.svelte"
@ -18,6 +19,7 @@
}
function onClickQuery(datasourceId, queryId) {
console.log(backendUiStore.selectedQueryId, queryId)
if ($backendUiStore.selectedQueryId === queryId) {
return
}
@ -28,6 +30,7 @@
onMount(() => {
backendUiStore.actions.datasources.fetch()
backendUiStore.actions.queries.fetch()
})
</script>
@ -42,15 +45,14 @@
on:click={() => selectDatasource(datasource)}>
<EditDatasourcePopover {datasource} />
</NavItem>
{#each Object.keys(datasource.queries) as queryId}
{#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query}
<NavItem
indentLevel={1}
icon="ri-eye-line"
text={datasource.queries[queryId].name}
selected={$backendUiStore.selectedQueryId === queryId}
on:click={() => onClickQuery(datasource._id, queryId)}>
<!-- <EditViewPopover
view={{ name: viewName, ...table.views[viewName] }} /> -->
text={query.name}
selected={$backendUiStore.selectedQueryId === query._id}
on:click={() => onClickQuery(datasource._id, query._id)}>
<EditQueryPopover {query} />
</NavItem>
{/each}
{/each}

View File

@ -0,0 +1,85 @@
<script>
import { backendUiStore, store, allScreens } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Input } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let query
let anchor
let dropdown
let confirmDeleteDialog
let error = ""
let willBeDeleted
function hideEditor() {
dropdown?.hide()
}
function showModal() {
hideEditor()
confirmDeleteDialog.show()
}
async function deleteQuery() {
await backendUiStore.actions.queries.delete(query._id)
notifier.success("Query deleted")
hideEditor()
}
</script>
<div on:click|stopPropagation>
<div bind:this={anchor} class="icon" on:click={dropdown.show}>
<i class="ri-more-line" />
</div>
<DropdownMenu align="left" {anchor} bind:this={dropdown}>
<DropdownContainer>
<DropdownItem
icon="ri-delete-bin-line"
title="Delete"
on:click={showModal}
data-cy="delete-datasource" />
</DropdownContainer>
</DropdownMenu>
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
okText="Delete Query"
onOk={deleteQuery}
title="Confirm Deletion">
Are you sure you wish to delete this query?
This action cannot be undone.
</ConfirmDialog>
<style>
div.icon {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
div.icon i {
font-size: 16px;
}
.actions {
padding: var(--spacing-xl);
display: grid;
grid-gap: var(--spacing-xl);
min-width: 400px;
}
h5 {
margin: 0;
font-weight: 500;
}
footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
</style>

View File

@ -1,17 +1,28 @@
<script>
import { TextArea } from "@budibase/bbui"
import { TextArea, Label, Input } from "@budibase/bbui"
import Editor from "./Editor.svelte"
const CAPTURE_VAR_INSIDE_MUSTACHE = /{{([^}]+)}}/g
const QueryTypes = {
SQL: "sql",
}
export let queryType
export let type
export let query
$: console.log(query)
// $: parameters = Array.from(
// query
// .matchAll(CAPTURE_VAR_INSIDE_MUSTACHE)
// .map(([_, paramName]) => paramName)
// )
</script>
{#if queryType === QueryTypes.SQL}
<!-- {#each parameters as param}
<Label grey extraSmall>{param}</Label>
<Input thin bind:value={query.params[param]} />
{/each} -->
{#if type === QueryTypes.SQL}
<Editor label="Query" bind:value={query} />
{/if}

View File

@ -24,7 +24,7 @@
<Label size="m" color="dark">Query</Label>
<Select secondary bind:value={parameters.queryId}>
<option value="" />
{#each Object.keys(datasource.queries) as query}
{#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query}
<option value={query}>{datasource.queries[query].name}</option>
{/each}
</Select>

View File

@ -31,17 +31,13 @@
return [...acc, ...viewsArr]
}, [])
$: queries = $backendUiStore.datasources.reduce((acc, cur) => {
let queriesArr = Object.entries(cur.queries).map(([key, value]) => ({
label: value.name,
name: value.name,
datasourceId: cur._id,
queryId: key,
schema: value.schema,
$: queries = $backendUiStore.queries.map(query => ({
label: query.name,
name: query.name,
...query,
schema: query.schema,
type: "query",
}))
return [...acc, ...queriesArr]
}, [])
}))
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,

View File

@ -3,13 +3,10 @@
import { backendUiStore } from "builderStore"
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
// TODO: refactor
$: datasource = $backendUiStore.datasources.find(
ds => ds._id === $params.selectedDatasource
)
$: query = datasource && datasource.queries[$params.query]
$: query = $backendUiStore.queries.find(query => query._id === $params.query)
$: console.log("updated", query)
</script>
{#if $backendUiStore.selectedDatabase._id && datasource && query}
<ExternalDataSourceTable {query} {datasource} />
{#if $backendUiStore.selectedDatabase._id && query}
<ExternalDataSourceTable {query} />
{/if}

View File

@ -3,9 +3,9 @@ import API from "./api"
/**
* Fetches all rows from a query.
*/
export const fetchQueryData = async ({ datasourceId, queryId }) => {
export const fetchQueryData = async ({ _id }) => {
const response = await API.get({
url: `/api/datasources/${datasourceId}/queries/${queryId}`,
url: `/api/queries/${_id}`,
})
return response.rows
}
@ -13,9 +13,9 @@ export const fetchQueryData = async ({ datasourceId, queryId }) => {
/**
* Executes a query against an external data connector.
*/
export const executeQuery = async ({ datasourceId, queryId }) => {
export const executeQuery = async ({ _id }) => {
const response = await API.post({
url: `/api/datasources/${datasourceId}/queries/${queryId}`,
url: `/api/queries/${_id}`,
// body: params,
})
return response.rows

View File

@ -1,11 +1,6 @@
const CouchDB = require("../../db")
const bcrypt = require("../../utilities/bcrypt")
const {
generateDatasourceID,
getDatasourceParams,
generateQueryID,
} = require("../../db/utils")
const { integrations } = require("../../integrations")
const { generateDatasourceID, getDatasourceParams } = require("../../db/utils")
exports.fetch = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
@ -30,7 +25,6 @@ exports.save = async function(ctx) {
const datasource = {
_id: generateDatasourceID(),
type: "datasource",
queries: {},
...ctx.request.body,
}
@ -77,90 +71,3 @@ exports.find = async function(ctx) {
const datasource = await database.get(ctx.params.datasourceId)
ctx.body = datasource
}
exports.saveQuery = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const query = ctx.request.body
//
// {
// type: "",
// query: "",
// otherStuff: ""
// }
const datasource = await db.get(ctx.params.datasourceId)
const queryId = generateQueryID()
datasource.queries[queryId] = query
const response = await db.put(datasource)
datasource._rev = response.rev
ctx.body = datasource
ctx.message = `Query ${query.name} saved successfully.`
}
exports.previewQuery = async function(ctx) {
const { type, config, query } = ctx.request.body
const Integration = integrations[type]
if (!Integration) {
ctx.throw(400, "Integration type does not exist.")
return
}
ctx.body = await new Integration(config, query).query()
}
exports.fetchQuery = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const datasource = await db.get(ctx.params.datasourceId)
const query = datasource.queries[ctx.params.queryId]
const Integration = integrations[datasource.source]
if (!Integration) {
ctx.throw(400, "Integration type does not exist.")
return
}
const rows = await new Integration(
datasource.config,
query.queryString
).query()
ctx.body = {
schema: query.schema,
rows,
}
}
exports.executeQuery = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const datasource = await db.get(ctx.params.datasourceId)
const query = datasource.queries[ctx.params.queryId]
const Integration = integrations[datasource.source]
if (!Integration) {
ctx.throw(400, "Integration type does not exist.")
return
}
// TODO: allow the ability to POST parameters down when executing the query
// const customParams = ctx.request.body
const response = await new Integration(
datasource.config,
query.queryString
).query()
ctx.body = response
}

View File

@ -1,39 +1,128 @@
// const CouchDB = require("../../../db")
// const { generateQueryID } = require("../../db/utils")
// const viewTemplate = require("./viewBuilder")
const handlebars = require("handlebars")
const Joi = require("joi")
const CouchDB = require("../../db")
const bcrypt = require("../../utilities/bcrypt")
const { generateQueryID, getQueryParams } = require("../../db/utils")
const { integrations } = require("../../integrations")
const joiValidator = require("../../middleware/joi-validator")
// exports.save = async ctx => {
// const db = new CouchDB(ctx.user.appId)
// const { datasourceId, query } = ctx.request.body
function generateQueryValidation() {
// prettier-ignore
return joiValidator.body(Joi.object({
name: Joi.string().required(),
queryString: Joi.string().required(),
datasourceId: Joi.string().required(),
queryType: Joi.string().required(),
schema: Joi.object({}).required().unknown(true)
}))
}
// const datasource = await db.get(datasourceId)
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
// const queryId = generateQueryID()
const body = await db.allDocs(
getQueryParams(null, {
include_docs: true,
})
)
ctx.body = body.rows.map(row => row.doc)
}
// datasource.queries[queryId] = query
exports.save = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const query = ctx.request.body
// const response = await db.put(datasource)
//
// {
// type: "",
// query: "",
// otherStuff: ""
// }
// ctx.body = query
// ctx.message = `View ${viewToSave.name} saved successfully.`
// }
if (!query._id) {
query._id = generateQueryID(query.datasourceId)
}
// exports.destroy = async ctx => {
// const db = new CouchDB(ctx.user.appId)
// const designDoc = await db.get("_design/database")
const response = await db.put(query)
query._rev = response.rev
// const viewName = decodeURI(ctx.params.viewName)
ctx.body = query
ctx.message = `Query ${query.name} saved successfully.`
}
// const view = designDoc.views[viewName]
exports.preview = async function(ctx) {
const { query, datasourceId } = ctx.request.body
// delete designDoc.views[viewName]
const db = new CouchDB(ctx.user.appId)
// await db.put(designDoc)
const datasource = await db.get(datasourceId)
// const table = await db.get(view.meta.tableId)
// delete table.views[viewName]
// await db.put(table)
const Integration = integrations[datasource.source]
// ctx.body = view
// ctx.message = `View ${ctx.params.viewName} saved successfully.`
// }
if (!Integration) {
ctx.throw(400, "Integration type does not exist.")
return
}
ctx.body = await new Integration(datasource.config, query).query()
}
exports.execute = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const datasource = await db.get(ctx.params.datasourceId)
const query = datasource.queries[ctx.params.queryId]
const Integration = integrations[datasource.source]
if (!Integration) {
ctx.throw(400, "Integration type does not exist.")
return
}
// TODO: allow the ability to POST parameters down when executing the query
// const customParams = ctx.request.body
const queryTemplate = handlebars.compile(query.queryString)
const response = await new Integration(
datasource.config,
queryTemplate({
// pass the params here from the UI and backend contexts
})
).query()
ctx.body = response
}
exports.fetchQuery = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const query = await db.get(ctx.params.queryId)
const datasource = await db.get(query.datasourceId)
const Integration = integrations[datasource.source]
if (!Integration) {
ctx.throw(400, "Integration type does not exist.")
return
}
const rows = await new Integration(
datasource.config,
query.queryString
).query()
ctx.body = {
schema: query.schema,
rows,
}
}
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
await db.destroy(ctx.params.queryId)
ctx.message = `Query deleted.`
ctx.status = 200
}

View File

@ -17,26 +17,26 @@ router
datasourceController.find
)
.post("/api/datasources", authorized(BUILDER), datasourceController.save)
.post(
"/api/datasources/:datasourceId/queries",
authorized(BUILDER),
datasourceController.saveQuery
)
.post(
"/api/datasources/queries/preview",
authorized(BUILDER),
datasourceController.previewQuery
)
.get(
"/api/datasources/:datasourceId/queries/:queryId",
authorized(BUILDER),
datasourceController.fetchQuery
)
.post(
"/api/datasources/:datasourceId/queries/:queryId",
authorized(BUILDER),
datasourceController.executeQuery
)
// .post(
// "/api/datasources/:datasourceId/queries",
// authorized(BUILDER),
// datasourceController.saveQuery
// )
// .post(
// "/api/datasources/queries/preview",
// authorized(BUILDER),
// datasourceController.previewQuery
// )
// .get(
// "/api/datasources/:datasourceId/queries/:queryId",
// authorized(BUILDER),
// datasourceController.fetchQuery
// )
// .post(
// "/api/datasources/:datasourceId/queries/:queryId",
// authorized(BUILDER),
// datasourceController.executeQuery
// )
.delete(
"/api/datasources/:datasourceId/:revId",
authorized(BUILDER),

View File

@ -19,7 +19,7 @@ const routingRoutes = require("./routing")
const integrationRoutes = require("./integration")
const permissionRoutes = require("./permission")
const datasourceRoutes = require("./datasource")
// const queryRoutes = require("./query")
const queryRoutes = require("./query")
exports.mainRoutes = [
deployRoutes,
@ -39,7 +39,7 @@ exports.mainRoutes = [
integrationRoutes,
permissionRoutes,
datasourceRoutes,
// queryRoutes,
queryRoutes,
// these need to be handled last as they still use /api/:tableId
// this could be breaking as koa may recognise other routes as this
tableRoutes,

View File

@ -1,28 +1,17 @@
// const Router = require("@koa/router")
// const queryController = require("../controllers/query")
// const authorized = require("../../middleware/authorized")
// const { BUILDER } = require("../../utilities/security/permissions")
const Router = require("@koa/router")
const queryController = require("../controllers/query")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions")
// const router = Router()
const router = Router()
// // TODO: send down the datasource ID as well
// TODO: sort out auth so apps have the right permissions
router
.get("/api/queries", authorized(BUILDER), queryController.fetch)
.get("/api/queries/:queryId", authorized(BUILDER), queryController.fetchQuery)
.post("/api/queries", authorized(BUILDER), queryController.save)
.post("/api/queries/preview", authorized(BUILDER), queryController.preview)
.post("/api/queries/:queryId", authorized(BUILDER), queryController.execute)
.delete("/api/queries/:queryId", authorized(BUILDER), queryController.destroy)
// router
// // .get("/api/queries", authorized(BUILDER), queryController.fetch)
// // .get(
// // "/api/datasources/:datasourceId/queries/:id",
// // authorized(PermissionTypes.TABLE, PermissionLevels.READ),
// // queryController.find
// // )
// .post(
// "/api/datasources/:datasourceId/queries",
// authorized(BUILDER),
// queryController.save
// )
// .delete(
// "/api/datasources/:datasourceId/queries/:queryId/:revId",
// authorized(BUILDER),
// queryController.destroy
// )
// module.exports = router
module.exports = router

View File

@ -38,6 +38,6 @@ function replicateLocal() {
})
}
// replicateLocal()
replicateLocal()
module.exports = Pouch

View File

@ -245,8 +245,10 @@ exports.getDatasourceParams = (datasourceId = null, otherProps = {}) => {
* Generates a new query ID.
* @returns {string} The new query ID which the query doc can be stored under.
*/
exports.generateQueryID = () => {
return `${DocumentTypes.QUERY}${SEPARATOR}${newid()}`
exports.generateQueryID = datasourceId => {
return `${
DocumentTypes.QUERY
}${SEPARATOR}${datasourceId}${SEPARATOR}${newid()}`
}
/**

View File

@ -14,6 +14,7 @@ const PermissionTypes = {
WEBHOOK: "webhook",
BUILDER: "builder",
VIEW: "view",
QUERY: "query",
}
function Permission(type, level) {