Merge pull request #3932 from Budibase/feature/query-variables
Fixes for new rest datasource
This commit is contained in:
commit
10d9928679
|
@ -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);
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
[],
|
[],
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue