custom fields in queries

This commit is contained in:
Martin McKeaveney 2021-01-13 14:11:53 +00:00
parent 33d63607e2
commit 94ee5855a5
10 changed files with 223 additions and 79 deletions

View File

@ -0,0 +1,69 @@
<script>
import {
Button,
TextArea,
Label,
Input,
Heading,
Spacer,
Select
} from "@budibase/bbui"
export let fields = {}
export let schema
let customSchema = {}
let draftField = {}
function addField() {
// Add the new field to custom fields for the query
customSchema[draftField.name] = {
type: draftField.type
}
// reset the draft field
draftField = {}
}
</script>
<form on:submit|preventDefault>
{#each Object.keys(schema.fields) as field}
<Label extraSmall grey>{field}</Label>
<Input
type={schema.fields[field]?.type}
required={schema.fields[field]?.required}
bind:value={fields[field]} />
<Spacer medium />
{/each}
{#if schema.customisable}
<Label>Add Custom Field</Label>
{#each Object.keys(customSchema) as field}
<Label extraSmall grey>{field}</Label>
<Input
thin
type={customSchema[field]?.type}
bind:value={fields[field]}
/>
<Spacer medium />
{/each}
<div class="new-field">
<Label extraSmall grey>Name</Label>
<Label extraSmall grey>Type</Label>
<Input thin bind:value={draftField.name} />
<Select thin secondary bind:value={draftField.name}>
<option value={"text"}>String</option>
<option value={"number"}>Number</option>
</Select>
</div>
<Button small thin primary on:click={addField}>Add Field</Button>
{/if}
</form>
<style>
.new-field {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: var(--spacing-m);
margin-top: var(--spacing-m);
margin-bottom: var(--spacing-m);
}
</style>

View File

@ -37,7 +37,7 @@
export let query export let query
export let fields = [] export let fields = []
let config = {} let config
let tab = "JSON" let tab = "JSON"
let parameters let parameters
let data let data
@ -60,6 +60,8 @@
$: datasourceType = datasource.source $: datasourceType = datasource.source
$: datasourceType && fetchQueryConfig() $: datasourceType && fetchQueryConfig()
$: shouldShowQueryConfig = config && query.queryVerb && query.queryType
function newField() { function newField() {
fields = [...fields, {}] fields = [...fields, {}]
} }
@ -75,8 +77,7 @@
const json = await response.json() const json = await response.json()
config = json.query config = json.query
} catch (err) { } catch (err) {
// TODO: Error fetching integration config notifier.danger("Error fetching integration configuration.")
// notifier.danger()
console.error(err) console.error(err)
} }
} }
@ -84,6 +85,7 @@
async function previewQuery() { async function previewQuery() {
try { try {
const response = await api.post(`/api/queries/preview`, { const response = await api.post(`/api/queries/preview`, {
fields: query.fields,
queryVerb: query.queryVerb, queryVerb: query.queryVerb,
parameters: query.parameters.reduce( parameters: query.parameters.reduce(
(acc, next) => ({ (acc, next) => ({
@ -93,7 +95,6 @@
{} {}
), ),
datasourceId: datasource._id, datasourceId: datasource._id,
query: query.queryString,
}) })
const json = await response.json() const json = await response.json()
@ -107,6 +108,7 @@
name: field, name: field,
type: "STRING", type: "STRING",
})) }))
notifier.success("Query executed successfully.")
} catch (err) { } catch (err) {
notifier.danger(`Query Error: ${err.message}`) notifier.danger(`Query Error: ${err.message}`)
console.error(err) console.error(err)
@ -138,17 +140,19 @@
</div> </div>
{/each} {/each}
</div> </div>
<Select thin secondary bind:value={query.queryType}> {#if config && query.queryVerb}
<option value={''}>Select an option</option> <Select thin secondary bind:value={query.queryType}>
{#each Object.keys(config) as queryType} <option value={''}>Select an option</option>
<option value={config[queryType].type}>{queryType}</option> {#each Object.keys(config[query.queryVerb]) as queryType}
{/each} <option value={queryType}>{queryType}</option>
</Select> {/each}
</Select>
{/if}
</header> </header>
<Spacer large /> <Spacer large />
{#if query.queryVerb && query.queryType} {#if shouldShowQueryConfig}
<section> <section>
<div class="config"> <div class="config">
<Label extraSmall grey>Query Name</Label> <Label extraSmall grey>Query Name</Label>
@ -156,7 +160,10 @@
<Spacer medium /> <Spacer medium />
<IntegrationQueryEditor {query} bind:parameters /> <IntegrationQueryEditor
schema={config[query.queryVerb][query.queryType]}
{query}
bind:parameters />
<Spacer medium /> <Spacer medium />

View File

@ -2,6 +2,7 @@
import { TextArea, Label, Input, Heading, Spacer } from "@budibase/bbui" import { TextArea, Label, Input, Heading, Spacer } from "@budibase/bbui"
import Editor from "./SvelteEditor.svelte" import Editor from "./SvelteEditor.svelte"
import ParameterBuilder from "./QueryParameterBuilder.svelte" import ParameterBuilder from "./QueryParameterBuilder.svelte"
import FieldsBuilder from "./QueryFieldsBuilder.svelte"
const QueryTypes = { const QueryTypes = {
SQL: "sql", SQL: "sql",
@ -10,9 +11,10 @@
} }
export let query export let query
export let schema
function updateQuery({ detail }) { function updateQuery({ detail }) {
query.queryString = detail.value query.fields.sql = detail.value
} }
</script> </script>
@ -22,20 +24,19 @@
<Heading extraSmall black>Query</Heading> <Heading extraSmall black>Query</Heading>
<Spacer large /> <Spacer large />
{#if query.queryType === QueryTypes.SQL} {#if schema.type === QueryTypes.SQL}
<!-- <TextArea bind:value={query.queryString} /> -->
<Editor <Editor
label="Query" label="Query"
mode="sql" mode="sql"
on:change={updateQuery} on:change={updateQuery}
value={query.queryString} /> value={query.fields.sql} />
{:else if query.queryType === QueryTypes.JSON} {:else if schema.type === QueryTypes.JSON}
<Spacer large /> <Spacer large />
<Editor <Editor
label="Query" label="Query"
mode="json" mode="json"
on:change={updateQuery} on:change={updateQuery}
value={query.queryString} /> value={query.fields.json} />
{:else if query.queryType === QueryTypes.FIELDS} {:else if schema.type === QueryTypes.FIELDS}
<!-- {#each Object.keys()} --> <FieldsBuilder bind:fields={query.fields} {schema} />
{/if} {/if}

View File

@ -57,7 +57,9 @@
bind:customParams={parameters.queryParams} bind:customParams={parameters.queryParams}
parameters={query.parameters} parameters={query.parameters}
bindings={bindableProperties} /> bindings={bindableProperties} />
<pre>{query.queryString}</pre> {#if query.fields.sql}
<pre>{query.fields.queryString}</pre>
{/if}
{/if} {/if}
</div> </div>

View File

@ -26,6 +26,7 @@
datasourceId: $params.selectedDatasource, datasourceId: $params.selectedDatasource,
name: "New Query", name: "New Query",
parameters: [], parameters: [],
fields: {},
} }
} }
} }

View File

@ -29,18 +29,20 @@ exports.save = async function(ctx) {
ctx.message = `Query ${query.name} saved successfully.` ctx.message = `Query ${query.name} saved successfully.`
} }
exports.preview = async function(ctx) { function enrichQueryFields(fields, parameters) {
const { query, datasourceId, parameters, queryVerb } = ctx.request.body const enrichedQuery = {}
// enrich the fields with dynamic parameters
let parsedQuery = "" for (let key in fields) {
if (query) { const template = handlebars.compile(fields[key])
const queryTemplate = handlebars.compile(query) enrichedQuery[key] = template(parameters)
parsedQuery = queryTemplate(parameters)
} }
return enrichedQuery
}
exports.preview = async function(ctx) {
const db = new CouchDB(ctx.user.appId) const db = new CouchDB(ctx.user.appId)
const datasource = await db.get(datasourceId) const datasource = await db.get(ctx.request.body.datasourceId)
const Integration = integrations[datasource.source] const Integration = integrations[datasource.source]
@ -49,7 +51,11 @@ exports.preview = async function(ctx) {
return return
} }
ctx.body = await new Integration(datasource.config, parsedQuery)[queryVerb]() const { fields, parameters, queryVerb } = ctx.request.body
const enrichedQuery = enrichQueryFields(fields, parameters)
ctx.body = await new Integration(datasource.config)[queryVerb](enrichedQuery)
} }
exports.execute = async function(ctx) { exports.execute = async function(ctx) {
@ -58,10 +64,6 @@ exports.execute = async function(ctx) {
const query = await db.get(ctx.params.queryId) const query = await db.get(ctx.params.queryId)
const datasource = await db.get(query.datasourceId) const datasource = await db.get(query.datasourceId)
const queryTemplate = handlebars.compile(query.queryString)
const parsedQuery = queryTemplate(ctx.request.body.parameters)
const Integration = integrations[datasource.source] const Integration = integrations[datasource.source]
if (!Integration) { if (!Integration) {
@ -69,10 +71,15 @@ exports.execute = async function(ctx) {
return return
} }
const enrichedQuery = enrichQueryFields(
query.fields,
ctx.request.body.parameters
)
// call the relevant CRUD method on the integration class // call the relevant CRUD method on the integration class
const response = await new Integration(datasource.config, parsedQuery)[ const response = await new Integration(datasource.config)[query.queryVerb](
query.queryVerb enrichedQuery
]() )
ctx.body = response ctx.body = response
} }

View File

@ -24,7 +24,7 @@ function generateQueryValidation() {
_id: Joi.string(), _id: Joi.string(),
_rev: Joi.string(), _rev: Joi.string(),
name: Joi.string().required(), name: Joi.string().required(),
queryString: Joi.string().required(), fields: Joi.object().required(),
datasourceId: Joi.string().required(), datasourceId: Joi.string().required(),
parameters: Joi.array().items(Joi.object({ parameters: Joi.array().items(Joi.object({
name: Joi.string(), name: Joi.string(),
@ -39,7 +39,7 @@ function generateQueryValidation() {
function generateQueryPreviewValidation() { function generateQueryPreviewValidation() {
// prettier-ignore // prettier-ignore
return joiValidator.body(Joi.object({ return joiValidator.body(Joi.object({
query: Joi.string(), fields: Joi.object().required(),
queryVerb: Joi.string().allow(...Object.values(QueryVerb)).required(), queryVerb: Joi.string().allow(...Object.values(QueryVerb)).required(),
datasourceId: Joi.string().required(), datasourceId: Joi.string().required(),
parameters: Joi.object({}).required().unknown(true) parameters: Joi.object({}).required().unknown(true)

View File

@ -0,0 +1,7 @@
class Field {
constructor(type, defaultValue, required) {
this.type = type
this.default = defaultValue
this.required = required
}
}

View File

@ -12,19 +12,51 @@ const SCHEMA = {
default: "mybase", default: "mybase",
required: true, required: true,
}, },
table: {
type: "string",
default: "mytable",
required: true,
},
}, },
query: { query: {
Custom: { create: {
type: "fields", "Airtable Record": {
custom: true, type: "fields",
customisable: true,
fields: {
table: {
type: "string",
required: true,
},
},
},
}, },
"Airtable Ids": { read: {
type: "list", Table: {
type: "fields",
fields: {
table: {
type: "string",
required: true,
},
view: {
type: "string",
required: true,
},
},
},
},
update: {
Fields: {
type: "fields",
customisable: true,
fields: {
id: {
type: "string",
required: true,
},
},
},
},
delete: {
"Airtable Ids": {
type: "list",
},
}, },
}, },
} }
@ -35,11 +67,13 @@ class AirtableIntegration {
this.client = new Airtable(config).base(config.base) this.client = new Airtable(config).base(config.base)
} }
async create(record) { async create(query) {
const { table, ...rest } = query
try { try {
const records = await this.client(this.config.table).create([ const records = await this.client(table).create([
{ {
fields: record, fields: rest,
}, },
]) ])
return records return records
@ -49,18 +83,23 @@ class AirtableIntegration {
} }
} }
async read() { async read(query) {
const records = await this.client(this.config.table) try {
.select({ maxRecords: this.query.records, view: this.query.view }) const records = await this.client(query.table)
.firstPage() .select({ maxRecords: 10, view: query.view })
return records.map(({ fields }) => fields) .firstPage()
return records.map(({ fields }) => fields)
} catch (err) {
console.error("Error writing to airtable", err)
return []
}
} }
async update(document) { async update(query) {
const { id, ...rest } = document const { table, id, ...rest } = query
try { try {
const records = await this.client(this.config.table).update([ const records = await this.client(table).update([
{ {
id, id,
fields: rest, fields: rest,
@ -73,9 +112,9 @@ class AirtableIntegration {
} }
} }
async delete(id) { async delete(query) {
try { try {
const records = await this.client(this.config.table).destroy([id]) const records = await this.client(query.table).destroy(query.ids)
return records return records
} catch (err) { } catch (err) {
console.error("Error writing to airtable", err) console.error("Error writing to airtable", err)

View File

@ -29,46 +29,57 @@ const SCHEMA = {
}, },
}, },
query: { query: {
SQL: { create: {
type: "sql", SQL: {
type: "sql",
},
},
read: {
SQL: {
type: "sql",
},
},
update: {
SQL: {
type: "sql",
},
},
delete: {
SQL: {
type: "sql",
},
}, },
}, },
} }
class PostgresIntegration { class PostgresIntegration {
constructor(config, query) { constructor(config) {
this.config = config this.config = config
this.queryString = this.buildQuery(query)
this.client = new Client(config) this.client = new Client(config)
this.connect() this.connect()
} }
buildQuery(query) {
// TODO: account for different types
return query
}
async connect() { async connect() {
return this.client.connect() return this.client.connect()
} }
async create() { async create({ sql }) {
const response = await this.client.query(this.queryString) const response = await this.client.query(sql)
return response.rows return response.rows
} }
async read() { async read({ sql }) {
const response = await this.client.query(this.queryString) const response = await this.client.query(sql)
return response.rows return response.rows
} }
async update() { async update({ sql }) {
const response = await this.client.query(this.queryString) const response = await this.client.query(sql)
return response.rows return response.rows
} }
async delete() { async delete({ sql }) {
const response = await this.client.query(this.queryString) const response = await this.client.query(sql)
return response.rows return response.rows
} }
} }