Adding query save, fixing routing to handle existing rest queries, adding in full saving of enabled headers functionality, fixing various issues discovered while testing.

This commit is contained in:
mike12345567 2021-12-07 18:24:10 +00:00
parent b31cd5b6f7
commit c0512fa242
15 changed files with 246 additions and 74 deletions

View File

@ -0,0 +1,37 @@
<script>
import { Label, Select } from "@budibase/bbui"
import { permissions, roles } from "stores/backend"
export let query
export let saveId
export let label
$: updateRole(roleId, saveId)
let roleId
async function updateRole(role, id) {
roleId = role
const queryId = query?._id || id
if (roleId && queryId) {
for (let level of ["read", "write"]) {
await permissions.save({
level,
role,
resource: queryId,
})
}
}
}
</script>
{#if label}
<Label>{label}</Label>
{/if}
<Select
value={roleId}
on:change={e => updateRole(e.detail)}
options={$roles}
getOptionLabel={x => x.name}
getOptionValue={x => x._id}
/>

View File

@ -14,32 +14,60 @@
export let defaults
export let object = defaults || {}
export let activity = {}
export let readOnly
export let noAddButton
export let name
export let headings = false
export let activity = false
export let options
export let toggle
let fields = Object.entries(object).map(([name, value]) => ({ name, value }))
let fieldActivity = []
$: object = fields.reduce(
(acc, next) => ({ ...acc, [next.name]: next.value }),
{}
)
$: fieldActivity = buildFieldActivity(activity)
function buildFieldActivity(obj) {
if (!obj || typeof obj !== "object") {
return []
}
const array = Array(fields.length)
for (let [key, value] of Object.entries(obj)) {
const field = fields.find(el => el.name === key)
const idx = fields.indexOf(field)
if (idx !== -1) {
array[idx] = value
}
}
return array
}
export function addEntry() {
fields = [...fields, { name: "", value: "" }]
fieldActivity = [...fieldActivity, true]
changed()
}
function deleteEntry(idx) {
fields.splice(idx, 1)
fieldActivity.splice(idx, 1)
changed()
}
function changed() {
fields = fields
const newActivity = {}
for (let idx = 0; idx < fields.length; idx++) {
const fieldName = fields[idx].name
if (fieldName) {
newActivity[fieldName] = fieldActivity[idx]
}
}
activity = newActivity
dispatch("change", fields)
}
</script>
@ -67,8 +95,12 @@
on:change={changed}
/>
{/if}
{#if activity}
<Toggle />
{#if toggle}
<Toggle
bind:value={fieldActivity[idx]}
on:change={changed}
default={true}
/>
{/if}
{#if !readOnly}
<Icon hoverable name="Close" on:click={() => deleteEntry(idx)} />

View File

@ -22,7 +22,6 @@
datasources,
integrations,
queries,
roles,
permissions,
} from "stores/backend"
import { capitalise } from "../../helpers"
@ -32,6 +31,7 @@
import { onMount } from "svelte"
import KeyValueBuilder from "./KeyValueBuilder.svelte"
import { fieldsToSchema, schemaToFields } from "helpers/data/utils"
import AccessLevelSelect from "./AccessLevelSelect.svelte"
export let query
@ -39,6 +39,7 @@
let parameters
let data = []
let roleId
let saveId
const transformerDocs =
"https://docs.budibase.com/building-apps/data/transformers"
@ -62,19 +63,6 @@
}
}
async function updateRole(role, id = null) {
roleId = role
if (query?._id || id) {
for (let level of ["read", "write"]) {
await permissions.save({
level,
role,
resource: query?._id || id,
})
}
}
}
function populateExtraQuery(extraQueryFields) {
query.fields.extra = extraQueryFields
}
@ -99,7 +87,7 @@
async function saveQuery() {
try {
const { _id } = await queries.save(query.datasourceId, query)
await updateRole(roleId, _id)
saveId = _id
notifications.success(`Query saved successfully.`)
$goto(`../${_id}`)
} catch (err) {
@ -142,14 +130,7 @@
/>
</div>
<div class="config-field">
<Label>Access level</Label>
<Select
value={roleId}
on:change={e => updateRole(e.detail)}
options={$roles}
getOptionLabel={x => x.name}
getOptionValue={x => x._id}
/>
<AccessLevelSelect {saveId} {query} label="Access Level" />
</div>
{#if integrationInfo?.extra && query.queryVerb}
<ExtraQueryConfig

View File

@ -17,3 +17,35 @@ export function fieldsToSchema(fields) {
}
return response
}
export function breakQueryString(qs) {
if (!qs) {
return {}
}
if (qs.includes("?")) {
qs = qs.split("?")[1]
}
const params = qs.split("&")
let paramObj = {}
for (let param of params) {
const [key, value] = param.split("=")
paramObj[key] = value
}
return paramObj
}
export function buildQueryString(obj) {
let str = ""
if (obj) {
for (let [key, value] of Object.entries(obj)) {
if (!key || key === "") {
continue
}
if (str !== "") {
str += "&"
}
str += `${key}=${value || ""}`
}
}
return str
}

View File

@ -1,6 +1,6 @@
<script>
import { store, automationStore } from "builderStore"
import { roles } from "stores/backend"
import { roles, flags } from "stores/backend"
import { Icon, ActionGroup, Tabs, Tab, notifications } from "@budibase/bbui"
import DeployModal from "components/deploy/DeployModal.svelte"
import RevertModal from "components/deploy/RevertModal.svelte"
@ -49,6 +49,7 @@
}
await automationStore.actions.fetch()
await roles.fetch()
await flags.fetch()
return pkg
} else {
throw new Error(pkg)

View File

@ -4,17 +4,19 @@
import { IntegrationTypes } from "constants/backend"
import { goto } from "@roxi/routify"
let datasourceId
if ($params.query) {
const query = $queries.list.find(q => q._id === $params.query)
if (query) {
queries.select(query)
datasourceId = query.datasourceId
}
}
const datasource = $datasources.list.find(
ds => ds._id === $datasources.selected
ds => ds._id === $datasources.selected || ds._id === datasourceId
)
if (datasource?.source === IntegrationTypes.REST) {
$goto("../rest")
$goto(`../rest/${$params.query}`)
}
</script>

View File

@ -0,0 +1,13 @@
<script>
import { params } from "@roxi/routify"
import { queries } from "stores/backend"
if ($params.query) {
const query = $queries.list.find(q => q._id === $params.query)
if (query) {
queries.select(query)
}
}
</script>
<slot />

View File

@ -1,6 +1,6 @@
<script>
import { params } from "@roxi/routify"
import { datasources, integrations, queries } from "stores/backend"
import { datasources, integrations, queries, flags } from "stores/backend"
import {
Layout,
Input,
@ -22,10 +22,15 @@
import CodeMirrorEditor, {
EditorModes,
} from "components/common/CodeMirrorEditor.svelte"
import RestBodyInput from "../_components/RestBodyInput.svelte"
import RestBodyInput from "../../_components/RestBodyInput.svelte"
import { capitalise } from "helpers"
import { onMount } from "svelte"
import { fieldsToSchema, schemaToFields } from "helpers/data/utils"
import {
fieldsToSchema,
schemaToFields,
breakQueryString,
buildQueryString,
} from "helpers/data/utils"
import {
RestBodyTypes as bodyTypes,
SchemaTypeOptions,
@ -35,7 +40,8 @@
let query, datasource
let breakQs = {}
let url = ""
let response, schema
let saveId
let response, schema, isGet
let datasourceType, integrationInfo, queryConfig, responseSuccess
$: datasource = $datasources.list.find(ds => ds._id === query?.datasourceId)
@ -44,6 +50,7 @@
$: queryConfig = integrationInfo?.query
$: url = buildUrl(url, breakQs)
$: checkQueryName(url)
$: isGet = query?.queryVerb === "read"
$: responseSuccess =
response?.info?.code >= 200 && response?.info?.code <= 206
@ -58,35 +65,6 @@
)
}
function breakQueryString(qs) {
if (!qs) {
return {}
}
if (qs.includes("?")) {
qs = qs.split("?")[1]
}
const params = qs.split("&")
let paramObj = {}
for (let param of params) {
const [key, value] = param.split("=")
paramObj[key] = value
}
}
function buildQueryString(obj) {
let str = ""
for (let [key, value] of Object.entries(obj)) {
if (!key || key === "") {
continue
}
if (str !== "") {
str += "&"
}
str += `${key}=${value || ""}`
}
return str
}
function checkQueryName(inputUrl = null) {
if (query && (!query.name || query.flags.urlName)) {
query.flags.urlName = true
@ -113,11 +91,19 @@
const queryString = buildQueryString(breakQs)
newQuery.fields.path = url.split("?")[0]
newQuery.fields.queryString = queryString
newQuery.schema = fieldsToSchema(schema)
return newQuery
}
function saveQuery() {
query.schema = fieldsToSchema(schema)
async function saveQuery() {
const toSave = buildQuery()
try {
const { _id } = await queries.save(toSave.datasourceId, toSave)
saveId = _id
notifications.success(`Request saved successfully.`)
} catch (err) {
notifications.error(`Error creating query. ${err.message}`)
}
}
async function runQuery() {
@ -138,7 +124,7 @@
query = getSelectedQuery()
const qs = query?.fields.queryString
breakQs = breakQueryString(qs)
url = buildUrl(query.fields.path, qs)
url = buildUrl(query.fields.path, breakQs)
schema = schemaToFields(query.schema)
if (query && !query.transformer) {
query.transformer = "return data"
@ -146,7 +132,6 @@
if (query && !query.flags) {
query.flags = {
urlName: false,
bannerCleared: false,
}
}
if (query && !query.fields.bodyType) {
@ -187,15 +172,16 @@
<Tab title="Headers">
<KeyValueBuilder
bind:object={query.fields.headers}
bind:activity={query.fields.enabledHeaders}
toggle
name="header"
headings
activity
/>
</Tab>
<Tab title="Body">
<RadioGroup
bind:value={query.fields.bodyType}
options={bodyTypes}
options={isGet ? [bodyTypes[0]] : bodyTypes}
direction="horizontal"
getOptionLabel={option => option.name}
getOptionValue={option => option.value}
@ -204,11 +190,12 @@
</Tab>
<Tab title="Transformer">
<Layout noPadding>
{#if !query.flags.bannerCleared}
{#if !$flags.queryTransformerBanner}
<Banner
extraButtonText="Learn more"
extraButtonAction={learnMoreBanner}
on:change={() => (query.flags.bannerCleared = true)}
on:change={() =>
flags.updateFlag("queryTransformerBanner", true)}
>
Add a JavaScript function to transform the query result.
</Banner>

View File

@ -0,0 +1,37 @@
import { writable } from "svelte/store"
import api from "builderStore/api"
export function createFlagsStore() {
const { subscribe, set } = writable({})
return {
subscribe,
fetch: async () => {
const { doc, response } = await getFlags()
set(doc)
return response
},
updateFlag: async (flag, value) => {
const response = await api.post("/api/users/flags", {
flag,
value,
})
if (response.status === 200) {
const { doc } = await getFlags()
set(doc)
}
return response
},
}
}
async function getFlags() {
const response = await api.get("/api/users/flags")
let doc = {}
if (response.status === 200) {
doc = await response.json()
}
return { doc, response }
}
export const flags = createFlagsStore()

View File

@ -7,3 +7,4 @@ export { roles } from "./roles"
export { datasources } from "./datasources"
export { integrations } from "./integrations"
export { queries } from "./queries"
export { flags } from "./flags"

View File

@ -2,6 +2,7 @@ const CouchDB = require("../../db")
const {
generateUserMetadataID,
getUserMetadataParams,
generateUserFlagID,
} = require("../../db/utils")
const { InternalTables } = require("../../db/utils")
const { getGlobalUsers, getRawGlobalUser } = require("../../utilities/global")
@ -195,3 +196,35 @@ exports.destroyMetadata = async function (ctx) {
exports.findMetadata = async function (ctx) {
ctx.body = await getFullUser(ctx, ctx.params.id)
}
exports.setFlag = async function (ctx) {
const userId = ctx.user._id
const { flag, value } = ctx.request.body
if (!flag) {
ctx.throw(400, "Must supply a 'flag' field in request body.")
}
const flagDocId = generateUserFlagID(userId)
const db = new CouchDB(ctx.appId)
let doc
try {
doc = await db.get(flagDocId)
} catch (err) {
doc = { _id: flagDocId }
}
doc[flag] = value || true
await db.put(doc)
ctx.body = { message: "Flag set successfully" }
}
exports.getFlags = async function (ctx) {
const userId = ctx.user._id
const docId = generateUserFlagID(userId)
const db = new CouchDB(ctx.appId)
let doc
try {
doc = await db.get(docId)
} catch (err) {
doc = { _id: docId }
}
ctx.body = doc
}

View File

@ -33,6 +33,7 @@ function generateQueryValidation() {
extra: Joi.object().optional(),
schema: Joi.object({}).required().unknown(true),
transformer: Joi.string().optional(),
flags: Joi.object().optional(),
}))
}

View File

@ -39,5 +39,15 @@ router
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
controller.syncUser
)
.post(
"/api/users/flags",
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
controller.setFlag
)
.get(
"/api/users/flags",
authorized(PermissionTypes.USER, PermissionLevels.READ),
controller.getFlags
)
module.exports = router

View File

@ -40,6 +40,7 @@ const DocumentTypes = {
DEPLOYMENTS: "deployments",
METADATA: "metadata",
MEM_VIEW: "view",
USER_FLAG: "flag",
}
const ViewNames = {
@ -339,6 +340,14 @@ exports.getQueryParams = (datasourceId = null, otherProps = {}) => {
)
}
/**
* Generates a new flag document ID.
* @returns {string} The ID of the flag document that was generated.
*/
exports.generateUserFlagID = userId => {
return `${DocumentTypes.USER_FLAG}${SEPARATOR}${userId}`
}
exports.generateMetadataID = (type, entityId) => {
return `${DocumentTypes.METADATA}${SEPARATOR}${type}${SEPARATOR}${entityId}`
}