Merge pull request #6315 from Budibase/feature/current-user-rest-bindings
REST query bindings for Current User
This commit is contained in:
commit
9b98c617bf
|
@ -40,5 +40,6 @@
|
|||
on:change={onChange}
|
||||
on:pick
|
||||
on:type
|
||||
on:blur
|
||||
/>
|
||||
</Field>
|
||||
|
|
|
@ -52,7 +52,10 @@
|
|||
{id}
|
||||
type="text"
|
||||
on:focus={() => (focus = true)}
|
||||
on:blur={() => (focus = false)}
|
||||
on:blur={() => {
|
||||
focus = false
|
||||
dispatch("blur")
|
||||
}}
|
||||
on:change={onType}
|
||||
value={value || ""}
|
||||
placeholder={placeholder || ""}
|
||||
|
|
|
@ -52,13 +52,6 @@ filterTests(['smoke', 'all'], () => {
|
|||
|
||||
// Start create app process. If apps already exist, click second button
|
||||
cy.get(interact.CREATE_APP_BUTTON, { timeout: 1000 }).click({ force: true })
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
.its("body")
|
||||
.then(val => {
|
||||
if (val.length > 0) {
|
||||
cy.get(interact.CREATE_APP_BUTTON).click({ force: true })
|
||||
}
|
||||
})
|
||||
|
||||
const appName = "Cypress Tests"
|
||||
cy.get(interact.SPECTRUM_MODAL).within(() => {
|
||||
|
|
|
@ -49,6 +49,95 @@ export const getBindableProperties = (asset, componentId) => {
|
|||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all rest bindable data fields
|
||||
*/
|
||||
export const getRestBindings = () => {
|
||||
const userBindings = getUserBindings()
|
||||
return [...userBindings, ...getAuthBindings()]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all rest bindable auth fields
|
||||
*/
|
||||
export const getAuthBindings = () => {
|
||||
let bindings = []
|
||||
const safeUser = makePropSafe("user")
|
||||
const safeOAuth2 = makePropSafe("oauth2")
|
||||
const safeAccessToken = makePropSafe("accessToken")
|
||||
|
||||
const authBindings = [
|
||||
{
|
||||
runtime: `${safeUser}.${safeOAuth2}.${safeAccessToken}`,
|
||||
readable: `Current User.OAuthToken`,
|
||||
key: "accessToken",
|
||||
},
|
||||
]
|
||||
|
||||
bindings = Object.keys(authBindings).map(key => {
|
||||
const fieldBinding = authBindings[key]
|
||||
return {
|
||||
type: "context",
|
||||
runtimeBinding: fieldBinding.runtime,
|
||||
readableBinding: fieldBinding.readable,
|
||||
fieldSchema: { type: "string", name: fieldBinding.key },
|
||||
providerId: "user",
|
||||
}
|
||||
})
|
||||
return bindings
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility - convert a key/value map to an array of custom 'context' bindings
|
||||
* @param {object} valueMap Key/value pairings
|
||||
* @param {string} prefix A contextual string prefix/path for a user readable binding
|
||||
* @return {object[]} An array containing readable/runtime binding objects
|
||||
*/
|
||||
export const toBindingsArray = (valueMap, prefix) => {
|
||||
if (!valueMap) {
|
||||
return []
|
||||
}
|
||||
return Object.keys(valueMap).reduce((acc, binding) => {
|
||||
if (!binding || !valueMap[binding]) {
|
||||
return acc
|
||||
}
|
||||
acc.push({
|
||||
type: "context",
|
||||
runtimeBinding: binding,
|
||||
readableBinding: `${prefix}.${binding}`,
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility - coverting a map of readable bindings to runtime
|
||||
*/
|
||||
export const readableToRuntimeMap = (bindings, ctx) => {
|
||||
if (!bindings || !ctx) {
|
||||
return {}
|
||||
}
|
||||
return Object.keys(ctx).reduce((acc, key) => {
|
||||
let parsedQuery = readableToRuntimeBinding(bindings, ctx[key])
|
||||
acc[key] = parsedQuery
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility - coverting a map of runtime bindings to readable
|
||||
*/
|
||||
export const runtimeToReadableMap = (bindings, ctx) => {
|
||||
if (!bindings || !ctx) {
|
||||
return {}
|
||||
}
|
||||
return Object.keys(ctx).reduce((acc, key) => {
|
||||
let parsedQuery = runtimeToReadableBinding(bindings, ctx[key])
|
||||
acc[key] = parsedQuery
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the bindable properties exposed by a certain component.
|
||||
*/
|
||||
|
@ -298,7 +387,6 @@ const getUserBindings = () => {
|
|||
providerId: "user",
|
||||
})
|
||||
})
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
|
|
|
@ -10,11 +10,31 @@
|
|||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
import RestAuthenticationBuilder from "./auth/RestAuthenticationBuilder.svelte"
|
||||
import ViewDynamicVariables from "./variables/ViewDynamicVariables.svelte"
|
||||
import {
|
||||
getRestBindings,
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableMap,
|
||||
} from "builderStore/dataBinding"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
export let datasource
|
||||
export let queries
|
||||
|
||||
let addHeader
|
||||
|
||||
let parsedHeaders = runtimeToReadableMap(
|
||||
getRestBindings(),
|
||||
cloneDeep(datasource?.config?.defaultHeaders)
|
||||
)
|
||||
|
||||
const onDefaultHeaderUpdate = headers => {
|
||||
const flatHeaders = cloneDeep(headers).reduce((acc, entry) => {
|
||||
acc[entry.name] = readableToRuntimeBinding(getRestBindings(), entry.value)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
datasource.config.defaultHeaders = flatHeaders
|
||||
}
|
||||
</script>
|
||||
|
||||
<Divider size="S" />
|
||||
|
@ -30,9 +50,10 @@
|
|||
</Body>
|
||||
<KeyValueBuilder
|
||||
bind:this={addHeader}
|
||||
bind:object={datasource.config.defaultHeaders}
|
||||
on:change
|
||||
bind:object={parsedHeaders}
|
||||
on:change={evt => onDefaultHeaderUpdate(evt.detail)}
|
||||
noAddButton
|
||||
bindings={getRestBindings()}
|
||||
/>
|
||||
<div>
|
||||
<ActionButton icon="Add" on:click={() => addHeader.addEntry()}>
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
import { onMount } from "svelte"
|
||||
import { ModalContent, Layout, Select, Body, Input } from "@budibase/bbui"
|
||||
import { AUTH_TYPE_LABELS, AUTH_TYPES } from "./authTypes"
|
||||
import BindableCombobox from "components/common/bindings/BindableCombobox.svelte"
|
||||
import { getAuthBindings } from "builderStore/dataBinding"
|
||||
|
||||
export let configs
|
||||
export let currentConfig
|
||||
|
@ -203,11 +205,23 @@
|
|||
/>
|
||||
{/if}
|
||||
{#if form.type === AUTH_TYPES.BEARER}
|
||||
<Input
|
||||
<BindableCombobox
|
||||
label="Token"
|
||||
bind:value={form.bearer.token}
|
||||
on:change={onFieldChange}
|
||||
on:blur={() => (blurred.bearer.token = true)}
|
||||
value={form.bearer.token}
|
||||
bindings={getAuthBindings()}
|
||||
on:change={e => {
|
||||
form.bearer.token = e.detail
|
||||
console.log(e.detail)
|
||||
onFieldChange()
|
||||
}}
|
||||
on:blur={() => {
|
||||
blurred.bearer.token = true
|
||||
onFieldChange()
|
||||
}}
|
||||
allowJS={false}
|
||||
placeholder="Token"
|
||||
appendBindingsAsOptions={true}
|
||||
drawerEnabled={false}
|
||||
error={blurred.bearer.token ? errors.bearer.token : null}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
<script>
|
||||
import { Combobox } from "@budibase/bbui"
|
||||
import {
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { isJSBinding } from "@budibase/string-templates"
|
||||
|
||||
export let value = ""
|
||||
export let bindings = []
|
||||
export let placeholder
|
||||
export let label
|
||||
export let disabled = false
|
||||
export let options
|
||||
export let appendBindingsAsOptions = true
|
||||
export let error
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||
$: isJS = isJSBinding(value)
|
||||
$: allOptions = buildOptions(options, bindings, appendBindingsAsOptions)
|
||||
|
||||
const onChange = (value, optionPicked) => {
|
||||
// Add HBS braces if picking binding
|
||||
if (optionPicked && !options?.includes(value)) {
|
||||
value = `{{ ${value} }}`
|
||||
}
|
||||
|
||||
dispatch("change", readableToRuntimeBinding(bindings, value))
|
||||
}
|
||||
|
||||
const buildOptions = (options, bindings, appendBindingsAsOptions) => {
|
||||
if (!appendBindingsAsOptions) {
|
||||
return options
|
||||
}
|
||||
return []
|
||||
.concat(options || [])
|
||||
.concat(bindings?.map(binding => binding.readableBinding) || [])
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="control" class:disabled>
|
||||
<Combobox
|
||||
{label}
|
||||
{disabled}
|
||||
readonly={isJS}
|
||||
value={isJS ? "(JavaScript function)" : readableValue}
|
||||
on:type={e => onChange(e.detail, false)}
|
||||
on:pick={e => onChange(e.detail, true)}
|
||||
on:blur={() => dispatch("blur")}
|
||||
{placeholder}
|
||||
options={allOptions}
|
||||
{error}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.control {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.control:not(.disabled) :global(.spectrum-Textfield-input) {
|
||||
padding-right: 40px;
|
||||
}
|
||||
</style>
|
|
@ -18,6 +18,7 @@
|
|||
export let options
|
||||
export let allowJS = true
|
||||
export let appendBindingsAsOptions = true
|
||||
export let error
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let bindingDrawer
|
||||
|
@ -59,8 +60,10 @@
|
|||
value={isJS ? "(JavaScript function)" : readableValue}
|
||||
on:type={e => onChange(e.detail, false)}
|
||||
on:pick={e => onChange(e.detail, true)}
|
||||
on:blur={() => dispatch("blur")}
|
||||
{placeholder}
|
||||
options={allOptions}
|
||||
{error}
|
||||
/>
|
||||
{#if !disabled}
|
||||
<div
|
||||
|
@ -72,6 +75,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Drawer bind:this={bindingDrawer} {title}>
|
||||
<svelte:fragment slot="description">
|
||||
Add the objects on the left to enrich your text.
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { lowercase } from "helpers"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -30,6 +31,7 @@
|
|||
export let tooltip
|
||||
export let menuItems
|
||||
export let showMenu = false
|
||||
export let bindings = []
|
||||
|
||||
let fields = Object.entries(object || {}).map(([name, value]) => ({
|
||||
name,
|
||||
|
@ -108,6 +110,16 @@
|
|||
/>
|
||||
{#if options}
|
||||
<Select bind:value={field.value} on:change={changed} {options} />
|
||||
{:else if bindings && bindings.length}
|
||||
<DrawerBindableInput
|
||||
{bindings}
|
||||
placeholder="Value"
|
||||
on:change={e => (field.value = e.detail)}
|
||||
disabled={readOnly}
|
||||
value={field.value}
|
||||
allowJS={false}
|
||||
fillWidth={true}
|
||||
/>
|
||||
{:else}
|
||||
<Input
|
||||
placeholder={valuePlaceholder}
|
||||
|
|
|
@ -57,7 +57,8 @@
|
|||
placeholder="Default"
|
||||
thin
|
||||
disabled={bindable}
|
||||
bind:value={binding.default}
|
||||
on:change={evt => onBindingChange(binding.name, evt.detail)}
|
||||
value={runtimeToReadableBinding(bindings, binding.default)}
|
||||
/>
|
||||
{#if bindable}
|
||||
<DrawerBindableInput
|
||||
|
|
|
@ -12,4 +12,6 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
{#key $params.selectedDatasource}
|
||||
<slot />
|
||||
{/key}
|
||||
|
|
|
@ -40,13 +40,39 @@
|
|||
import { cloneDeep } from "lodash/fp"
|
||||
import { RawRestBodyTypes } from "constants/backend"
|
||||
|
||||
import {
|
||||
getRestBindings,
|
||||
toBindingsArray,
|
||||
runtimeToReadableBinding,
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableMap,
|
||||
readableToRuntimeMap,
|
||||
} from "builderStore/dataBinding"
|
||||
|
||||
let query, datasource
|
||||
let breakQs = {},
|
||||
bindings = {}
|
||||
requestBindings = {}
|
||||
let saveId, url
|
||||
let response, schema, enabledHeaders
|
||||
let authConfigId
|
||||
let dynamicVariables, addVariableModal, varBinding
|
||||
let restBindings = getRestBindings()
|
||||
|
||||
$: staticVariables = datasource?.config?.staticVariables || {}
|
||||
|
||||
$: customRequestBindings = toBindingsArray(requestBindings, "Binding")
|
||||
$: dynamicRequestBindings = toBindingsArray(dynamicVariables, "Dynamic")
|
||||
$: dataSourceStaticBindings = toBindingsArray(
|
||||
staticVariables,
|
||||
"Datasource.Static"
|
||||
)
|
||||
|
||||
$: mergedBindings = [
|
||||
...restBindings,
|
||||
...customRequestBindings,
|
||||
...dynamicRequestBindings,
|
||||
...dataSourceStaticBindings,
|
||||
]
|
||||
|
||||
$: datasourceType = datasource?.source
|
||||
$: integrationInfo = $integrations[datasourceType]
|
||||
|
@ -63,8 +89,10 @@
|
|||
Object.keys(schema || {}).length !== 0 ||
|
||||
Object.keys(query?.schema || {}).length !== 0
|
||||
|
||||
$: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs)
|
||||
|
||||
function getSelectedQuery() {
|
||||
return cloneDeep(
|
||||
const cloneQuery = cloneDeep(
|
||||
$queries.list.find(q => q._id === $queries.selected) || {
|
||||
datasourceId: $params.selectedDatasource,
|
||||
parameters: [],
|
||||
|
@ -76,6 +104,7 @@
|
|||
queryVerb: "read",
|
||||
}
|
||||
)
|
||||
return cloneQuery
|
||||
}
|
||||
|
||||
function checkQueryName(inputUrl = null) {
|
||||
|
@ -89,7 +118,9 @@
|
|||
if (!base) {
|
||||
return base
|
||||
}
|
||||
const qs = restUtils.buildQueryString(qsObj)
|
||||
const qs = restUtils.buildQueryString(
|
||||
runtimeToReadableMap(mergedBindings, qsObj)
|
||||
)
|
||||
let newUrl = base
|
||||
if (base.includes("?")) {
|
||||
newUrl = base.split("?")[0]
|
||||
|
@ -98,14 +129,21 @@
|
|||
}
|
||||
|
||||
function buildQuery() {
|
||||
const newQuery = { ...query }
|
||||
const queryString = restUtils.buildQueryString(breakQs)
|
||||
const newQuery = cloneDeep(query)
|
||||
const queryString = restUtils.buildQueryString(runtimeUrlQueries)
|
||||
|
||||
newQuery.parameters = restUtils.keyValueToQueryParameters(requestBindings)
|
||||
newQuery.fields.requestBody =
|
||||
typeof newQuery.fields.requestBody === "object"
|
||||
? readableToRuntimeMap(mergedBindings, newQuery.fields.requestBody)
|
||||
: readableToRuntimeBinding(mergedBindings, newQuery.fields.requestBody)
|
||||
|
||||
newQuery.fields.path = url.split("?")[0]
|
||||
newQuery.fields.queryString = queryString
|
||||
newQuery.fields.authConfigId = authConfigId
|
||||
newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders)
|
||||
newQuery.schema = restUtils.fieldsToSchema(schema)
|
||||
newQuery.parameters = restUtils.keyValueToQueryParameters(bindings)
|
||||
|
||||
return newQuery
|
||||
}
|
||||
|
||||
|
@ -120,6 +158,13 @@
|
|||
datasource.config.dynamicVariables = rebuildVariables(saveId)
|
||||
datasource = await datasources.save(datasource)
|
||||
}
|
||||
prettifyQueryRequestBody(
|
||||
query,
|
||||
requestBindings,
|
||||
dynamicVariables,
|
||||
staticVariables,
|
||||
restBindings
|
||||
)
|
||||
} catch (err) {
|
||||
notifications.error(`Error saving query`)
|
||||
}
|
||||
|
@ -127,7 +172,7 @@
|
|||
|
||||
async function runQuery() {
|
||||
try {
|
||||
response = await queries.preview(buildQuery(query))
|
||||
response = await queries.preview(buildQuery())
|
||||
if (response.rows.length === 0) {
|
||||
notifications.info("Request did not return any data")
|
||||
} else {
|
||||
|
@ -236,6 +281,36 @@
|
|||
}
|
||||
}
|
||||
|
||||
const prettifyQueryRequestBody = (
|
||||
query,
|
||||
requestBindings,
|
||||
dynamicVariables,
|
||||
staticVariables,
|
||||
restBindings
|
||||
) => {
|
||||
let customRequestBindings = toBindingsArray(requestBindings, "Binding")
|
||||
let dynamicRequestBindings = toBindingsArray(dynamicVariables, "Dynamic")
|
||||
let dataSourceStaticBindings = toBindingsArray(
|
||||
staticVariables,
|
||||
"Datasource.Static"
|
||||
)
|
||||
|
||||
const prettyBindings = [
|
||||
...restBindings,
|
||||
...customRequestBindings,
|
||||
...dynamicRequestBindings,
|
||||
...dataSourceStaticBindings,
|
||||
]
|
||||
|
||||
//Parse the body here as now all bindings have been updated.
|
||||
if (query?.fields?.requestBody) {
|
||||
query.fields.requestBody =
|
||||
typeof query.fields.requestBody === "object"
|
||||
? runtimeToReadableMap(prettyBindings, query.fields.requestBody)
|
||||
: runtimeToReadableBinding(prettyBindings, query.fields.requestBody)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
query = getSelectedQuery()
|
||||
|
||||
|
@ -250,6 +325,8 @@
|
|||
const datasourceUrl = datasource?.config.url
|
||||
const qs = query?.fields.queryString
|
||||
breakQs = restUtils.breakQueryString(qs)
|
||||
breakQs = runtimeToReadableMap(mergedBindings, breakQs)
|
||||
|
||||
const path = query.fields.path
|
||||
if (
|
||||
datasourceUrl &&
|
||||
|
@ -260,7 +337,7 @@
|
|||
}
|
||||
url = buildUrl(query.fields.path, breakQs)
|
||||
schema = restUtils.schemaToFields(query.schema)
|
||||
bindings = restUtils.queryParametersToKeyValue(query.parameters)
|
||||
requestBindings = restUtils.queryParametersToKeyValue(query.parameters)
|
||||
authConfigId = getAuthConfigId()
|
||||
if (!query.fields.disabledHeaders) {
|
||||
query.fields.disabledHeaders = {}
|
||||
|
@ -291,6 +368,14 @@
|
|||
query.fields.pagination = {}
|
||||
}
|
||||
dynamicVariables = getDynamicVariables(datasource, query._id)
|
||||
|
||||
prettifyQueryRequestBody(
|
||||
query,
|
||||
requestBindings,
|
||||
dynamicVariables,
|
||||
staticVariables,
|
||||
restBindings
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -344,16 +429,26 @@
|
|||
<Tabs selected="Bindings" quiet noPadding noHorizPadding onTop>
|
||||
<Tab title="Bindings">
|
||||
<KeyValueBuilder
|
||||
bind:object={bindings}
|
||||
bind:object={requestBindings}
|
||||
tooltip="Set the name of the binding which can be used in Handlebars statements throughout your query"
|
||||
name="binding"
|
||||
headings
|
||||
keyPlaceholder="Binding name"
|
||||
valuePlaceholder="Default"
|
||||
bindings={[
|
||||
...restBindings,
|
||||
...dynamicRequestBindings,
|
||||
...dataSourceStaticBindings,
|
||||
]}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab title="Params">
|
||||
<KeyValueBuilder bind:object={breakQs} name="param" headings />
|
||||
<KeyValueBuilder
|
||||
bind:object={breakQs}
|
||||
name="param"
|
||||
headings
|
||||
bindings={mergedBindings}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab title="Headers">
|
||||
<KeyValueBuilder
|
||||
|
@ -362,6 +457,7 @@
|
|||
toggle
|
||||
name="header"
|
||||
headings
|
||||
bindings={mergedBindings}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab title="Body">
|
||||
|
|
|
@ -15,6 +15,15 @@ module FetchMock {
|
|||
},
|
||||
},
|
||||
json: async () => {
|
||||
//x-www-form-encoded body is a URLSearchParams
|
||||
//The call to stringify it leaves it blank
|
||||
if (body?.opts?.body instanceof URLSearchParams) {
|
||||
const paramArray = Array.from(body.opts.body.entries())
|
||||
body.opts.body = paramArray.reduce((acc: any, pair: any) => {
|
||||
acc[pair[0]] = pair[1]
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
return body
|
||||
},
|
||||
}
|
||||
|
|
|
@ -129,6 +129,9 @@ export async function preview(ctx: any) {
|
|||
parameters,
|
||||
transformer,
|
||||
queryId,
|
||||
ctx: {
|
||||
user: ctx.user,
|
||||
},
|
||||
})
|
||||
|
||||
const { rows, keys, info, extra } = await quotas.addQuery(runFn)
|
||||
|
@ -172,6 +175,9 @@ async function execute(ctx: any, opts = { rowsOnly: false }) {
|
|||
parameters: enrichedParameters,
|
||||
transformer: query.transformer,
|
||||
queryId: ctx.params.queryId,
|
||||
ctx: {
|
||||
user: ctx.user,
|
||||
},
|
||||
})
|
||||
|
||||
const { rows, pagination, extra } = await quotas.addQuery(runFn)
|
||||
|
|
|
@ -346,4 +346,170 @@ describe("/queries", () => {
|
|||
expect(contents).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Current User Request Mapping", () => {
|
||||
|
||||
async function previewGet(datasource, fields, params) {
|
||||
return config.previewQuery(request, config, datasource, fields, params)
|
||||
}
|
||||
|
||||
async function previewPost(datasource, fields, params) {
|
||||
return config.previewQuery(request, config, datasource, fields, params, "create")
|
||||
}
|
||||
|
||||
it("should parse global and query level header mappings", async () => {
|
||||
const userDetails = config.getUserDetails()
|
||||
|
||||
const datasource = await config.restDatasource({
|
||||
defaultHeaders: {
|
||||
"test": "headerVal",
|
||||
"emailHdr": "{{[user].[email]}}"
|
||||
}
|
||||
})
|
||||
const res = await previewGet(datasource, {
|
||||
path: "www.google.com",
|
||||
queryString: "email={{[user].[email]}}",
|
||||
headers: {
|
||||
queryHdr : "{{[user].[firstName]}}",
|
||||
secondHdr : "1234"
|
||||
}
|
||||
})
|
||||
|
||||
const parsedRequest = JSON.parse(res.body.extra.raw)
|
||||
expect(parsedRequest.opts.headers).toEqual({
|
||||
"test": "headerVal",
|
||||
"emailHdr": userDetails.email,
|
||||
"queryHdr": userDetails.firstName,
|
||||
"secondHdr" : "1234"
|
||||
})
|
||||
expect(res.body.rows[0].url).toEqual("http://www.google.com?email=" + userDetails.email)
|
||||
})
|
||||
|
||||
it("should bind the current user to query parameters", async () => {
|
||||
const userDetails = config.getUserDetails()
|
||||
|
||||
const datasource = await config.restDatasource()
|
||||
|
||||
const res = await previewGet(datasource, {
|
||||
path: "www.google.com",
|
||||
queryString: "test={{myEmail}}&testName={{myName}}&testParam={{testParam}}",
|
||||
}, {
|
||||
"myEmail" : "{{[user].[email]}}",
|
||||
"myName" : "{{[user].[firstName]}}",
|
||||
"testParam" : "1234"
|
||||
})
|
||||
|
||||
expect(res.body.rows[0].url).toEqual("http://www.google.com?test=" + userDetails.email +
|
||||
"&testName=" + userDetails.firstName + "&testParam=1234")
|
||||
})
|
||||
|
||||
it("should bind the current user the request body - plain text", async () => {
|
||||
const userDetails = config.getUserDetails()
|
||||
const datasource = await config.restDatasource()
|
||||
|
||||
const res = await previewPost(datasource, {
|
||||
path: "www.google.com",
|
||||
queryString: "testParam={{testParam}}",
|
||||
requestBody: "This is plain text and this is my email: {{[user].[email]}}. This is a test param: {{testParam}}",
|
||||
bodyType: "text"
|
||||
}, {
|
||||
"testParam" : "1234"
|
||||
})
|
||||
|
||||
const parsedRequest = JSON.parse(res.body.extra.raw)
|
||||
expect(parsedRequest.opts.body).toEqual(`This is plain text and this is my email: ${userDetails.email}. This is a test param: 1234`)
|
||||
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234")
|
||||
})
|
||||
|
||||
it("should bind the current user the request body - json", async () => {
|
||||
const userDetails = config.getUserDetails()
|
||||
const datasource = await config.restDatasource()
|
||||
|
||||
const res = await previewPost(datasource, {
|
||||
path: "www.google.com",
|
||||
queryString: "testParam={{testParam}}",
|
||||
requestBody: "{\"email\":\"{{[user].[email]}}\",\"queryCode\":{{testParam}},\"userRef\":\"{{userRef}}\"}",
|
||||
bodyType: "json"
|
||||
}, {
|
||||
"testParam" : "1234",
|
||||
"userRef" : "{{[user].[firstName]}}"
|
||||
})
|
||||
|
||||
const parsedRequest = JSON.parse(res.body.extra.raw)
|
||||
const test = `{"email":"${userDetails.email}","queryCode":1234,"userRef":"${userDetails.firstName}"}`
|
||||
expect(parsedRequest.opts.body).toEqual(test)
|
||||
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234")
|
||||
})
|
||||
|
||||
it("should bind the current user the request body - xml", async () => {
|
||||
const userDetails = config.getUserDetails()
|
||||
const datasource = await config.restDatasource()
|
||||
|
||||
const res = await previewPost(datasource, {
|
||||
path: "www.google.com",
|
||||
queryString: "testParam={{testParam}}",
|
||||
requestBody: "<note> <email>{{[user].[email]}}</email> <code>{{testParam}}</code> " +
|
||||
"<ref>{{userId}}</ref> <somestring>testing</somestring> </note>",
|
||||
bodyType: "xml"
|
||||
}, {
|
||||
"testParam" : "1234",
|
||||
"userId" : "{{[user].[firstName]}}"
|
||||
})
|
||||
|
||||
const parsedRequest = JSON.parse(res.body.extra.raw)
|
||||
const test = `<note> <email>${userDetails.email}</email> <code>1234</code> <ref>${userDetails.firstName}</ref> <somestring>testing</somestring> </note>`
|
||||
|
||||
expect(parsedRequest.opts.body).toEqual(test)
|
||||
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234")
|
||||
})
|
||||
|
||||
it("should bind the current user the request body - form-data", async () => {
|
||||
const userDetails = config.getUserDetails()
|
||||
const datasource = await config.restDatasource()
|
||||
|
||||
const res = await previewPost(datasource, {
|
||||
path: "www.google.com",
|
||||
queryString: "testParam={{testParam}}",
|
||||
requestBody: "{\"email\":\"{{[user].[email]}}\",\"queryCode\":{{testParam}},\"userRef\":\"{{userRef}}\"}",
|
||||
bodyType: "form"
|
||||
}, {
|
||||
"testParam" : "1234",
|
||||
"userRef" : "{{[user].[firstName]}}"
|
||||
})
|
||||
|
||||
const parsedRequest = JSON.parse(res.body.extra.raw)
|
||||
|
||||
const emailData = parsedRequest.opts.body._streams[1]
|
||||
expect(emailData).toEqual(userDetails.email)
|
||||
|
||||
const queryCodeData = parsedRequest.opts.body._streams[4]
|
||||
expect(queryCodeData).toEqual("1234")
|
||||
|
||||
const userRef = parsedRequest.opts.body._streams[7]
|
||||
expect(userRef).toEqual(userDetails.firstName)
|
||||
|
||||
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234")
|
||||
})
|
||||
|
||||
it("should bind the current user the request body - encoded", async () => {
|
||||
const userDetails = config.getUserDetails()
|
||||
const datasource = await config.restDatasource()
|
||||
|
||||
const res = await previewPost(datasource, {
|
||||
path: "www.google.com",
|
||||
queryString: "testParam={{testParam}}",
|
||||
requestBody: "{\"email\":\"{{[user].[email]}}\",\"queryCode\":{{testParam}},\"userRef\":\"{{userRef}}\"}",
|
||||
bodyType: "encoded"
|
||||
}, {
|
||||
"testParam" : "1234",
|
||||
"userRef" : "{{[user].[firstName]}}"
|
||||
})
|
||||
const parsedRequest = JSON.parse(res.body.extra.raw)
|
||||
|
||||
expect(parsedRequest.opts.body.email).toEqual(userDetails.email)
|
||||
expect(parsedRequest.opts.body.queryCode).toEqual("1234")
|
||||
expect(parsedRequest.opts.body.userRef).toEqual(userDetails.firstName)
|
||||
})
|
||||
|
||||
});
|
||||
})
|
||||
|
|
|
@ -9,7 +9,9 @@ export function enrichQueryFields(
|
|||
parameters = {}
|
||||
) {
|
||||
const enrichedQuery: { [key: string]: any } = Array.isArray(fields) ? [] : {}
|
||||
|
||||
if (!fields || !parameters) {
|
||||
return enrichedQuery
|
||||
}
|
||||
// enrich the fields with dynamic parameters
|
||||
for (let key of Object.keys(fields)) {
|
||||
if (fields[key] == null) {
|
||||
|
|
|
@ -287,7 +287,7 @@ module RestModule {
|
|||
input.body = form
|
||||
break
|
||||
case BodyTypes.XML:
|
||||
if (object != null) {
|
||||
if (object != null && Object.keys(object).length) {
|
||||
string = new XmlBuilder().buildObject(object)
|
||||
}
|
||||
input.body = string
|
||||
|
|
|
@ -155,12 +155,27 @@ describe("REST Integration", () => {
|
|||
expect(output.headers["Content-Type"]).toEqual("application/json")
|
||||
})
|
||||
|
||||
it("should allow XML", () => {
|
||||
it("should allow raw XML", () => {
|
||||
const output = config.integration.addBody("xml", "<a>1</a><b>2</b>", {})
|
||||
expect(output.body.includes("<a>1</a>")).toEqual(true)
|
||||
expect(output.body.includes("<b>2</b>")).toEqual(true)
|
||||
expect(output.headers["Content-Type"]).toEqual("application/xml")
|
||||
})
|
||||
|
||||
it("should allow a valid js object and parse the contents to xml", () => {
|
||||
const output = config.integration.addBody("xml", input, {})
|
||||
expect(output.body.includes("<a>1</a>")).toEqual(true)
|
||||
expect(output.body.includes("<b>2</b>")).toEqual(true)
|
||||
expect(output.headers["Content-Type"]).toEqual("application/xml")
|
||||
})
|
||||
|
||||
it("should allow a valid json string and parse the contents to xml", () => {
|
||||
const output = config.integration.addBody("xml", JSON.stringify(input), {})
|
||||
expect(output.body.includes("<a>1</a>")).toEqual(true)
|
||||
expect(output.body.includes("<b>2</b>")).toEqual(true)
|
||||
expect(output.headers["Content-Type"]).toEqual("application/xml")
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe("response", () => {
|
||||
|
|
|
@ -93,8 +93,24 @@ describe("migrations", () => {
|
|||
await clearMigrations()
|
||||
const appId = config.prodAppId
|
||||
const roles = { [appId]: "role_12345" }
|
||||
await config.createUser(undefined, undefined, false, true, roles) // admin only
|
||||
await config.createUser(undefined, undefined, false, false, roles) // non admin non builder
|
||||
await config.createUser(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
true,
|
||||
roles
|
||||
) // admin only
|
||||
await config.createUser(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
false,
|
||||
roles
|
||||
) // non admin non builder
|
||||
await config.createTable()
|
||||
await config.createRow()
|
||||
await config.createRow()
|
||||
|
|
|
@ -28,6 +28,8 @@ const { encrypt } = require("@budibase/backend-core/encryption")
|
|||
|
||||
const GLOBAL_USER_ID = "us_uuid1"
|
||||
const EMAIL = "babs@babs.com"
|
||||
const FIRSTNAME = "Barbara"
|
||||
const LASTNAME = "Barbington"
|
||||
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
|
||||
|
||||
class TestConfiguration {
|
||||
|
@ -59,6 +61,15 @@ class TestConfiguration {
|
|||
return this.prodAppId
|
||||
}
|
||||
|
||||
getUserDetails() {
|
||||
return {
|
||||
globalId: GLOBAL_USER_ID,
|
||||
email: EMAIL,
|
||||
firstName: FIRSTNAME,
|
||||
lastName: LASTNAME,
|
||||
}
|
||||
}
|
||||
|
||||
async doInContext(appId, task) {
|
||||
if (!appId) {
|
||||
appId = this.appId
|
||||
|
@ -118,6 +129,8 @@ class TestConfiguration {
|
|||
// USER / AUTH
|
||||
async globalUser({
|
||||
id = GLOBAL_USER_ID,
|
||||
firstName = FIRSTNAME,
|
||||
lastName = LASTNAME,
|
||||
builder = true,
|
||||
admin = false,
|
||||
email = EMAIL,
|
||||
|
@ -135,6 +148,8 @@ class TestConfiguration {
|
|||
...existing,
|
||||
roles: roles || {},
|
||||
tenantId: TENANT_ID,
|
||||
firstName,
|
||||
lastName,
|
||||
}
|
||||
await createASession(id, {
|
||||
sessionId: "sessionid",
|
||||
|
@ -161,6 +176,8 @@ class TestConfiguration {
|
|||
|
||||
async createUser(
|
||||
id = null,
|
||||
firstName = FIRSTNAME,
|
||||
lastName = LASTNAME,
|
||||
email = EMAIL,
|
||||
builder = true,
|
||||
admin = false,
|
||||
|
@ -169,6 +186,8 @@ class TestConfiguration {
|
|||
const globalId = !id ? `us_${Math.random()}` : `us_${id}`
|
||||
const resp = await this.globalUser({
|
||||
id: globalId,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
builder,
|
||||
admin,
|
||||
|
@ -520,14 +539,14 @@ class TestConfiguration {
|
|||
|
||||
// QUERY
|
||||
|
||||
async previewQuery(request, config, datasource, fields) {
|
||||
async previewQuery(request, config, datasource, fields, params, verb) {
|
||||
return request
|
||||
.post(`/api/queries/preview`)
|
||||
.send({
|
||||
datasourceId: datasource._id,
|
||||
parameters: {},
|
||||
parameters: params || {},
|
||||
fields,
|
||||
queryVerb: "read",
|
||||
queryVerb: verb || "read",
|
||||
name: datasource.name,
|
||||
})
|
||||
.set(config.defaultHeaders())
|
||||
|
|
|
@ -21,6 +21,8 @@ class QueryRunner {
|
|||
this.queryId = input.queryId
|
||||
this.noRecursiveQuery = flags.noRecursiveQuery
|
||||
this.cachedVariables = []
|
||||
// Additional context items for enrichment
|
||||
this.ctx = input.ctx
|
||||
// allows the response from a query to be stored throughout this
|
||||
// execution so that if it needs to be re-used for another variable
|
||||
// it can be
|
||||
|
@ -34,16 +36,39 @@ class QueryRunner {
|
|||
if (!Integration) {
|
||||
throw "Integration type does not exist."
|
||||
}
|
||||
|
||||
if (datasource.config.authConfigs) {
|
||||
datasource.config.authConfigs = datasource.config.authConfigs.map(
|
||||
config => {
|
||||
return enrichQueryFields(config, this.ctx)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const integration = new Integration(datasource.config)
|
||||
|
||||
// pre-query, make sure datasource variables are added to parameters
|
||||
const parameters = await this.addDatasourceVariables()
|
||||
|
||||
// Enrich the parameters with the addition context items.
|
||||
// 'user' is now a reserved variable key in mapping parameters
|
||||
const enrichedParameters = enrichQueryFields(parameters, this.ctx)
|
||||
const enrichedContext = { ...enrichedParameters, ...this.ctx }
|
||||
|
||||
// Parse global headers
|
||||
if (datasource.config.defaultHeaders) {
|
||||
datasource.config.defaultHeaders = enrichQueryFields(
|
||||
datasource.config.defaultHeaders,
|
||||
enrichedContext
|
||||
)
|
||||
}
|
||||
|
||||
let query
|
||||
// handle SQL injections by interpolating the variables
|
||||
if (isSQL(datasource)) {
|
||||
query = interpolateSQL(fields, parameters, integration)
|
||||
query = interpolateSQL(fields, enrichedParameters, integration)
|
||||
} else {
|
||||
query = enrichQueryFields(fields, parameters)
|
||||
query = enrichQueryFields(fields, enrichedContext)
|
||||
}
|
||||
|
||||
// Add pagination values for REST queries
|
||||
|
@ -67,7 +92,7 @@ class QueryRunner {
|
|||
if (transformer) {
|
||||
const runner = new ScriptRunner(transformer, {
|
||||
data: rows,
|
||||
params: parameters,
|
||||
params: enrichedParameters,
|
||||
})
|
||||
rows = runner.execute()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue