From d50a62fd376b1c1f4ac99f9b549d4d9fafb0a0ff Mon Sep 17 00:00:00 2001 From: Dean Date: Tue, 14 Jun 2022 10:14:05 +0100 Subject: [PATCH 1/8] Context binding for authenticated user in REST API querys. Includes fix for REST datasource UI --- .../builder/src/builderStore/dataBinding.js | 37 +++++++++- .../rest/RestExtraConfigForm.svelte | 24 +++++- .../integration/QueryBindingBuilder.svelte | 3 +- .../[selectedDatasource]/_layout.svelte | 4 +- .../rest/[query]/index.svelte | 73 ++++++++++++++++--- .../server/src/api/controllers/query/index.ts | 3 + packages/server/src/threads/query.js | 20 ++++- 7 files changed, 147 insertions(+), 17 deletions(-) diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 8cbc629291..cddb061e24 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -49,6 +49,42 @@ export const getBindableProperties = (asset, componentId) => { ] } +/** + * Gets all rest bindable data fields + */ +export const getRestBindings = () => { + const userBindings = getUserBindings() + return [...userBindings] +} + +/** + * 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 +334,6 @@ const getUserBindings = () => { providerId: "user", }) }) - return bindings } diff --git a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/RestExtraConfigForm.svelte b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/RestExtraConfigForm.svelte index 2c8b699849..e049e0cbdf 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/RestExtraConfigForm.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/RestExtraConfigForm.svelte @@ -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 + } @@ -30,8 +50,8 @@ onDefaultHeaderUpdate(evt.detail)} noAddButton />
diff --git a/packages/builder/src/components/integration/QueryBindingBuilder.svelte b/packages/builder/src/components/integration/QueryBindingBuilder.svelte index 9bcf9d36f8..7f6bd5ffd5 100644 --- a/packages/builder/src/components/integration/QueryBindingBuilder.svelte +++ b/packages/builder/src/components/integration/QueryBindingBuilder.svelte @@ -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} - +{#key $params.selectedDatasource} + +{/key} diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte index 2baa6aab41..ab01d4224e 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte @@ -40,13 +40,22 @@ import { cloneDeep } from "lodash/fp" import { RawRestBodyTypes } from "constants/backend" + import { + getRestBindings, + readableToRuntimeBinding, + runtimeToReadableBinding, + 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() $: datasourceType = datasource?.source $: integrationInfo = $integrations[datasourceType] @@ -63,8 +72,10 @@ Object.keys(schema || {}).length !== 0 || Object.keys(query?.schema || {}).length !== 0 + $: runtimeUrlQueries = readableToRuntimeMap(restBindings, breakQs) + function getSelectedQuery() { - return cloneDeep( + const cloneQuery = cloneDeep( $queries.list.find(q => q._id === $queries.selected) || { datasourceId: $params.selectedDatasource, parameters: [], @@ -76,6 +87,30 @@ queryVerb: "read", } ) + + if (cloneQuery?.fields?.headers) { + cloneQuery.fields.headers = runtimeToReadableMap( + restBindings, + cloneQuery.fields.headers + ) + } + + if (cloneQuery?.fields?.requestBody) { + cloneQuery.fields.requestBody = runtimeToReadableBinding( + restBindings, + cloneQuery.fields.requestBody + ) + } + + if (cloneQuery?.parameters) { + const flatParams = restUtils.queryParametersToKeyValue( + cloneQuery.parameters + ) + const updatedParams = runtimeToReadableMap(restBindings, flatParams) + cloneQuery.parameters = restUtils.keyValueToQueryParameters(updatedParams) + } + + return cloneQuery } function checkQueryName(inputUrl = null) { @@ -89,7 +124,9 @@ if (!base) { return base } - const qs = restUtils.buildQueryString(qsObj) + const qs = restUtils.buildQueryString( + runtimeToReadableMap(restBindings, qsObj) + ) let newUrl = base if (base.includes("?")) { newUrl = base.split("?")[0] @@ -98,14 +135,30 @@ } function buildQuery() { - const newQuery = { ...query } - const queryString = restUtils.buildQueryString(breakQs) + const newQuery = cloneDeep(query) + const queryString = restUtils.buildQueryString(runtimeUrlQueries) + newQuery.fields.headers = readableToRuntimeMap( + restBindings, + newQuery.fields.headers + ) + newQuery.fields.requestBody = readableToRuntimeBinding( + restBindings, + 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) + + const parsedRequestBindings = readableToRuntimeMap( + restBindings, + requestBindings + ) + newQuery.parameters = restUtils.keyValueToQueryParameters( + parsedRequestBindings + ) + return newQuery } @@ -127,7 +180,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 { @@ -250,6 +303,8 @@ const datasourceUrl = datasource?.config.url const qs = query?.fields.queryString breakQs = restUtils.breakQueryString(qs) + breakQs = runtimeToReadableMap(restBindings, breakQs) + const path = query.fields.path if ( datasourceUrl && @@ -260,7 +315,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 = {} @@ -344,7 +399,7 @@ Date: Wed, 15 Jun 2022 10:09:47 +0100 Subject: [PATCH 2/8] Fix to ignore global rest query headers when they are not configured. --- packages/server/src/integrations/queries/sql.ts | 4 +++- packages/server/src/threads/query.js | 10 ++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/server/src/integrations/queries/sql.ts b/packages/server/src/integrations/queries/sql.ts index cf71f2ee2a..271a414d44 100644 --- a/packages/server/src/integrations/queries/sql.ts +++ b/packages/server/src/integrations/queries/sql.ts @@ -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) { diff --git a/packages/server/src/threads/query.js b/packages/server/src/threads/query.js index b6454e758e..36f73b154d 100644 --- a/packages/server/src/threads/query.js +++ b/packages/server/src/threads/query.js @@ -47,10 +47,12 @@ class QueryRunner { const enrichedContext = { ...enrichedParameters, ...this.ctx } // Parse global headers - datasource.config.defaultHeaders = enrichQueryFields( - datasource.config.defaultHeaders, - enrichedContext - ) + if (datasource.config.defaultHeaders) { + datasource.config.defaultHeaders = enrichQueryFields( + datasource.config.defaultHeaders, + enrichedContext + ) + } let query // handle SQL injections by interpolating the variables From 28f722cf4bd15f5fbe371985beadf3cefaa3f854 Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 17 Jun 2022 12:00:42 +0100 Subject: [PATCH 3/8] Fixes for Rest API request UI. Rest test fixes for XML API request body. Fix for raw XML api request body parsing issue. General fixes for query testing. --- .../rest/[query]/index.svelte | 20 ++- packages/server/__mocks__/node-fetch.ts | 9 + .../server/src/api/routes/tests/query.spec.js | 166 ++++++++++++++++++ packages/server/src/integrations/rest.ts | 2 +- .../src/integrations/tests/rest.spec.js | 17 +- .../server/src/migrations/tests/index.spec.ts | 20 ++- .../src/tests/utilities/TestConfiguration.js | 25 ++- 7 files changed, 244 insertions(+), 15 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte index ab01d4224e..f236eaaf33 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte @@ -96,10 +96,13 @@ } if (cloneQuery?.fields?.requestBody) { - cloneQuery.fields.requestBody = runtimeToReadableBinding( - restBindings, - cloneQuery.fields.requestBody - ) + cloneQuery.fields.requestBody = + typeof cloneQuery.fields.requestBody === "object" + ? runtimeToReadableMap(restBindings, cloneQuery.fields.requestBody) + : runtimeToReadableBinding( + restBindings, + cloneQuery.fields.requestBody + ) } if (cloneQuery?.parameters) { @@ -141,10 +144,11 @@ restBindings, newQuery.fields.headers ) - newQuery.fields.requestBody = readableToRuntimeBinding( - restBindings, - newQuery.fields.requestBody - ) + newQuery.fields.requestBody = + typeof newQuery.fields.requestBody === "object" + ? readableToRuntimeMap(restBindings, newQuery.fields.requestBody) + : readableToRuntimeBinding(restBindings, newQuery.fields.requestBody) + newQuery.fields.path = url.split("?")[0] newQuery.fields.queryString = queryString newQuery.fields.authConfigId = authConfigId diff --git a/packages/server/__mocks__/node-fetch.ts b/packages/server/__mocks__/node-fetch.ts index 350680691b..1a7015fa52 100644 --- a/packages/server/__mocks__/node-fetch.ts +++ b/packages/server/__mocks__/node-fetch.ts @@ -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 }, } diff --git a/packages/server/src/api/routes/tests/query.spec.js b/packages/server/src/api/routes/tests/query.spec.js index 5dacda3505..273bdb9993 100644 --- a/packages/server/src/api/routes/tests/query.spec.js +++ b/packages/server/src/api/routes/tests/query.spec.js @@ -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: " {{[user].[email]}} {{testParam}} " + + "{{userId}} testing ", + bodyType: "xml" + }, { + "testParam" : "1234", + "userId" : "{{[user].[firstName]}}" + }) + + const parsedRequest = JSON.parse(res.body.extra.raw) + const test = ` ${userDetails.email} 1234 ${userDetails.firstName} testing ` + + 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) + }) + + }); }) diff --git a/packages/server/src/integrations/rest.ts b/packages/server/src/integrations/rest.ts index 6174613fc8..e9c5f59fad 100644 --- a/packages/server/src/integrations/rest.ts +++ b/packages/server/src/integrations/rest.ts @@ -286,7 +286,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 diff --git a/packages/server/src/integrations/tests/rest.spec.js b/packages/server/src/integrations/tests/rest.spec.js index 8f3c7f7f58..0bb1e3a75d 100644 --- a/packages/server/src/integrations/tests/rest.spec.js +++ b/packages/server/src/integrations/tests/rest.spec.js @@ -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", "12", {}) + expect(output.body.includes("1")).toEqual(true) + expect(output.body.includes("2")).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("1")).toEqual(true) expect(output.body.includes("2")).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("1")).toEqual(true) + expect(output.body.includes("2")).toEqual(true) + expect(output.headers["Content-Type"]).toEqual("application/xml") + }) + }) describe("response", () => { diff --git a/packages/server/src/migrations/tests/index.spec.ts b/packages/server/src/migrations/tests/index.spec.ts index ca30fbca06..84400f3df3 100644 --- a/packages/server/src/migrations/tests/index.spec.ts +++ b/packages/server/src/migrations/tests/index.spec.ts @@ -91,8 +91,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() diff --git a/packages/server/src/tests/utilities/TestConfiguration.js b/packages/server/src/tests/utilities/TestConfiguration.js index fc4d302c63..baa4ec13b8 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.js +++ b/packages/server/src/tests/utilities/TestConfiguration.js @@ -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()) From fb74b2299369a9d9f317f4d3bb6c1ab0d350506c Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 20 Jun 2022 11:11:15 +0100 Subject: [PATCH 4/8] Added missing request context for user bindings --- packages/server/src/api/controllers/query/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/server/src/api/controllers/query/index.ts b/packages/server/src/api/controllers/query/index.ts index 21c9eb2a4c..19a547b401 100644 --- a/packages/server/src/api/controllers/query/index.ts +++ b/packages/server/src/api/controllers/query/index.ts @@ -175,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) From 31b51e1ecfa022ee9dfa61f0ee00c60c49e355d1 Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 1 Jul 2022 17:27:24 +0100 Subject: [PATCH 5/8] Fixes for datasource authentication parsing. Mapping UX updates --- packages/bbui/src/Form/Combobox.svelte | 1 + packages/bbui/src/Form/Core/Combobox.svelte | 5 +- .../builder/src/builderStore/dataBinding.js | 51 ++++++++++- .../rest/auth/RestAuthenticationModal.svelte | 22 ++++- .../bindings/DrawerBindableCombobox.svelte | 38 ++++---- .../integration/KeyValueBuilder.svelte | 12 +++ .../rest/[query]/index.svelte | 89 +++++++++++-------- packages/server/src/threads/query.js | 6 ++ 8 files changed, 165 insertions(+), 59 deletions(-) diff --git a/packages/bbui/src/Form/Combobox.svelte b/packages/bbui/src/Form/Combobox.svelte index 83927b05db..343af559cb 100644 --- a/packages/bbui/src/Form/Combobox.svelte +++ b/packages/bbui/src/Form/Combobox.svelte @@ -40,5 +40,6 @@ on:change={onChange} on:pick on:type + on:blur /> diff --git a/packages/bbui/src/Form/Core/Combobox.svelte b/packages/bbui/src/Form/Core/Combobox.svelte index 2a4bac4a2c..2835b3cd40 100644 --- a/packages/bbui/src/Form/Core/Combobox.svelte +++ b/packages/bbui/src/Form/Core/Combobox.svelte @@ -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 || ""} diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index cddb061e24..05d17ecf38 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -54,7 +54,56 @@ export const getBindableProperties = (asset, componentId) => { */ export const getRestBindings = () => { const userBindings = getUserBindings() - return [...userBindings] + 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).map(binding => { + return { + type: "context", + runtimeBinding: binding, + readableBinding: `${prefix}.${binding}`, + } + }) } /** diff --git a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/auth/RestAuthenticationModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/auth/RestAuthenticationModal.svelte index 7f0cc7357b..d80b60e96c 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/auth/RestAuthenticationModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/auth/RestAuthenticationModal.svelte @@ -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 DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte" + import { getAuthBindings } from "builderStore/dataBinding" export let configs export let currentConfig @@ -203,11 +205,23 @@ /> {/if} {#if form.type === AUTH_TYPES.BEARER} - (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} diff --git a/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte b/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte index 44f88e841a..2d235c8026 100644 --- a/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte +++ b/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte @@ -18,6 +18,8 @@ export let options export let allowJS = true export let appendBindingsAsOptions = true + export let drawerEnabled = false + export let error const dispatch = createEventDispatcher() let bindingDrawer @@ -59,10 +61,12 @@ 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} + {#if !disabled && drawerEnabled}
{/if}
- - - Add the objects on the left to enrich your text. - - - (tempValue = event.detail)} - {bindings} - {allowJS} - /> - +{#if !drawerEnabled} + + + Add the objects on the left to enrich your text. + + + (tempValue = event.detail)} + {bindings} + {allowJS} + /> + +{/if} diff --git a/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte b/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte index 2d235c8026..9033844dd0 100644 --- a/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte +++ b/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte @@ -18,7 +18,6 @@ export let options export let allowJS = true export let appendBindingsAsOptions = true - export let drawerEnabled = false export let error const dispatch = createEventDispatcher() @@ -66,7 +65,7 @@ options={allOptions} {error} /> - {#if !disabled && drawerEnabled} + {#if !disabled}
{/if}
-{#if !drawerEnabled} - - - Add the objects on the left to enrich your text. - - - (tempValue = event.detail)} - {bindings} - {allowJS} - /> - -{/if} + + + + Add the objects on the left to enrich your text. + + + (tempValue = event.detail)} + {bindings} + {allowJS} + /> +