custom fields in queries
This commit is contained in:
parent
33d63607e2
commit
94ee5855a5
|
@ -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>
|
|
@ -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 />
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
datasourceId: $params.selectedDatasource,
|
datasourceId: $params.selectedDatasource,
|
||||||
name: "New Query",
|
name: "New Query",
|
||||||
parameters: [],
|
parameters: [],
|
||||||
|
fields: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
class Field {
|
||||||
|
constructor(type, defaultValue, required) {
|
||||||
|
this.type = type
|
||||||
|
this.default = defaultValue
|
||||||
|
this.required = required
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue