Merge pull request #3932 from Budibase/feature/query-variables

Fixes for new rest datasource
This commit is contained in:
Rory Powell 2022-01-10 12:08:47 +00:00 committed by GitHub
commit 10d9928679
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 79 additions and 39 deletions

View File

@ -10,6 +10,8 @@
export let noHorizPadding = false export let noHorizPadding = false
export let quiet = false export let quiet = false
export let emphasized = false export let emphasized = false
// overlay content from the tab bar onto tabs e.g. for a dropdown
export let onTop = false
let thisSelected = undefined let thisSelected = undefined
@ -78,6 +80,7 @@
'spectrum-Tabs--quiet'} spectrum-Tabs--{vertical 'spectrum-Tabs--quiet'} spectrum-Tabs--{vertical
? 'vertical' ? 'vertical'
: 'horizontal'}" : 'horizontal'}"
class:onTop
> >
<slot /> <slot />
{#if $tab.info} {#if $tab.info}
@ -98,7 +101,9 @@
.quiet { .quiet {
border-bottom: none !important; border-bottom: none !important;
} }
.onTop {
z-index: 20;
}
.spectrum-Tabs { .spectrum-Tabs {
padding-left: var(--spacing-xl); padding-left: var(--spacing-xl);
padding-right: var(--spacing-xl); padding-right: var(--spacing-xl);

View File

@ -137,7 +137,7 @@
selected={$queries.selected === query._id} selected={$queries.selected === query._id}
on:click={() => onClickQuery(query)} on:click={() => onClickQuery(query)}
> >
<EditQueryPopover {query} /> <EditQueryPopover {query} {onClickQuery} />
</NavItem> </NavItem>
{/each} {/each}
{/if} {/if}

View File

@ -58,7 +58,7 @@
/> />
{/if} {/if}
<div> <div>
<ActionButton on:click={() => openConfigModal()} con="Add" <ActionButton on:click={() => openConfigModal()} icon="Add"
>Add authentication</ActionButton >Add authentication</ActionButton
> >
</div> </div>

View File

@ -5,22 +5,29 @@
import { datasources, queries } from "stores/backend" import { datasources, queries } from "stores/backend"
export let query export let query
export let onClickQuery
let confirmDeleteDialog let confirmDeleteDialog
async function deleteQuery() { async function deleteQuery() {
const wasSelectedQuery = $queries.selected const wasSelectedQuery = $queries.selected
const selectedDatasource = $datasources.selected // need to calculate this before the query is deleted
const navigateToDatasource = wasSelectedQuery === query._id
await queries.delete(query) await queries.delete(query)
if (wasSelectedQuery === query._id) { await datasources.fetch()
$goto(`./datasource/${selectedDatasource}`)
if (navigateToDatasource) {
await datasources.select(query.datasourceId)
$goto(`./datasource/${query.datasourceId}`)
} }
notifications.success("Query deleted") notifications.success("Query deleted")
} }
async function duplicateQuery() { async function duplicateQuery() {
try { try {
await queries.duplicate(query) const newQuery = await queries.duplicate(query)
onClickQuery(newQuery)
} catch (e) { } catch (e) {
notifications.error(e.message) notifications.error(e.message)
} }

View File

@ -232,8 +232,12 @@
const datasourceUrl = datasource?.config.url const datasourceUrl = datasource?.config.url
const qs = query?.fields.queryString const qs = query?.fields.queryString
breakQs = restUtils.breakQueryString(qs) breakQs = restUtils.breakQueryString(qs)
if (datasourceUrl && !query.fields.path?.startsWith(datasourceUrl)) { const path = query.fields.path
const path = query.fields.path if (
datasourceUrl &&
!path?.startsWith("http") &&
!path?.startsWith("{{") // don't substitute the datasource url when query starts with a variable e.g. the upgrade path
) {
query.fields.path = `${datasource.config.url}/${path ? path : ""}` query.fields.path = `${datasource.config.url}/${path ? path : ""}`
} }
url = buildUrl(query.fields.path, breakQs) url = buildUrl(query.fields.path, breakQs)
@ -306,7 +310,7 @@
</div> </div>
<Button cta disabled={!url} on:click={runQuery}>Send</Button> <Button cta disabled={!url} on:click={runQuery}>Send</Button>
</div> </div>
<Tabs selected="Bindings" quiet noPadding noHorizPadding> <Tabs selected="Bindings" quiet noPadding noHorizPadding onTop>
<Tab title="Bindings"> <Tab title="Bindings">
<KeyValueBuilder <KeyValueBuilder
bind:object={bindings} bind:object={bindings}
@ -450,7 +454,7 @@
<Layout noPadding gap="S"> <Layout noPadding gap="S">
<Body size="S"> <Body size="S">
Create dynamic variables based on response body or headers Create dynamic variables based on response body or headers
from other queries. from this query.
</Body> </Body>
<KeyValueBuilder <KeyValueBuilder
bind:object={dynamicVariables} bind:object={dynamicVariables}

View File

@ -134,7 +134,7 @@ export function createQueriesStore() {
list.map(q => q.name) list.map(q => q.name)
) )
actions.save(datasourceId, newQuery) return actions.save(datasourceId, newQuery)
}, },
} }

View File

@ -1,7 +1,7 @@
import { Query, QueryParameter } from "../../../../../../definitions/datasource" import { Query, QueryParameter } from "../../../../../../definitions/datasource"
import { URL } from "url"
export interface ImportInfo { export interface ImportInfo {
url: string
name: string name: string
} }
@ -23,6 +23,7 @@ export abstract class ImportSource {
name: string, name: string,
method: string, method: string,
path: string, path: string,
url: URL,
queryString: string, queryString: string,
headers: object = {}, headers: object = {},
parameters: QueryParameter[] = [], parameters: QueryParameter[] = [],
@ -33,6 +34,7 @@ export abstract class ImportSource {
const transformer = "return data" const transformer = "return data"
const schema = {} const schema = {}
path = this.processPath(path) path = this.processPath(path)
path = `${url.origin}/${path}`
queryString = this.processQuery(queryString) queryString = this.processQuery(queryString)
const requestBody = JSON.stringify(body, null, 2) const requestBody = JSON.stringify(body, null, 2)

View File

@ -60,16 +60,19 @@ export class Curl extends ImportSource {
return true return true
} }
getUrl = (): URL => {
return new URL(this.curl.raw_url)
}
getInfo = async (): Promise<ImportInfo> => { getInfo = async (): Promise<ImportInfo> => {
const url = new URL(this.curl.url) const url = this.getUrl()
return { return {
url: url.origin,
name: url.hostname, name: url.hostname,
} }
} }
getQueries = async (datasourceId: string): Promise<Query[]> => { getQueries = async (datasourceId: string): Promise<Query[]> => {
const url = new URL(this.curl.raw_url) const url = this.getUrl()
const name = url.pathname const name = url.pathname
const path = url.pathname const path = url.pathname
const method = this.curl.method const method = this.curl.method
@ -87,6 +90,7 @@ export class Curl extends ImportSource {
name, name,
method, method,
path, path,
url,
queryString, queryString,
headers, headers,
[], [],

View File

@ -2,6 +2,7 @@ import { ImportInfo } from "./base"
import { Query, QueryParameter } from "../../../../../definitions/datasource" import { Query, QueryParameter } from "../../../../../definitions/datasource"
import { OpenAPIV2 } from "openapi-types" import { OpenAPIV2 } from "openapi-types"
import { OpenAPISource } from "./base/openapi" import { OpenAPISource } from "./base/openapi"
import { URL } from "url"
const parameterNotRef = ( const parameterNotRef = (
param: OpenAPIV2.Parameter | OpenAPIV2.ReferenceObject param: OpenAPIV2.Parameter | OpenAPIV2.ReferenceObject
@ -55,20 +56,22 @@ export class OpenAPI2 extends OpenAPISource {
} }
} }
getInfo = async (): Promise<ImportInfo> => { getUrl = (): URL => {
const scheme = this.document.schemes?.includes("https") ? "https" : "http" const scheme = this.document.schemes?.includes("https") ? "https" : "http"
const basePath = this.document.basePath || "" const basePath = this.document.basePath || ""
const host = this.document.host || "<host>" const host = this.document.host || "<host>"
const url = `${scheme}://${host}${basePath}` return new URL(`${scheme}://${host}${basePath}`)
const name = this.document.info.title || "Swagger Import" }
getInfo = async (): Promise<ImportInfo> => {
const name = this.document.info.title || "Swagger Import"
return { return {
url: url, name,
name: name,
} }
} }
getQueries = async (datasourceId: string): Promise<Query[]> => { getQueries = async (datasourceId: string): Promise<Query[]> => {
const url = this.getUrl()
const queries = [] const queries = []
for (let [path, pathItem] of Object.entries(this.document.paths)) { for (let [path, pathItem] of Object.entries(this.document.paths)) {
@ -145,6 +148,7 @@ export class OpenAPI2 extends OpenAPISource {
name, name,
methodName, methodName,
path, path,
url,
queryString, queryString,
headers, headers,
parameters, parameters,

View File

@ -35,7 +35,6 @@ describe("Curl Import", () => {
it("returns import info", async () => { it("returns import info", async () => {
await init("get") await init("get")
const info = await curl.getInfo() const info = await curl.getInfo()
expect(info.url).toBe("http://example.com")
expect(info.name).toBe("example.com") expect(info.name).toBe("example.com")
}) })
@ -67,8 +66,8 @@ describe("Curl Import", () => {
} }
it("populates path", async () => { it("populates path", async () => {
await testPath("get", "") await testPath("get", "http://example.com/")
await testPath("path", "paths/abc") await testPath("path", "http://example.com/paths/abc")
}) })
const testHeaders = async (file, headers) => { const testHeaders = async (file, headers) => {

View File

@ -41,7 +41,6 @@ describe("OpenAPI2 Import", () => {
const testImportInfo = async (file, extension) => { const testImportInfo = async (file, extension) => {
await init(file, extension) await init(file, extension)
const info = await openapi2.getInfo() const info = await openapi2.getInfo()
expect(info.url).toBe("https://petstore.swagger.io/v2")
expect(info.name).toBe("Swagger Petstore") expect(info.name).toBe("Swagger Petstore")
} }
@ -92,12 +91,12 @@ describe("OpenAPI2 Import", () => {
it("populates path", async () => { it("populates path", async () => {
const assertions = { const assertions = {
"createEntity" : "entities", "createEntity" : "http://example.com/entities",
"getEntities" : "entities", "getEntities" : "http://example.com/entities",
"getEntity" : "entities/{{entityId}}", "getEntity" : "http://example.com/entities/{{entityId}}",
"updateEntity" : "entities/{{entityId}}", "updateEntity" : "http://example.com/entities/{{entityId}}",
"patchEntity" : "entities/{{entityId}}", "patchEntity" : "http://example.com/entities/{{entityId}}",
"deleteEntity" : "entities/{{entityId}}" "deleteEntity" : "http://example.com/entities/{{entityId}}"
} }
await runTests("crud", testPath, assertions) await runTests("crud", testPath, assertions)
}) })

View File

@ -51,30 +51,24 @@ describe("Rest Importer", () => {
await init(data) await init(data)
const info = await restImporter.getInfo() const info = await restImporter.getInfo()
expect(info.name).toBe(assertions[key].name) expect(info.name).toBe(assertions[key].name)
expect(info.url).toBe(assertions[key].url)
} }
it("gets info", async () => { it("gets info", async () => {
const assertions = { const assertions = {
"oapi2CrudJson" : { "oapi2CrudJson" : {
name: "CRUD", name: "CRUD",
url: "http://example.com"
}, },
"oapi2CrudYaml" : { "oapi2CrudYaml" : {
name: "CRUD", name: "CRUD",
url: "http://example.com"
}, },
"oapi2PetstoreJson" : { "oapi2PetstoreJson" : {
name: "Swagger Petstore", name: "Swagger Petstore",
url: "https://petstore.swagger.io/v2"
}, },
"oapi2PetstoreYaml" :{ "oapi2PetstoreYaml" :{
name: "Swagger Petstore", name: "Swagger Petstore",
url: "https://petstore.swagger.io/v2"
}, },
"curl": { "curl": {
name: "example.com", name: "example.com",
url: "http://example.com"
} }
} }
await runTest(testGetInfo, assertions) await runTest(testGetInfo, assertions)

View File

@ -8,6 +8,7 @@ const { BaseQueryVerbs } = require("../../../constants")
const { Thread, ThreadType } = require("../../../threads") const { Thread, ThreadType } = require("../../../threads")
const { save: saveDatasource } = require("../datasource") const { save: saveDatasource } = require("../datasource")
const { RestImporter } = require("./import") const { RestImporter } = require("./import")
const { invalidateDynamicVariables } = require("../../../threads/utils")
const Runner = new Thread(ThreadType.QUERY, { timeoutMs: 10000 }) const Runner = new Thread(ThreadType.QUERY, { timeoutMs: 10000 })
@ -166,8 +167,28 @@ exports.executeV2 = async function (ctx) {
return execute(ctx, { rowsOnly: false }) return execute(ctx, { rowsOnly: false })
} }
const removeDynamicVariables = async (db, queryId) => {
const query = await db.get(queryId)
const datasource = await db.get(query.datasourceId)
const dynamicVariables = datasource.config.dynamicVariables
if (dynamicVariables) {
// delete dynamic variables from the datasource
const newVariables = dynamicVariables.filter(dv => dv.queryId !== queryId)
datasource.config.dynamicVariables = newVariables
await db.put(datasource)
// invalidate the deleted variables
const variablesToDelete = dynamicVariables.filter(
dv => dv.queryId === queryId
)
await invalidateDynamicVariables(variablesToDelete)
}
}
exports.destroy = async function (ctx) { exports.destroy = async function (ctx) {
const db = new CouchDB(ctx.appId) const db = new CouchDB(ctx.appId)
await removeDynamicVariables(db, ctx.params.queryId)
await db.remove(ctx.params.queryId, ctx.params.revId) await db.remove(ctx.params.queryId, ctx.params.revId)
ctx.message = `Query deleted.` ctx.message = `Query deleted.`
ctx.status = 200 ctx.status = 200

View File

@ -171,7 +171,7 @@ module RestModule {
getUrl(path: string, queryString: string): string { getUrl(path: string, queryString: string): string {
const main = `${path}?${queryString}` const main = `${path}?${queryString}`
let complete = main let complete = main
if (this.config.url && !main.startsWith(this.config.url)) { if (this.config.url && !main.startsWith("http")) {
complete = !this.config.url ? main : `${this.config.url}/${main}` complete = !this.config.url ? main : `${this.config.url}/${main}`
} }
if (!complete.startsWith("http")) { if (!complete.startsWith("http")) {

View File

@ -42,10 +42,11 @@ exports.checkCacheForDynamicVariable = async (queryId, variable) => {
} }
exports.invalidateDynamicVariables = async cachedVars => { exports.invalidateDynamicVariables = async cachedVars => {
const cache = await getClient()
let promises = [] let promises = []
for (let variable of cachedVars) { for (let variable of cachedVars) {
promises.push( promises.push(
client.delete(makeVariableKey(variable.queryId, variable.name)) cache.delete(makeVariableKey(variable.queryId, variable.name))
) )
} }
await Promise.all(promises) await Promise.all(promises)