This commit is contained in:
Martin McKeaveney 2021-09-09 11:15:21 +01:00
commit e37e6af4f6
35 changed files with 411 additions and 135 deletions

15
SECURITY.md Normal file
View File

@ -0,0 +1,15 @@
# Security Policy
## Versions
As an open source product, we will only patch the latest major version for security vulnerabilities. Previous versions of budibase will not be retroactively patched.
## Disclosing
You can get in touch with us regarding a vulnerability via email at community@budibase.com.
You can also disclose via huntr.dev. If you believe you have found a vulnerability, please disclose it on huntr and let us know.
https://huntr.dev/bounties/disclose
This will enable us to review the vulnerability and potentially reward you for your work.

View File

@ -33,11 +33,19 @@ static_resources:
route:
cluster: app-service
# special case for worker admin API
# special cases for worker admin (deprecated), global and system API
- match: { prefix: "/api/global/" }
route:
cluster: worker-service
- match: { prefix: "/api/admin/" }
route:
cluster: worker-service
- match: { prefix: "/api/system/" }
route:
cluster: worker-service
- match: { path: "/" }
route:
cluster: app-service

View File

@ -1,5 +1,5 @@
{
"version": "0.9.121",
"version": "0.9.123-alpha.1",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -20,7 +20,6 @@
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
"bootstrap": "lerna link && lerna bootstrap",
"build": "lerna run build",
"initialise": "lerna run initialise",
"publishdev": "lerna run publishdev",
"publishnpm": "yarn build && lerna publish --force-publish",
"release": "yarn build && lerna publish patch --yes --force-publish",

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/auth",
"version": "0.9.121",
"version": "0.9.123-alpha.1",
"description": "Authentication middlewares for budibase builder and apps",
"main": "src/index.js",
"author": "Budibase",

View File

@ -2,12 +2,13 @@ const { setTenantId } = require("../tenancy")
const ContextFactory = require("../tenancy/FunctionContext")
const { buildMatcherRegex, matches } = require("./matchers")
module.exports = (allowQueryStringPatterns, noTenancyPatterns) => {
module.exports = (allowQueryStringPatterns, noTenancyPatterns, opts = {}) => {
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns)
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns)
return ContextFactory.getMiddleware(ctx => {
const allowNoTenant = !!matches(ctx, noTenancyOptions)
const allowNoTenant =
opts.noTenancyRequired || !!matches(ctx, noTenancyOptions)
const allowQs = !!matches(ctx, allowQsOptions)
setTenantId(ctx, { allowQs, allowNoTenant })
})

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "0.9.121",
"version": "0.9.123-alpha.1",
"license": "AGPL-3.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",

View File

@ -3,6 +3,7 @@
import "@spectrum-css/table/dist/index-vars.css"
import CellRenderer from "./CellRenderer.svelte"
import SelectEditRenderer from "./SelectEditRenderer.svelte"
import { cloneDeep } from "lodash"
/**
* The expected schema is our normal couch schemas for our tables.
@ -197,7 +198,7 @@
const editRow = (e, row) => {
e.stopPropagation()
dispatch("editrow", row)
dispatch("editrow", cloneDeep(row))
}
const toggleSelectRow = row => {

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "0.9.121",
"version": "0.9.123-alpha.1",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@ -65,10 +65,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^0.9.121",
"@budibase/client": "^0.9.121",
"@budibase/bbui": "^0.9.123-alpha.1",
"@budibase/client": "^0.9.123-alpha.1",
"@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^0.9.121",
"@budibase/string-templates": "^0.9.123-alpha.1",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",

View File

@ -1,6 +1,10 @@
import { cloneDeep } from "lodash/fp"
import { get } from "svelte/store"
import { findComponent, findComponentPath } from "./storeUtils"
import {
findComponent,
findComponentPath,
findAllMatchingComponents,
} from "./storeUtils"
import { store } from "builderStore"
import { tables as tablesStore, queries as queriesStores } from "stores/backend"
import { makePropSafe } from "@budibase/string-templates"
@ -18,7 +22,9 @@ export const getBindableProperties = (asset, componentId) => {
const userBindings = getUserBindings()
const urlBindings = getUrlBindings(asset)
const deviceBindings = getDeviceBindings()
const stateBindings = getStateBindings()
return [
...stateBindings,
...deviceBindings,
...urlBindings,
...contextBindings,
@ -256,6 +262,18 @@ const getDeviceBindings = () => {
return bindings
}
/**
* Gets all state bindings that are globally available.
*/
const getStateBindings = () => {
const safeState = makePropSafe("state")
return getAllStateVariables().map(key => ({
type: "context",
runtimeBinding: `${safeState}.${makePropSafe(key)}`,
readableBinding: `State.${key}`,
}))
}
/**
* Gets all bindable properties from URL parameters.
*/
@ -458,3 +476,49 @@ export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
"readableBinding"
)
}
/**
* Returns an array of the keys of any state variables which are set anywhere
* in the app.
*/
export const getAllStateVariables = () => {
let allComponents = []
// Find all onClick settings in all layouts
get(store).layouts.forEach(layout => {
const components = findAllMatchingComponents(
layout.props,
c => c.onClick != null
)
allComponents = allComponents.concat(components || [])
})
// Find all onClick settings in all screens
get(store).screens.forEach(screen => {
const components = findAllMatchingComponents(
screen.props,
c => c.onClick != null
)
allComponents = allComponents.concat(components || [])
})
// Add state bindings for all state actions
let bindingSet = new Set()
allComponents.forEach(component => {
if (!Array.isArray(component.onClick)) {
return
}
component.onClick.forEach(action => {
if (
action["##eventHandlerType"] === "Update State" &&
action.parameters?.type === "set" &&
action.parameters?.key &&
action.parameters?.value
) {
bindingSet.add(action.parameters.key)
}
})
})
return Array.from(bindingSet)
}

