diff --git a/charts/budibase/README.md b/charts/budibase/README.md index 342011bdb1..dea7d1dbae 100644 --- a/charts/budibase/README.md +++ b/charts/budibase/README.md @@ -140,7 +140,7 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml | ingress.className | string | `""` | What ingress class to use. | | ingress.enabled | bool | `true` | Whether to create an Ingress resource pointing to the Budibase proxy. | | ingress.hosts | list | `[]` | Standard hosts block for the Ingress resource. Defaults to pointing to the Budibase proxy. | -| nameOverride | string | `""` | Override the name of the deploymen. Defaults to {{ .Chart.Name }}. | +| nameOverride | string | `""` | Override the name of the deployment. Defaults to {{ .Chart.Name }}. | | service.port | int | `10000` | Port to expose on the service. | | service.type | string | `"ClusterIP"` | Service type for the service that points to the main Budibase proxy pod. | | serviceAccount.annotations | object | `{}` | Annotations to add to the service account | diff --git a/charts/budibase/values.yaml b/charts/budibase/values.yaml index 09262df463..19b6c22d6c 100644 --- a/charts/budibase/values.yaml +++ b/charts/budibase/values.yaml @@ -1,6 +1,6 @@ # -- Passed to all pods created by this chart. Should not ordinarily need to be changed. imagePullSecrets: [] -# -- Override the name of the deploymen. Defaults to {{ .Chart.Name }}. +# -- Override the name of the deployment. Defaults to {{ .Chart.Name }}. nameOverride: "" serviceAccount: diff --git a/lerna.json b/lerna.json index b0003be838..01e56982d5 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.22.2", + "version": "2.22.3", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/backend-core/src/cache/docWritethrough.ts b/packages/backend-core/src/cache/docWritethrough.ts index 1b129bb26a..05f13a0d91 100644 --- a/packages/backend-core/src/cache/docWritethrough.ts +++ b/packages/backend-core/src/cache/docWritethrough.ts @@ -1,6 +1,6 @@ import { AnyDocument, Database } from "@budibase/types" -import { JobQueue, createQueue } from "../queue" +import { JobQueue, Queue, createQueue } from "../queue" import * as dbUtils from "../db" interface ProcessDocMessage { @@ -12,18 +12,26 @@ interface ProcessDocMessage { const PERSIST_MAX_ATTEMPTS = 100 let processor: DocWritethroughProcessor | undefined -export const docWritethroughProcessorQueue = createQueue( - JobQueue.DOC_WRITETHROUGH_QUEUE, - { - jobOptions: { - attempts: PERSIST_MAX_ATTEMPTS, - }, - } -) +export class DocWritethroughProcessor { + private static _queue: Queue + + public static get queue() { + if (!DocWritethroughProcessor._queue) { + DocWritethroughProcessor._queue = createQueue( + JobQueue.DOC_WRITETHROUGH_QUEUE, + { + jobOptions: { + attempts: PERSIST_MAX_ATTEMPTS, + }, + } + ) + } + + return DocWritethroughProcessor._queue + } -class DocWritethroughProcessor { init() { - docWritethroughProcessorQueue.process(async message => { + DocWritethroughProcessor.queue.process(async message => { try { await this.persistToDb(message.data) } catch (err: any) { @@ -76,7 +84,7 @@ export class DocWritethrough { } async patch(data: Record) { - await docWritethroughProcessorQueue.add({ + await DocWritethroughProcessor.queue.add({ dbName: this.db.name, docId: this.docId, data, diff --git a/packages/backend-core/src/cache/tests/docWritethrough.spec.ts b/packages/backend-core/src/cache/tests/docWritethrough.spec.ts index 9eb27efcad..47b3f0672f 100644 --- a/packages/backend-core/src/cache/tests/docWritethrough.spec.ts +++ b/packages/backend-core/src/cache/tests/docWritethrough.spec.ts @@ -6,7 +6,7 @@ import { getDB } from "../../db" import { DocWritethrough, - docWritethroughProcessorQueue, + DocWritethroughProcessor, init, } from "../docWritethrough" @@ -15,7 +15,7 @@ import InMemoryQueue from "../../queue/inMemoryQueue" const initialTime = Date.now() async function waitForQueueCompletion() { - const queue: InMemoryQueue = docWritethroughProcessorQueue as never + const queue: InMemoryQueue = DocWritethroughProcessor.queue as never await queue.waitForCompletion() } @@ -235,7 +235,7 @@ describe("docWritethrough", () => { return acc }, {}) } - const queueMessageSpy = jest.spyOn(docWritethroughProcessorQueue, "add") + const queueMessageSpy = jest.spyOn(DocWritethroughProcessor.queue, "add") await config.doInTenant(async () => { let patches = await parallelPatch(5) diff --git a/packages/builder/src/helpers/components.js b/packages/builder/src/helpers/components.js index 4f4f3ed380..a03ebfdfa7 100644 --- a/packages/builder/src/helpers/components.js +++ b/packages/builder/src/helpers/components.js @@ -279,3 +279,11 @@ export const buildContextTreeLookupMap = rootComponent => { }) return map } + +// Get a flat list of ids for all descendants of a component +export const getChildIdsForComponent = component => { + return [ + component._id, + ...(component?._children ?? []).map(getChildIdsForComponent).flat(1), + ] +} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte index 95e7a66be9..e74a05ff99 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte @@ -10,6 +10,7 @@ navigationStore, selectedScreen, hoverStore, + componentTreeNodesStore, snippets, } from "stores/builder" import ConfirmDialog from "components/common/ConfirmDialog.svelte" @@ -132,6 +133,7 @@ error = event.error || "An unknown error occurred" } else if (type === "select-component" && data.id) { componentStore.select(data.id) + componentTreeNodesStore.makeNodeVisible(data.id) } else if (type === "hover-component") { hoverStore.hover(data.id, false) } else if (type === "update-prop") { diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentKeyHandler.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentKeyHandler.svelte index f6bbac39a5..7e9c113a77 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentKeyHandler.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentKeyHandler.svelte @@ -4,12 +4,12 @@ selectedScreen, componentStore, selectedComponent, + componentTreeNodesStore, } from "stores/builder" - import { findComponent } from "helpers/components" + import { findComponent, getChildIdsForComponent } from "helpers/components" import { goto, isActive } from "@roxi/routify" import { notifications } from "@budibase/bbui" import ConfirmDialog from "components/common/ConfirmDialog.svelte" - import componentTreeNodesStore from "stores/portal/componentTreeNodesStore" let confirmDeleteDialog let confirmEjectDialog @@ -63,38 +63,25 @@ componentStore.selectNext() }, ["ArrowRight"]: component => { - componentTreeNodesStore.expandNode(component._id) + componentTreeNodesStore.expandNodes([component._id]) }, ["ArrowLeft"]: component => { - componentTreeNodesStore.collapseNode(component._id) + // Select the collapsing root component to ensure the currently selected component is not + // hidden in a collapsed node + componentStore.select(component._id) + componentTreeNodesStore.collapseNodes([component._id]) }, ["Ctrl+ArrowRight"]: component => { - componentTreeNodesStore.expandNode(component._id) - - const expandChildren = component => { - const children = component._children ?? [] - - children.forEach(child => { - componentTreeNodesStore.expandNode(child._id) - expandChildren(child) - }) - } - - expandChildren(component) + const childIds = getChildIdsForComponent(component) + componentTreeNodesStore.expandNodes(childIds) }, ["Ctrl+ArrowLeft"]: component => { - componentTreeNodesStore.collapseNode(component._id) + // Select the collapsing root component to ensure the currently selected component is not + // hidden in a collapsed node + componentStore.select(component._id) - const collapseChildren = component => { - const children = component._children ?? [] - - children.forEach(child => { - componentTreeNodesStore.collapseNode(child._id) - collapseChildren(child) - }) - } - - collapseChildren(component) + const childIds = getChildIdsForComponent(component) + componentTreeNodesStore.collapseNodes(childIds) }, ["Escape"]: () => { if ($isActive(`./:componentId/new`)) { diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentTree.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentTree.svelte index f24235ad07..0219dc304d 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentTree.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentTree.svelte @@ -7,8 +7,8 @@ componentStore, userSelectedResourceMap, selectedComponent, - selectedComponentPath, hoverStore, + componentTreeNodesStore, } from "stores/builder" import { findComponentPath, @@ -17,7 +17,6 @@ } from "helpers/components" import { get } from "svelte/store" import { dndStore } from "./dndStore" - import componentTreeNodesStore from "stores/portal/componentTreeNodesStore" export let components = [] export let level = 0 @@ -64,14 +63,11 @@ } } - const isOpen = (component, selectedComponentPath, openNodes) => { + const isOpen = component => { if (!component?._children?.length) { return false } - if (selectedComponentPath.slice(0, -1).includes(component._id)) { - return true - } - return openNodes[`nodeOpen-${component._id}`] + return componentTreeNodesStore.isNodeExpanded(component._id) } const isChildOfSelectedComponent = component => { @@ -83,6 +79,11 @@ return findComponentPath($selectedComponent, component._id)?.length > 0 } + const handleIconClick = componentId => { + componentStore.select(componentId) + componentTreeNodesStore.toggleNode(componentId) + } + const hover = hoverStore.hover @@ -90,7 +91,7 @@
    {#each filteredComponents || [] as component, index (component._id)} - {@const opened = isOpen(component, $selectedComponentPath, openNodes)} + {@const opened = isOpen(component, openNodes)}
  • { componentStore.select(component._id) @@ -104,7 +105,7 @@ on:dragend={dndStore.actions.reset} on:dragstart={() => dndStore.actions.dragstart(component)} on:dragover={dragover(component, index)} - on:iconClick={() => componentTreeNodesStore.toggleNode(component._id)} + on:iconClick={() => handleIconClick(component._id)} on:drop={onDrop} hovering={$hoverStore.componentId === component._id} on:mouseenter={() => hover(component._id)} diff --git a/packages/builder/src/stores/builder/componentTreeNodes.js b/packages/builder/src/stores/builder/componentTreeNodes.js new file mode 100644 index 0000000000..a8e09a2051 --- /dev/null +++ b/packages/builder/src/stores/builder/componentTreeNodes.js @@ -0,0 +1,67 @@ +import { get } from "svelte/store" +import { createSessionStorageStore } from "@budibase/frontend-core" +import { selectedScreen as selectedScreenStore } from "./screens" +import { findComponentPath } from "helpers/components" + +const baseStore = createSessionStorageStore("openNodes", {}) + +const toggleNode = componentId => { + baseStore.update(openNodes => { + openNodes[`nodeOpen-${componentId}`] = !openNodes[`nodeOpen-${componentId}`] + + return openNodes + }) +} + +const expandNodes = componentIds => { + baseStore.update(openNodes => { + const newNodes = Object.fromEntries( + componentIds.map(id => [`nodeOpen-${id}`, true]) + ) + + return { ...openNodes, ...newNodes } + }) +} + +const collapseNodes = componentIds => { + baseStore.update(openNodes => { + const newNodes = Object.fromEntries( + componentIds.map(id => [`nodeOpen-${id}`, false]) + ) + + return { ...openNodes, ...newNodes } + }) +} + +// Will ensure all parents of a node are expanded so that it is visible in the tree +const makeNodeVisible = componentId => { + const selectedScreen = get(selectedScreenStore) + + const path = findComponentPath(selectedScreen.props, componentId) + + const componentIds = path.map(component => component._id) + + baseStore.update(openNodes => { + const newNodes = Object.fromEntries( + componentIds.map(id => [`nodeOpen-${id}`, true]) + ) + + return { ...openNodes, ...newNodes } + }) +} + +const isNodeExpanded = componentId => { + const openNodes = get(baseStore) + return !!openNodes[`nodeOpen-${componentId}`] +} + +const store = { + subscribe: baseStore.subscribe, + toggleNode, + expandNodes, + makeNodeVisible, + collapseNodes, + isNodeExpanded, +} + +export default store diff --git a/packages/builder/src/stores/builder/components.js b/packages/builder/src/stores/builder/components.js index bf8a1ef896..0d7da1ba58 100644 --- a/packages/builder/src/stores/builder/components.js +++ b/packages/builder/src/stores/builder/components.js @@ -19,6 +19,7 @@ import { appStore, previewStore, tables, + componentTreeNodesStore, } from "stores/builder/index" import { buildFormSchema, getSchemaForDatasource } from "dataBinding" import { @@ -29,7 +30,6 @@ import { } from "constants/backend" import BudiStore from "../BudiStore" import { Utils } from "@budibase/frontend-core" -import componentTreeNodesStore from "stores/portal/componentTreeNodesStore" export const INITIAL_COMPONENTS_STATE = { components: {}, @@ -653,8 +653,11 @@ export class ComponentStore extends BudiStore { this.update(state => { state.selectedScreenId = targetScreenId state.selectedComponentId = newComponentId + return state }) + + componentTreeNodesStore.makeNodeVisible(newComponentId) } getPrevious() { @@ -663,7 +666,6 @@ export class ComponentStore extends BudiStore { const screen = get(selectedScreen) const parent = findComponentParent(screen.props, componentId) const index = parent?._children.findIndex(x => x._id === componentId) - const componentTreeNodes = get(componentTreeNodesStore) // Check for screen and navigation component edge cases const screenComponentId = `${screen._id}-screen` @@ -680,16 +682,16 @@ export class ComponentStore extends BudiStore { // If we have siblings above us, choose the sibling or a descendant if (index > 0) { - // If sibling before us accepts children, select a descendant + // If sibling before us accepts children, and is not collapsed, select a descendant const previousSibling = parent._children[index - 1] if ( previousSibling._children?.length && - componentTreeNodes[`nodeOpen-${previousSibling._id}`] + componentTreeNodesStore.isNodeExpanded(previousSibling._id) ) { let target = previousSibling while ( target._children?.length && - componentTreeNodes[`nodeOpen-${target._id}`] + componentTreeNodesStore.isNodeExpanded(target._id) ) { target = target._children[target._children.length - 1] } @@ -711,7 +713,6 @@ export class ComponentStore extends BudiStore { const screen = get(selectedScreen) const parent = findComponentParent(screen.props, componentId) const index = parent?._children.findIndex(x => x._id === componentId) - const componentTreeNodes = get(componentTreeNodesStore) // Check for screen and navigation component edge cases const screenComponentId = `${screen._id}-screen` @@ -720,11 +721,11 @@ export class ComponentStore extends BudiStore { return navComponentId } - // If we have children, select first child + // If we have children, select first child, and the node is not collapsed if ( component._children?.length && (state.selectedComponentId === navComponentId || - componentTreeNodes[`nodeOpen-${component._id}`]) + componentTreeNodesStore.isNodeExpanded(component._id)) ) { return component._children[0]._id } else if (!parent) { @@ -803,7 +804,10 @@ export class ComponentStore extends BudiStore { // sibling const previousSibling = parent._children[index - 1] const definition = this.getDefinition(previousSibling._component) - if (definition.hasChildren) { + if ( + definition.hasChildren && + componentTreeNodesStore.isNodeExpanded(previousSibling._id) + ) { previousSibling._children.push(originalComponent) } @@ -852,10 +856,13 @@ export class ComponentStore extends BudiStore { // Move below the next sibling if we are not the last sibling if (index < parent._children.length) { - // If the next sibling has children, become the first child + // If the next sibling has children, and is not collapsed, become the first child const nextSibling = parent._children[index] const definition = this.getDefinition(nextSibling._component) - if (definition.hasChildren) { + if ( + definition.hasChildren && + componentTreeNodesStore.isNodeExpanded(nextSibling._id) + ) { nextSibling._children.splice(0, 0, originalComponent) } @@ -1151,13 +1158,3 @@ export const selectedComponent = derived( return clone } ) - -export const selectedComponentPath = derived( - [componentStore, selectedScreen], - ([$store, $selectedScreen]) => { - return findComponentPath( - $selectedScreen?.props, - $store.selectedComponentId - ).map(component => component._id) - } -) diff --git a/packages/builder/src/stores/builder/index.js b/packages/builder/src/stores/builder/index.js index 4df4bc3c41..5e94ce0552 100644 --- a/packages/builder/src/stores/builder/index.js +++ b/packages/builder/src/stores/builder/index.js @@ -1,10 +1,6 @@ import { layoutStore } from "./layouts.js" import { appStore } from "./app.js" -import { - componentStore, - selectedComponent, - selectedComponentPath, -} from "./components" +import { componentStore, selectedComponent } from "./components" import { navigationStore } from "./navigation.js" import { themeStore } from "./theme.js" import { screenStore, selectedScreen, sortedScreens } from "./screens.js" @@ -31,8 +27,10 @@ import { integrations } from "./integrations" import { sortedIntegrations } from "./sortedIntegrations" import { queries } from "./queries" import { flags } from "./flags" +import componentTreeNodesStore from "./componentTreeNodes" export { + componentTreeNodesStore, layoutStore, appStore, componentStore, @@ -51,7 +49,6 @@ export { isOnlyUser, deploymentStore, selectedComponent, - selectedComponentPath, tables, views, viewsV2, diff --git a/packages/builder/src/stores/portal/componentTreeNodesStore.js b/packages/builder/src/stores/portal/componentTreeNodesStore.js deleted file mode 100644 index a6c07ae03b..0000000000 --- a/packages/builder/src/stores/portal/componentTreeNodesStore.js +++ /dev/null @@ -1,36 +0,0 @@ -import { createSessionStorageStore } from "@budibase/frontend-core" - -const baseStore = createSessionStorageStore("openNodes", {}) - -const toggleNode = componentId => { - baseStore.update(openNodes => { - openNodes[`nodeOpen-${componentId}`] = !openNodes[`nodeOpen-${componentId}`] - - return openNodes - }) -} - -const expandNode = componentId => { - baseStore.update(openNodes => { - openNodes[`nodeOpen-${componentId}`] = true - - return openNodes - }) -} - -const collapseNode = componentId => { - baseStore.update(openNodes => { - openNodes[`nodeOpen-${componentId}`] = false - - return openNodes - }) -} - -const store = { - subscribe: baseStore.subscribe, - toggleNode, - expandNode, - collapseNode, -} - -export default store diff --git a/packages/builder/tsconfig.json b/packages/builder/tsconfig.json index b2084dba65..be79dfd85c 100644 --- a/packages/builder/tsconfig.json +++ b/packages/builder/tsconfig.json @@ -4,6 +4,16 @@ "composite": true, "declaration": true, "sourceMap": true, - "baseUrl": "." + "baseUrl": ".", + "paths": { + "assets/*": ["./assets/*"], + "@budibase/*": [ + "../*/src/index.ts", + "../*/src/index.js", + "../*", + "../../node_modules/@budibase/*" + ], + "*": ["./src/*"] + } } } diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json index f89ad95c28..e52d886194 100644 --- a/packages/cli/tsconfig.build.json +++ b/packages/cli/tsconfig.build.json @@ -11,6 +11,7 @@ "types": ["node", "jest"], "outDir": "dist", "skipLibCheck": true, + "baseUrl": ".", "paths": { "@budibase/types": ["../types/src"], "@budibase/backend-core": ["../backend-core/src"], diff --git a/packages/client/src/components/app/blocks/MultiStepFormblock.svelte b/packages/client/src/components/app/blocks/MultiStepFormblock.svelte index 2443aea017..762afc1579 100644 --- a/packages/client/src/components/app/blocks/MultiStepFormblock.svelte +++ b/packages/client/src/components/app/blocks/MultiStepFormblock.svelte @@ -75,6 +75,7 @@ .filter(field => !field.autocolumn) .map(field => ({ name: field.name, + active: true, })) } diff --git a/packages/server/src/api/controllers/query/index.ts b/packages/server/src/api/controllers/query/index.ts index 3c21537484..0dba20dacd 100644 --- a/packages/server/src/api/controllers/query/index.ts +++ b/packages/server/src/api/controllers/query/index.ts @@ -14,22 +14,35 @@ import { SessionCookie, JsonFieldSubType, QueryResponse, - QueryPreview, QuerySchema, FieldType, ExecuteQueryRequest, ExecuteQueryResponse, - Row, QueryParameter, PreviewQueryRequest, PreviewQueryResponse, } from "@budibase/types" import { ValidQueryNameRegex, utils as JsonUtils } from "@budibase/shared-core" +import { findHBSBlocks } from "@budibase/string-templates" const Runner = new Thread(ThreadType.QUERY, { timeoutMs: env.QUERY_THREAD_TIMEOUT, }) +function validateQueryInputs(parameters: Record) { + for (let entry of Object.entries(parameters)) { + const [key, value] = entry + if (typeof value !== "string") { + continue + } + if (findHBSBlocks(value).length !== 0) { + throw new Error( + `Parameter '${key}' input contains a handlebars binding - this is not allowed.` + ) + } + } +} + export async function fetch(ctx: UserCtx) { ctx.body = await sdk.queries.fetch() } @@ -123,10 +136,10 @@ function getAuthConfig(ctx: UserCtx) { function enrichParameters( queryParameters: QueryParameter[], - requestParameters: { [key: string]: string } = {} -): { - [key: string]: string -} { + requestParameters: Record = {} +): Record { + // first check parameters are all valid + validateQueryInputs(requestParameters) // make sure parameters are fully enriched with defaults for (let parameter of queryParameters) { if (!requestParameters[parameter.name]) { diff --git a/packages/server/src/api/routes/tests/queries/query.seq.spec.ts b/packages/server/src/api/routes/tests/queries/query.seq.spec.ts index 86df60899c..0ca424c48b 100644 --- a/packages/server/src/api/routes/tests/queries/query.seq.spec.ts +++ b/packages/server/src/api/routes/tests/queries/query.seq.spec.ts @@ -411,6 +411,21 @@ describe("/queries", () => { }, }) }) + + it("shouldn't allow handlebars to be passed as parameters", async () => { + const res = await request + .post(`/api/queries/${query._id}`) + .send({ + parameters: { + a: "{{ 'test' }}", + }, + }) + .set(config.defaultHeaders()) + .expect(400) + expect(res.body.message).toEqual( + "Parameter 'a' input contains a handlebars binding - this is not allowed." + ) + }) }) describe("variables", () => { diff --git a/packages/types/src/api/web/query.ts b/packages/types/src/api/web/query.ts index 3959cdea19..66f0248647 100644 --- a/packages/types/src/api/web/query.ts +++ b/packages/types/src/api/web/query.ts @@ -11,7 +11,7 @@ export interface PreviewQueryResponse { } export interface ExecuteQueryRequest { - parameters?: { [key: string]: string } + parameters?: Record pagination?: any }