From 94ee5855a519920e83ad46386ba92ac7057e12f3 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Wed, 13 Jan 2021 14:11:53 +0000 Subject: [PATCH] custom fields in queries --- .../integration/QueryFieldsBuilder.svelte | 69 +++++++++++++++ .../components/integration/QueryViewer.svelte | 31 ++++--- .../src/components/integration/index.svelte | 17 ++-- .../EventsEditor/actions/ExecuteQuery.svelte | 4 +- .../[selectedDatasource]/[query]/index.svelte | 1 + packages/server/src/api/controllers/query.js | 39 +++++---- packages/server/src/api/routes/query.js | 4 +- .../server/src/integrations/Integration.js | 7 ++ packages/server/src/integrations/airtable.js | 85 ++++++++++++++----- packages/server/src/integrations/postgres.js | 45 ++++++---- 10 files changed, 223 insertions(+), 79 deletions(-) create mode 100644 packages/builder/src/components/integration/QueryFieldsBuilder.svelte diff --git a/packages/builder/src/components/integration/QueryFieldsBuilder.svelte b/packages/builder/src/components/integration/QueryFieldsBuilder.svelte new file mode 100644 index 0000000000..cf067dfb93 --- /dev/null +++ b/packages/builder/src/components/integration/QueryFieldsBuilder.svelte @@ -0,0 +1,69 @@ + + +
+ {#each Object.keys(schema.fields) as field} + + + + {/each} + {#if schema.customisable} + + {#each Object.keys(customSchema) as field} + + + + {/each} +
+ + + + +
+ + {/if} + + + \ No newline at end of file diff --git a/packages/builder/src/components/integration/QueryViewer.svelte b/packages/builder/src/components/integration/QueryViewer.svelte index 99d3afbdf4..48eda4016b 100644 --- a/packages/builder/src/components/integration/QueryViewer.svelte +++ b/packages/builder/src/components/integration/QueryViewer.svelte @@ -37,7 +37,7 @@ export let query export let fields = [] - let config = {} + let config let tab = "JSON" let parameters let data @@ -60,6 +60,8 @@ $: datasourceType = datasource.source $: datasourceType && fetchQueryConfig() + $: shouldShowQueryConfig = config && query.queryVerb && query.queryType + function newField() { fields = [...fields, {}] } @@ -75,8 +77,7 @@ const json = await response.json() config = json.query } catch (err) { - // TODO: Error fetching integration config - // notifier.danger() + notifier.danger("Error fetching integration configuration.") console.error(err) } } @@ -84,6 +85,7 @@ async function previewQuery() { try { const response = await api.post(`/api/queries/preview`, { + fields: query.fields, queryVerb: query.queryVerb, parameters: query.parameters.reduce( (acc, next) => ({ @@ -93,7 +95,6 @@ {} ), datasourceId: datasource._id, - query: query.queryString, }) const json = await response.json() @@ -107,6 +108,7 @@ name: field, type: "STRING", })) + notifier.success("Query executed successfully.") } catch (err) { notifier.danger(`Query Error: ${err.message}`) console.error(err) @@ -138,17 +140,19 @@ {/each} - + {#if config && query.queryVerb} + + {/if} -{#if query.queryVerb && query.queryType} +{#if shouldShowQueryConfig}
@@ -156,7 +160,10 @@ - + diff --git a/packages/builder/src/components/integration/index.svelte b/packages/builder/src/components/integration/index.svelte index 9e3e9aaa08..b0003ef2ef 100644 --- a/packages/builder/src/components/integration/index.svelte +++ b/packages/builder/src/components/integration/index.svelte @@ -2,6 +2,7 @@ import { TextArea, Label, Input, Heading, Spacer } from "@budibase/bbui" import Editor from "./SvelteEditor.svelte" import ParameterBuilder from "./QueryParameterBuilder.svelte" + import FieldsBuilder from "./QueryFieldsBuilder.svelte" const QueryTypes = { SQL: "sql", @@ -10,9 +11,10 @@ } export let query + export let schema function updateQuery({ detail }) { - query.queryString = detail.value + query.fields.sql = detail.value } @@ -22,20 +24,19 @@ Query -{#if query.queryType === QueryTypes.SQL} - +{#if schema.type === QueryTypes.SQL} -{:else if query.queryType === QueryTypes.JSON} + value={query.fields.sql} /> +{:else if schema.type === QueryTypes.JSON} -{:else if query.queryType === QueryTypes.FIELDS} - + value={query.fields.json} /> +{:else if schema.type === QueryTypes.FIELDS} + {/if} diff --git a/packages/builder/src/components/userInterface/EventsEditor/actions/ExecuteQuery.svelte b/packages/builder/src/components/userInterface/EventsEditor/actions/ExecuteQuery.svelte index 2cbfbcd6e3..910fa1e593 100644 --- a/packages/builder/src/components/userInterface/EventsEditor/actions/ExecuteQuery.svelte +++ b/packages/builder/src/components/userInterface/EventsEditor/actions/ExecuteQuery.svelte @@ -57,7 +57,9 @@ bind:customParams={parameters.queryParams} parameters={query.parameters} bindings={bindableProperties} /> -
{query.queryString}
+ {#if query.fields.sql} +
{query.fields.queryString}
+ {/if} {/if}
diff --git a/packages/builder/src/pages/[application]/data/datasource/[selectedDatasource]/[query]/index.svelte b/packages/builder/src/pages/[application]/data/datasource/[selectedDatasource]/[query]/index.svelte index c10813076e..bc2cae7537 100644 --- a/packages/builder/src/pages/[application]/data/datasource/[selectedDatasource]/[query]/index.svelte +++ b/packages/builder/src/pages/[application]/data/datasource/[selectedDatasource]/[query]/index.svelte @@ -26,6 +26,7 @@ datasourceId: $params.selectedDatasource, name: "New Query", parameters: [], + fields: {}, } } } diff --git a/packages/server/src/api/controllers/query.js b/packages/server/src/api/controllers/query.js index d967c83d09..257e644b3c 100644 --- a/packages/server/src/api/controllers/query.js +++ b/packages/server/src/api/controllers/query.js @@ -29,18 +29,20 @@ exports.save = async function(ctx) { ctx.message = `Query ${query.name} saved successfully.` } -exports.preview = async function(ctx) { - const { query, datasourceId, parameters, queryVerb } = ctx.request.body - - let parsedQuery = "" - if (query) { - const queryTemplate = handlebars.compile(query) - parsedQuery = queryTemplate(parameters) +function enrichQueryFields(fields, parameters) { + const enrichedQuery = {} + // enrich the fields with dynamic parameters + for (let key in fields) { + const template = handlebars.compile(fields[key]) + enrichedQuery[key] = template(parameters) } + return enrichedQuery +} +exports.preview = async function(ctx) { 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] @@ -49,7 +51,11 @@ exports.preview = async function(ctx) { 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) { @@ -58,10 +64,6 @@ exports.execute = async function(ctx) { const query = await db.get(ctx.params.queryId) 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] if (!Integration) { @@ -69,10 +71,15 @@ exports.execute = async function(ctx) { return } + const enrichedQuery = enrichQueryFields( + query.fields, + ctx.request.body.parameters + ) + // call the relevant CRUD method on the integration class - const response = await new Integration(datasource.config, parsedQuery)[ - query.queryVerb - ]() + const response = await new Integration(datasource.config)[query.queryVerb]( + enrichedQuery + ) ctx.body = response } diff --git a/packages/server/src/api/routes/query.js b/packages/server/src/api/routes/query.js index 8d1526f1e2..62ddc83ac0 100644 --- a/packages/server/src/api/routes/query.js +++ b/packages/server/src/api/routes/query.js @@ -24,7 +24,7 @@ function generateQueryValidation() { _id: Joi.string(), _rev: Joi.string(), name: Joi.string().required(), - queryString: Joi.string().required(), + fields: Joi.object().required(), datasourceId: Joi.string().required(), parameters: Joi.array().items(Joi.object({ name: Joi.string(), @@ -39,7 +39,7 @@ function generateQueryValidation() { function generateQueryPreviewValidation() { // prettier-ignore return joiValidator.body(Joi.object({ - query: Joi.string(), + fields: Joi.object().required(), queryVerb: Joi.string().allow(...Object.values(QueryVerb)).required(), datasourceId: Joi.string().required(), parameters: Joi.object({}).required().unknown(true) diff --git a/packages/server/src/integrations/Integration.js b/packages/server/src/integrations/Integration.js index e69de29bb2..2305f0ea55 100644 --- a/packages/server/src/integrations/Integration.js +++ b/packages/server/src/integrations/Integration.js @@ -0,0 +1,7 @@ +class Field { + constructor(type, defaultValue, required) { + this.type = type + this.default = defaultValue + this.required = required + } +} diff --git a/packages/server/src/integrations/airtable.js b/packages/server/src/integrations/airtable.js index f21fc3e301..b7300ac446 100644 --- a/packages/server/src/integrations/airtable.js +++ b/packages/server/src/integrations/airtable.js @@ -12,19 +12,51 @@ const SCHEMA = { default: "mybase", required: true, }, - table: { - type: "string", - default: "mytable", - required: true, - }, }, query: { - Custom: { - type: "fields", - custom: true, + create: { + "Airtable Record": { + type: "fields", + customisable: true, + fields: { + table: { + type: "string", + required: true, + }, + }, + }, }, - "Airtable Ids": { - type: "list", + read: { + 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) } - async create(record) { + async create(query) { + const { table, ...rest } = query + try { - const records = await this.client(this.config.table).create([ + const records = await this.client(table).create([ { - fields: record, + fields: rest, }, ]) return records @@ -49,18 +83,23 @@ class AirtableIntegration { } } - async read() { - const records = await this.client(this.config.table) - .select({ maxRecords: this.query.records, view: this.query.view }) - .firstPage() - return records.map(({ fields }) => fields) + async read(query) { + try { + const records = await this.client(query.table) + .select({ maxRecords: 10, view: query.view }) + .firstPage() + return records.map(({ fields }) => fields) + } catch (err) { + console.error("Error writing to airtable", err) + return [] + } } - async update(document) { - const { id, ...rest } = document + async update(query) { + const { table, id, ...rest } = query try { - const records = await this.client(this.config.table).update([ + const records = await this.client(table).update([ { id, fields: rest, @@ -73,9 +112,9 @@ class AirtableIntegration { } } - async delete(id) { + async delete(query) { try { - const records = await this.client(this.config.table).destroy([id]) + const records = await this.client(query.table).destroy(query.ids) return records } catch (err) { console.error("Error writing to airtable", err) diff --git a/packages/server/src/integrations/postgres.js b/packages/server/src/integrations/postgres.js index 8b6ce6f618..d6e5041003 100644 --- a/packages/server/src/integrations/postgres.js +++ b/packages/server/src/integrations/postgres.js @@ -29,46 +29,57 @@ const SCHEMA = { }, }, query: { - SQL: { - type: "sql", + create: { + SQL: { + type: "sql", + }, + }, + read: { + SQL: { + type: "sql", + }, + }, + update: { + SQL: { + type: "sql", + }, + }, + delete: { + SQL: { + type: "sql", + }, }, }, } class PostgresIntegration { - constructor(config, query) { + constructor(config) { this.config = config - this.queryString = this.buildQuery(query) this.client = new Client(config) this.connect() } - buildQuery(query) { - // TODO: account for different types - return query - } - async connect() { return this.client.connect() } - async create() { - const response = await this.client.query(this.queryString) + async create({ sql }) { + const response = await this.client.query(sql) return response.rows } - async read() { - const response = await this.client.query(this.queryString) + async read({ sql }) { + const response = await this.client.query(sql) return response.rows } - async update() { - const response = await this.client.query(this.queryString) + async update({ sql }) { + const response = await this.client.query(sql) return response.rows } - async delete() { - const response = await this.client.query(this.queryString) + async delete({ sql }) { + const response = await this.client.query(sql) return response.rows } }