View File

@ -18,12 +18,10 @@
let exportFormat = FORMATS[0].key
async function exportView() {
const filename = `export.${exportFormat}`
download(
`/api/views/export?view=${encodeURIComponent(
view
)}&format=${exportFormat}`,
filename
)}&format=${exportFormat}`
)
}
</script>

View File

@ -0,0 +1,65 @@
<script>
import { Select, Label, Combobox, Checkbox, Body } from "@budibase/bbui"
import { onMount } from "svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { getAllStateVariables } from "builderStore/dataBinding"
export let parameters
export let bindings = []
const keyOptions = getAllStateVariables()
const typeOptions = [
{
label: "Set value",
value: "set",
},
{
label: "Delete value",
value: "delete",
},
]
onMount(() => {
if (!parameters.type) {
parameters.type = "set"
}
})
</script>
<div class="root">
<Label small>Type</Label>
<Select
placeholder={null}
bind:value={parameters.type}
options={typeOptions}
/>
<Label small>Key</Label>
<Combobox bind:value={parameters.key} options={keyOptions} />
{#if parameters.type === "set"}
<Label small>Value</Label>
<DrawerBindableInput
{bindings}
value={parameters.value}
on:change={e => (parameters.value = e.detail)}
/>
<div />
<Checkbox bind:value={parameters.persist} text="Persist this value" />
<div />
<Body size="XS">
Persisted values will remain even after reloading the page or closing the
browser.
</Body>
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: 60px 1fr;
align-items: center;
max-width: 400px;
margin: 0 auto;
}
</style>

View File

@ -8,6 +8,7 @@ import LogOut from "./LogOut.svelte"
import ClearForm from "./ClearForm.svelte"
import CloseScreenModal from "./CloseScreenModal.svelte"
import ChangeFormStep from "./ChangeFormStep.svelte"
import UpdateStateStep from "./UpdateState.svelte"
// Defines which actions are available to configure in the front end.
// Unfortunately the "name" property is used as the identifier so please don't
@ -57,4 +58,8 @@ export default [
name: "Change Form Step",
component: ChangeFormStep,
},
{
name: "Update State",
component: UpdateStateStep,
},
]

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/cli",
"version": "0.9.121",
"version": "0.9.123-alpha.1",
"description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js",
"bin": {

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "0.9.121",
"version": "0.9.123-alpha.1",
"license": "MPL-2.0",
"module": "dist/budibase-client.js",
"main": "dist/budibase-client.js",
@ -18,9 +18,9 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/bbui": "^0.9.121",
"@budibase/standard-components": "^0.9.121",
"@budibase/string-templates": "^0.9.121",
"@budibase/bbui": "^0.9.123-alpha.1",
"@budibase/standard-components": "^0.9.123-alpha.1",
"@budibase/string-templates": "^0.9.123-alpha.1",
"regexparam": "^1.3.0",
"shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5"

View File

@ -112,16 +112,24 @@ export const enrichRows = async (rows, tableId) => {
if (!Array.isArray(rows)) {
return []
}
if (rows.length && tableId) {
if (rows.length) {
// map of tables, incase a row being loaded is not from the same table
const tables = {}
for (let row of rows) {
// fallback to passed in tableId if row doesn't have it specified
let rowTableId = row.tableId || tableId
let table = tables[rowTableId]
if (!table) {
// Fetch table schema so we can check column types
const tableDefinition = await fetchTableDefinition(tableId)
const schema = tableDefinition && tableDefinition.schema
table = await fetchTableDefinition(rowTableId)
tables[rowTableId] = table
}
const schema = table?.schema
if (schema) {
const keys = Object.keys(schema)
rows.forEach(row => {
for (let key of keys) {
const type = schema[key].type
if (type === "link") {
if (type === "link" && Array.isArray(row[key])) {
// Enrich row a string join of relationship fields
row[`${key}_text`] =
row[key]
@ -137,7 +145,7 @@ export const enrichRows = async (rows, tableId) => {
row[`${key}_first`] = url
}
}
})
}
}
}
return rows

View File

@ -22,6 +22,7 @@
import ErrorSVG from "../../../builder/assets/error.svg"
import UserBindingsProvider from "./UserBindingsProvider.svelte"
import DeviceBindingsProvider from "./DeviceBindingsProvider.svelte"
import StateBindingsProvider from "./StateBindingsProvider.svelte"
// Provide contexts
setContext("sdk", SDK)
@ -85,6 +86,7 @@
{:else if $screenStore.activeLayout}
<UserBindingsProvider>
<DeviceBindingsProvider>
<StateBindingsProvider>
<div id="app-root" class:preview={$builderStore.inBuilder}>
{#key $screenStore.activeLayout._id}
<Component instance={$screenStore.activeLayout.props} />
@ -107,6 +109,7 @@
<SelectionIndicator />
<HoverIndicator />
{/if}
</StateBindingsProvider>
</DeviceBindingsProvider>
</UserBindingsProvider>
{/if}

View File

@ -0,0 +1,8 @@
<script>
import Provider from "./Provider.svelte"
import { stateStore } from "../store"
</script>
<Provider key="state" data={$stateStore}>
<slot />
</Provider>

View File

@ -7,6 +7,7 @@ export { builderStore } from "./builder"
export { dataSourceStore } from "./dataSource"
export { confirmationStore } from "./confirmation"
export { peekStore } from "./peek"
export { stateStore } from "./state"
// Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context"

View File

@ -0,0 +1,54 @@
import { writable, get, derived } from "svelte/store"
import { localStorageStore } from "../../../builder/src/builderStore/store/localStorage"
import { appStore } from "./app"
const createStateStore = () => {
const localStorageKey = `${get(appStore).appId}.state`
const persistentStore = localStorageStore(localStorageKey, {})
// Initialise the temp store to mirror the persistent store
const tempStore = writable(get(persistentStore))
// Sets a value to state, optionally persistent
const setValue = (key, value, persist = false) => {
const storeToSave = persist ? persistentStore : tempStore
const storeToClear = persist ? tempStore : persistentStore
storeToSave.update(state => {
state[key] = value
return state
})
storeToClear.update(state => {
delete state[key]
return state
})
}
// Delete a certain key from both stores
const deleteValue = key => {
const stores = [tempStore, persistentStore]
stores.forEach(store => {
store.update(state => {
delete state[key]
return state
})
})
}
// Derive the combination of both persisted and non persisted stores
const store = derived(
[tempStore, persistentStore],
([$tempStore, $persistentStore]) => {
return {
...$tempStore,
...$persistentStore,
}
}
)
return {
subscribe: store.subscribe,
actions: { setValue, deleteValue },
}
}
export const stateStore = createStateStore()

View File

@ -5,6 +5,7 @@ import {
confirmationStore,
authStore,
peekStore,
stateStore,
} from "../store"
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
import { ActionTypes } from "../constants"
@ -122,6 +123,15 @@ const closeScreenModalHandler = () => {
window.dispatchEvent(new Event("close-screen-modal"))
}
const updateStateHandler = action => {
const { type, key, value, persist } = action.parameters
if (type === "set") {
stateStore.actions.setValue(key, value, persist)
} else if (type === "delete") {
stateStore.actions.deleteValue(key)
}
}
const handlerMap = {
["Save Row"]: saveRowHandler,
["Delete Row"]: deleteRowHandler,
@ -134,6 +144,7 @@ const handlerMap = {
["Clear Form"]: clearFormHandler,
["Close Screen Modal"]: closeScreenModalHandler,
["Change Form Step"]: changeFormStepHandler,
["Update State"]: updateStateHandler,
}
const confirmTextMap = {

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "0.9.121",
"version": "0.9.123-alpha.1",
"description": "Budibase Web Server",
"main": "src/index.js",
"repository": {
@ -23,7 +23,6 @@
"format": "prettier --config ../../.prettierrc.json 'src/**/*.ts' --write",
"lint": "eslint --fix src/",
"lint:fix": "yarn run format && yarn run lint",
"initialise": "node scripts/initialise.js",
"multi:enable": "node scripts/multiTenancy.js enable",
"multi:disable": "node scripts/multiTenancy.js disable"
},
@ -62,9 +61,9 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/auth": "^0.9.121",
"@budibase/client": "^0.9.121",
"@budibase/string-templates": "^0.9.121",
"@budibase/auth": "^0.9.123-alpha.1",
"@budibase/client": "^0.9.123-alpha.1",
"@budibase/string-templates": "^0.9.123-alpha.1",
"@elastic/elasticsearch": "7.10.0",
"@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1",
@ -117,7 +116,7 @@
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/preset-env": "^7.14.4",
"@budibase/standard-components": "^0.9.121",
"@budibase/standard-components": "^0.9.123-alpha.1",
"@jest/test-sequencer": "^24.8.0",
"@types/bull": "^3.15.1",
"@types/jest": "^26.0.23",

View File

@ -6,8 +6,16 @@ import {
SearchFilters,
SortJson,
} from "../../../definitions/datasource"
import {Datasource, FieldSchema, Row, Table} from "../../../definitions/common"
import {breakRowIdField, generateRowIdField} from "../../../integrations/utils"
import {
Datasource,
FieldSchema,
Row,
Table,
} from "../../../definitions/common"
import {
breakRowIdField,
generateRowIdField,
} from "../../../integrations/utils"
import { RelationshipTypes } from "../../../constants"
interface ManyRelationship {
@ -348,7 +356,7 @@ module External {
* information.
*/
async lookupRelations(tableId: string, row: Row) {
const related: {[key: string]: any} = {}
const related: { [key: string]: any } = {}
const { tableName } = breakExternalTableId(tableId)
const table = this.tables[tableName]
// @ts-ignore
@ -387,7 +395,11 @@ module External {
* isn't supposed to exist anymore and delete those. This is better than the usual method of delete them
* all and then re-create, as theres no chance of losing data (e.g. delete succeed, but write fail).
*/
async handleManyRelationships(mainTableId: string, row: Row, relationships: ManyRelationship[]) {
async handleManyRelationships(
mainTableId: string,
row: Row,
relationships: ManyRelationship[]
) {
const { appId } = this
// if we're creating (in a through table) need to wipe the existing ones first
const promises = []
@ -399,8 +411,10 @@ module External {
// @ts-ignore
const linkPrimary = linkTable.primary[0]
const rows = related[key].rows || []
const found = rows.find((row: { [key: string]: any }) =>
row[linkPrimary] === relationship.id || row[linkPrimary] === body[linkPrimary]
const found = rows.find(
(row: { [key: string]: any }) =>
row[linkPrimary] === relationship.id ||
row[linkPrimary] === body[linkPrimary]
)
const operation = isUpdate
? DataSourceOperation.UPDATE
@ -420,13 +434,17 @@ module External {
}
}
// finally cleanup anything that needs to be removed
for (let [colName, {isMany, rows, tableId}] of Object.entries(related)) {
for (let [colName, { isMany, rows, tableId }] of Object.entries(
related
)) {
const table = this.getTable(tableId)
for (let row of rows) {
const filters = buildFilters(generateIdForRow(row, table), {}, table)
// safety check, if there are no filters on deletion bad things happen
if (Object.keys(filters).length !== 0) {
const op = isMany ? DataSourceOperation.DELETE : DataSourceOperation.UPDATE
const op = isMany
? DataSourceOperation.DELETE
: DataSourceOperation.UPDATE
const body = isMany ? null : { [colName]: null }
promises.push(
makeExternalQuery(this.appId, {
@ -448,7 +466,10 @@ module External {
* Creating the specific list of fields that we desire, and excluding the ones that are no use to us
* is more performant and has the added benefit of protecting against this scenario.
*/
buildFields(table: Table, includeRelations: IncludeRelationships = IncludeRelationships.INCLUDE) {
buildFields(
table: Table,
includeRelations: IncludeRelationships = IncludeRelationships.INCLUDE
) {
function extractNonLinkFieldNames(table: Table, existing: string[] = []) {
return Object.entries(table.schema)
.filter(
@ -523,7 +544,10 @@ module External {
// can't really use response right now
const response = await makeExternalQuery(appId, json)
// handle many to many relationships now if we know the ID (could be auto increment)
if (operation !== DataSourceOperation.READ && processed.manyRelationships) {
if (
operation !== DataSourceOperation.READ &&
processed.manyRelationships
) {
await this.handleManyRelationships(
table._id || "",
response[0],

View File

@ -10,27 +10,6 @@ const env = require("../environment")
const router = new Router()
const NO_TENANCY_ENDPOINTS = [
{
route: "/api/analytics",
method: "GET",
},
{
route: "/builder",
method: "GET",
},
// when using this locally there can be pass through, need
// to allow all pass through endpoints to go without tenancy
{
route: "/api/global",
method: "ALL",
},
{
route: "/api/system",
method: "ALL",
},
]
router
.use(
compress({
@ -53,13 +32,21 @@ router
})
.use("/health", ctx => (ctx.status = 200))
.use("/version", ctx => (ctx.body = pkg.version))
// re-direct before any middlewares occur
.redirect("/", "/builder")
.use(
buildAuthMiddleware(null, {
publicAllowed: true,
})
)
// nothing in the server should allow query string tenants
.use(buildTenancyMiddleware(null, NO_TENANCY_ENDPOINTS))
// the server can be public anywhere, so nowhere should throw errors
// if the tenancy has not been set, it'll have to be discovered at application layer
.use(
buildTenancyMiddleware(null, null, {
noTenancyRequired: true,
})
)
.use(currentApp)
.use(auditLog)
@ -93,7 +80,4 @@ for (let route of mainRoutes) {
router.use(staticRoutes.routes())
router.use(staticRoutes.allowedMethods())
// add a redirect for when hitting server directly
router.redirect("/", "/builder")
module.exports = router

View File

@ -203,19 +203,17 @@ exports.attachFullLinkedDocs = async (ctx, table, rows) => {
exports.squashLinksToPrimaryDisplay = async (appId, table, enriched) => {
const db = new CouchDB(appId)
// will populate this as we find them
const linkedTables = []
for (let [column, schema] of Object.entries(table.schema)) {
if (schema.type !== FieldTypes.LINK) {
continue
}
const linkedTables = [table]
for (let row of enriched) {
if (!row[column] || !row[column].length) {
// this only fetches the table if its not already in array
const rowTable = await getLinkedTable(db, row.tableId, linkedTables)
for (let [column, schema] of Object.entries(rowTable.schema)) {
if (schema.type !== FieldTypes.LINK || !Array.isArray(row[column])) {
continue
}
const newLinks = []
for (let link of row[column]) {
const linkTblId = link.tableId || getRelatedTableForField(table, column)
// this only fetches the table if its not already in array
const linkedTable = await getLinkedTable(db, linkTblId, linkedTables)
const obj = { _id: link._id }
if (link[linkedTable.primaryDisplay]) {

View File

@ -42,7 +42,7 @@ export enum SourceNames {
export enum IncludeRelationships {
INCLUDE = 1,
EXCLUDE = 0
EXCLUDE = 0,
}
export interface QueryDefinition {

View File

@ -943,10 +943,10 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/auth@^0.9.121":
version "0.9.121"
resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-0.9.121.tgz#509ba49ca542fa741424b9a1310cf8e16dc59eeb"
integrity sha512-SQbp6PsIg0C9IMFMBaFJ8UgJdantheOrCjphMCxpaq3rATl2MOsh9koyo5R/WMYYeBtdG7V+WKd4O5kBUtrfXQ==
"@budibase/auth@^0.9.123-alpha.1":
version "0.9.123-alpha.1"
resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-0.9.123-alpha.1.tgz#48e011b2afbf0133ebcd564416d0f2399a107b82"
integrity sha512-mI0rN0uwCCVjQiwfELjgIm2WpUHfDdzD84lSumRQ8HWAXywD94MdR/qe+B1r7YqjboXxC5SWydoiOivU7jhaTA==
dependencies:
"@techpass/passport-openidconnect" "^0.3.0"
aws-sdk "^2.901.0"
@ -966,10 +966,10 @@
uuid "^8.3.2"
zlib "^1.0.5"
"@budibase/bbui@^0.9.121":
version "0.9.121"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.121.tgz#d374f2856c8e335b0167440dbc2819671203be58"
integrity sha512-60zNUJ/Nil8yfWdKgjAo4Xxk3mbxipCpZBIgGl/RuE0r6Am0oylRx8pRMmwHaQ92cofbXwApWyC/9VLQQGSJzw==
"@budibase/bbui@^0.9.123-alpha.1":
version "0.9.123-alpha.1"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.123-alpha.1.tgz#704b693938efdb23eed9d1f089ad86a7f06f35d0"
integrity sha512-QGeR3f/xsalkRC3/HRUdG7VaNakYK9MCL3819M5bQxMjyhVoFdZw18mnc7zNfcCL3kTyw0C6capJULho4ic+AA==
dependencies:
"@adobe/spectrum-css-workflow-icons" "^1.2.1"
"@spectrum-css/actionbutton" "^1.0.1"
@ -1015,14 +1015,14 @@
svelte-flatpickr "^3.1.0"
svelte-portal "^1.0.0"
"@budibase/client@^0.9.121":
version "0.9.121"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.9.121.tgz#fe00d672eea51bd2c8e60c283332a072168971d1"
integrity sha512-1IMjIy2IuL5Y3GuIBgNe7ASGloA+As/CUBLtHQMe7oM8Q3FbRBOm7SKyV6FZGYXiG7hggq0fKJqm5YcRO9Lqgw==
"@budibase/client@^0.9.123-alpha.1":
version "0.9.123-alpha.1"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.9.123-alpha.1.tgz#a70199937c546544a2750e4273c8347e2fddab53"
integrity sha512-s/diiHoN/1kd2JavHy+otYDryfH3ijdjna/jqsZYPK/6Y9ZGcHwtVL43+VVtz9YlBw5aD4Av+YW2t1sqNHCsJw==
dependencies:
"@budibase/bbui" "^0.9.121"
"@budibase/standard-components" "^0.9.121"
"@budibase/string-templates" "^0.9.121"
"@budibase/bbui" "^0.9.123-alpha.1"
"@budibase/standard-components" "^0.9.123-alpha.1"
"@budibase/string-templates" "^0.9.123-alpha.1"
regexparam "^1.3.0"
shortid "^2.2.15"
svelte-spa-router "^3.0.5"
@ -1055,12 +1055,12 @@
to-gfm-code-block "^0.1.1"
year "^0.2.1"
"@budibase/standard-components@^0.9.121":
version "0.9.121"
resolved "https://registry.yarnpkg.com/@budibase/standard-components/-/standard-components-0.9.121.tgz#7950a83b1111310b700790a933f68ba9bbababb6"
integrity sha512-VPbJCisPprPUEVrkFn/yz+ZOTpAJQN+8PRo7gSWtuXLPlLXKdPZ+B8ICDQNA8i3QBswfFACoIZQ7QwEJ+kBJVg==
"@budibase/standard-components@^0.9.123-alpha.1":
version "0.9.123-alpha.1"
resolved "https://registry.yarnpkg.com/@budibase/standard-components/-/standard-components-0.9.123-alpha.1.tgz#944e4906e88ce9f8be6a26dc46d50afa17ef3a58"
integrity sha512-tCaiOhmmLAOBh+A8rFXnPHKZaSYRK7xlrI23r1IRgvDTkmuflr0o/VrRBVQuaxwWClKQPGkQ3wgnOJgXvk6CgQ==
dependencies:
"@budibase/bbui" "^0.9.121"
"@budibase/bbui" "^0.9.123-alpha.1"
"@spectrum-css/button" "^3.0.3"
"@spectrum-css/card" "^3.0.3"
"@spectrum-css/divider" "^1.0.3"
@ -1073,10 +1073,10 @@
svelte-apexcharts "^1.0.2"
svelte-flatpickr "^3.1.0"
"@budibase/string-templates@^0.9.121":
version "0.9.121"
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.9.121.tgz#bc16761e9571cd44748b414bda24a3830a867672"
integrity sha512-7j+o9D/qwP1lbclptu5tRLj5AzNRB6RyqAB13DkvfqCDxYkxK5qO3+Gdnm7eVZRguESwCcVEJHe+a/qVBUWhog==
"@budibase/string-templates@^0.9.123-alpha.1":
version "0.9.123-alpha.1"
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.9.123-alpha.1.tgz#6b2e718478d98aa8ba046ad23e7190894360906a"
integrity sha512-Id5AUdzTiYKGo/TTU3cuy1m4hS1Wy66hMIxVCM6GKoEnDinEB/rEA6IwP3IDFyTs2Uld8uWLBnEru93sP982AQ==
dependencies:
"@budibase/handlebars-helpers" "^0.11.4"
dayjs "^1.10.4"
@ -11353,9 +11353,9 @@ typescript@^4.3.5:
integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
uglify-js@^3.1.4:
version "3.14.1"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.1.tgz#e2cb9fe34db9cb4cf7e35d1d26dfea28e09a7d06"
integrity sha512-JhS3hmcVaXlp/xSo3PKY5R0JqKs5M3IV+exdLHW99qKvKivPO4Z8qbej6mte17SOPqAOVMjt/XGgWacnFSzM3g==
version "3.14.2"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.2.tgz#d7dd6a46ca57214f54a2d0a43cad0f35db82ac99"
integrity sha512-rtPMlmcO4agTUfz10CbgJ1k6UAoXM2gWb3GoMPPZB/+/Ackf8lNWk11K4rYi2D0apgoFRLtQOZhb+/iGNJq26A==
uid2@0.0.x:
version "0.0.4"

View File

@ -29,11 +29,11 @@
"keywords": [
"svelte"
],
"version": "0.9.121",
"version": "0.9.123-alpha.1",
"license": "MIT",
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc",
"dependencies": {
"@budibase/bbui": "^0.9.121",
"@budibase/bbui": "^0.9.123-alpha.1",
"@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",

View File

@ -25,6 +25,7 @@
const labelPosition = fieldGroupContext?.labelPosition || "above"
const formField = formApi?.registerField(
field,
type,
defaultValue,
disabled,
validation,

View File

@ -26,6 +26,7 @@
// Reactive derived stores to derive form state from field array
$: values = deriveFieldProperty(fields, f => f.fieldState.value)
$: errors = deriveFieldProperty(fields, f => f.fieldState.error)
$: enrichments = deriveBindingEnrichments(fields)
$: valid = !Object.values($errors).some(error => error != null)
// Derive whether the current form step is valid
@ -57,6 +58,26 @@
})
}
// Derives any enrichments which need to be made so that bindings work for
// special data types like attachments. Relationships are currently not
// handled as we don't have the primaryDisplay field that is required.
const deriveBindingEnrichments = fieldStores => {
return derived(fieldStores, fieldValues => {
let enrichments = {}
fieldValues.forEach(field => {
if (field.type === "attachment") {
const value = field.fieldState.value
let url = null
if (Array.isArray(value) && value[0] != null) {
url = value[0].url
}
enrichments[`${field.name}_first`] = url
}
})
return enrichments
})
}
// Searches the field array for a certain field
const getField = name => {
return fields.find(field => get(field).name === name)
@ -65,6 +86,7 @@
const formApi = {
registerField: (
field,
type,
defaultValue = null,
fieldDisabled = false,
validationRules,
@ -100,6 +122,7 @@
// Construct field info
const fieldInfo = writable({
name: field,
type,
step: step || 1,
fieldState: {
fieldId: `id-${generateID()}`,
@ -262,6 +285,7 @@
$: dataContext = {
...initialValues,
...$values,
...$enrichments,
// These static values are prefixed to avoid clashes with actual columns
__valid: valid,

View File

@ -105,12 +105,12 @@ export const luceneQuery = (docs, query) => {
// Process an equal match (fails if the value is different)
const equalMatch = match("equal", (key, value, doc) => {
return doc[key] !== value
return value != null && value !== "" && doc[key] !== value
})
// Process a not-equal match (fails if the value is the same)
const notEqualMatch = match("notEqual", (key, value, doc) => {
return doc[key] === value
return value != null && value !== "" && doc[key] === value
})
// Process an empty match (fails if the value is not empty)

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "0.9.121",
"version": "0.9.123-alpha.1",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs",
"module": "dist/bundle.mjs",

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/worker",
"email": "hi@budibase.com",
"version": "0.9.121",
"version": "0.9.123-alpha.1",
"description": "Budibase background service",
"main": "src/index.js",
"repository": {
@ -23,8 +23,8 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/auth": "^0.9.121",
"@budibase/string-templates": "^0.9.121",
"@budibase/auth": "^0.9.123-alpha.1",
"@budibase/string-templates": "^0.9.123-alpha.1",
"@koa/router": "^8.0.0",
"@techpass/passport-openidconnect": "^0.3.0",
"aws-sdk": "^2.811.0",

View File

@ -35,6 +35,7 @@ const PUBLIC_ENDPOINTS = [
method: "GET",
},
{
// TODO: Add an provisioning API key to this endpoint in the cloud
route: "/api/global/users/init",
method: "POST",
},
@ -46,6 +47,10 @@ const PUBLIC_ENDPOINTS = [
route: "api/system/flags",
method: "GET",
},
{
route: "/api/global/users/tenant/:id",
method: "GET",
},
]
const NO_TENANCY_ENDPOINTS = [

View File

@ -94,7 +94,7 @@ router
controller.adminUser
)
.get("/api/global/users/self", controller.getSelf)
.get("/api/global/users/tenant/:id", adminOnly, controller.tenantLookup)
.get("/api/global/users/tenant/:id", controller.tenantLookup)
// global endpoint but needs to come at end (blocks other endpoints otherwise)
.get("/api/global/users/:id", adminOnly, controller.find)