diff --git a/.prettierignore b/.prettierignore
index b1ee287391..b0f9f8cdbf 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -9,4 +9,5 @@ packages/backend-core/coverage
packages/builder/.routify
packages/sdk/sdk
packages/pro/coverage
-**/*.ivm.bundle.js
\ No newline at end of file
+**/*.ivm.bundle.js
+!**/bson-polyfills.ivm.bundle.js
\ No newline at end of file
diff --git a/README.md b/README.md
index 26ad9f80c2..552a849581 100644
--- a/README.md
+++ b/README.md
@@ -54,17 +54,21 @@
+
## ✨ Features
-### Build and ship real software
+### Build and ship real software
+
Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing users with a great experience.
### Open source and extensible
+
Budibase is open-source - licensed as GPL v3. This should fill you with confidence that Budibase will always be around. You can also code against Budibase or fork it and make changes as you please, providing a developer-friendly experience.
### Load data or start from scratch
+
Budibase pulls data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MariaDB, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
@@ -82,10 +86,12 @@ Budibase comes out of the box with beautifully designed, powerful components whi
### Automate processes, integrate with other tools and connect to webhooks
+
Save time by automating manual processes and workflows. From connecting to webhooks to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [Request new automation](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
### Integrate with your favorite tools
+
Budibase integrates with a number of popular tools allowing you to build apps that perfectly fit your stack.
@@ -94,6 +100,7 @@ Budibase integrates with a number of popular tools allowing you to build apps th
### Deploy with confidence and security
+
Budibase is made to scale. With Budibase, you can self-host on your own infrastructure and globally manage users, onboarding, SMTP, apps, groups, theming and more. You can also provide users/groups with an app portal and disseminate user management to the group manager.
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
@@ -104,15 +111,15 @@ Budibase is made to scale. With Budibase, you can self-host on your own infrastr
-
## Budibase Public API
+
As with anything that we build in Budibase, our new public API is simple to use, flexible, and introduces new extensibility. To summarize, the Budibase API enables:
- Budibase as a backend
- Interoperability
-
#### Docs
+
You can learn more about the Budibase API at the following places:
- [General documentation](https://docs.budibase.com/docs/public-api): Learn how to get your API key, how to use spec, and how to use Postman
@@ -132,10 +139,8 @@ Deploy Budibase using Docker, Kubernetes, and Digital Ocean on your existing inf
- [Digital Ocean](https://docs.budibase.com/docs/digitalocean)
- [Portainer](https://docs.budibase.com/docs/portainer)
-
### [Get started with Budibase Cloud](https://budibase.com)
-
## 🎓 Learning Budibase
@@ -143,7 +148,6 @@ Deploy Budibase using Docker, Kubernetes, and Digital Ocean on your existing inf
The Budibase documentation [lives here](https://docs.budibase.com/docs).
-
## 💬 Community
@@ -152,25 +156,24 @@ If you have a question or would like to talk with other Budibase users and join
-
## ❗ Code of conduct
Budibase is dedicated to providing everyone a welcoming, diverse, and harassment-free experience. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/docs/CODE_OF_CONDUCT.md). Please read it.
-
-
## 🙌 Contributing to Budibase
From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API, please create an issue first. This way, we can ensure your work is not in vain.
Environment setup instructions are available [here](https://github.com/Budibase/budibase/tree/HEAD/docs/CONTRIBUTING.md).
### Not Sure Where to Start?
-A good place to start contributing is the [First time issues project](https://github.com/Budibase/budibase/projects/22).
+
+A good place to start contributing is by looking for the [good first issue](https://github.com/Budibase/budibase/labels/good%20first%20issue) tag.
### How the repository is organized
+
Budibase is a monorepo managed by lerna. Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up Budibase.
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contains code for the budibase builder client-side svelte application.
@@ -183,7 +186,6 @@ For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase
-
## 📝 License
Budibase is open-source, licensed as [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). The client and component libraries are licensed as [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - so the apps you build can be licensed however you like.
@@ -202,7 +204,6 @@ If you are having issues between updates of the builder, please use the guide [h
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
-
diff --git a/eslint-local-rules/index.js b/eslint-local-rules/index.js
index 9348706399..d9d894c33e 100644
--- a/eslint-local-rules/index.js
+++ b/eslint-local-rules/index.js
@@ -41,12 +41,11 @@ module.exports = {
if (
/^@budibase\/[^/]+\/.*$/.test(importPath) &&
importPath !== "@budibase/backend-core/tests" &&
- importPath !== "@budibase/string-templates/test/utils" &&
- importPath !== "@budibase/client/manifest.json"
+ importPath !== "@budibase/string-templates/test/utils"
) {
context.report({
node,
- message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests, @budibase/string-templates/test/utils and @budibase/client/manifest.json.`,
+ message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests and @budibase/string-templates/test/utils.`,
})
}
},
diff --git a/lerna.json b/lerna.json
index 8d7460f053..730d145ced 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
- "version": "3.3.3",
+ "version": "3.4.4",
"npmClient": "yarn",
"concurrency": 20,
"command": {
diff --git a/packages/backend-core/src/docIds/params.ts b/packages/backend-core/src/docIds/params.ts
index 016604b69b..5f1c053bde 100644
--- a/packages/backend-core/src/docIds/params.ts
+++ b/packages/backend-core/src/docIds/params.ts
@@ -83,11 +83,15 @@ export function isViewId(id: string): boolean {
/**
* Check if a given ID is that of a datasource or datasource plus.
*/
-export const isDatasourceId = (id: string): boolean => {
+export function isDatasourceId(id: string): boolean {
// this covers both datasources and datasource plus
return !!id && id.startsWith(`${DocumentType.DATASOURCE}${SEPARATOR}`)
}
+export function isQueryId(id: string): boolean {
+ return !!id && id.startsWith(`${DocumentType.QUERY}${SEPARATOR}`)
+}
+
/**
* Gets parameters for retrieving workspaces.
*/
diff --git a/packages/backend-core/src/queue/inMemoryQueue.ts b/packages/backend-core/src/queue/inMemoryQueue.ts
index dd8d3daa37..6833c9a306 100644
--- a/packages/backend-core/src/queue/inMemoryQueue.ts
+++ b/packages/backend-core/src/queue/inMemoryQueue.ts
@@ -1,45 +1,58 @@
import events from "events"
import { newid } from "../utils"
import { Queue, QueueOptions, JobOptions } from "./queue"
+import { helpers } from "@budibase/shared-core"
+import { Job, JobId, JobInformation } from "bull"
-interface JobMessage {
+function jobToJobInformation(job: Job): JobInformation {
+ let cron = ""
+ let every = -1
+ let tz: string | undefined = undefined
+ let endDate: number | undefined = undefined
+
+ const repeat = job.opts?.repeat
+ if (repeat) {
+ endDate = repeat.endDate ? new Date(repeat.endDate).getTime() : Date.now()
+ tz = repeat.tz
+ if ("cron" in repeat) {
+ cron = repeat.cron
+ } else {
+ every = repeat.every
+ }
+ }
+
+ return {
+ id: job.id.toString(),
+ name: "",
+ key: job.id.toString(),
+ tz,
+ endDate,
+ cron,
+ every,
+ next: 0,
+ }
+}
+
+interface JobMessage extends Partial> {
id: string
timestamp: number
- queue: string
+ queue: Queue
data: any
opts?: JobOptions
}
/**
- * Bull works with a Job wrapper around all messages that contains a lot more information about
- * the state of the message, this object constructor implements the same schema of Bull jobs
- * for the sake of maintaining API consistency.
- * @param queue The name of the queue which the message will be carried on.
- * @param message The JSON message which will be passed back to the consumer.
- * @returns A new job which can now be put onto the queue, this is mostly an
- * internal structure so that an in memory queue can be easily swapped for a Bull queue.
- */
-function newJob(queue: string, message: any, opts?: JobOptions): JobMessage {
- return {
- id: newid(),
- timestamp: Date.now(),
- queue: queue,
- data: message,
- opts,
- }
-}
-
-/**
- * This is designed to replicate Bull (https://github.com/OptimalBits/bull) in memory as a sort of mock.
- * It is relatively simple, using an event emitter internally to register when messages are available
- * to the consumers - in can support many inputs and many consumers.
+ * This is designed to replicate Bull (https://github.com/OptimalBits/bull) in
+ * memory as a sort of mock. It is relatively simple, using an event emitter
+ * internally to register when messages are available to the consumers - in can
+ * support many inputs and many consumers.
*/
class InMemoryQueue implements Partial {
_name: string
_opts?: QueueOptions
_messages: JobMessage[]
_queuedJobIds: Set
- _emitter: NodeJS.EventEmitter
+ _emitter: NodeJS.EventEmitter<{ message: [JobMessage]; completed: [Job] }>
_runCount: number
_addCount: number
@@ -69,34 +82,29 @@ class InMemoryQueue implements Partial {
*/
async process(concurrencyOrFunc: number | any, func?: any) {
func = typeof concurrencyOrFunc === "number" ? func : concurrencyOrFunc
- this._emitter.on("message", async () => {
- if (this._messages.length <= 0) {
- return
- }
- let msg = this._messages.shift()
-
- let resp = func(msg)
+ this._emitter.on("message", async message => {
+ let resp = func(message)
async function retryFunc(fnc: any) {
try {
await fnc
} catch (e: any) {
- await new Promise(r => setTimeout(() => r(), 50))
-
- await retryFunc(func(msg))
+ await helpers.wait(50)
+ await retryFunc(func(message))
}
}
if (resp.then != null) {
try {
await retryFunc(resp)
+ this._emitter.emit("completed", message as Job)
} catch (e: any) {
console.error(e)
}
}
this._runCount++
- const jobId = msg?.opts?.jobId?.toString()
- if (jobId && msg?.opts?.removeOnComplete) {
+ const jobId = message.opts?.jobId?.toString()
+ if (jobId && message.opts?.removeOnComplete) {
this._queuedJobIds.delete(jobId)
}
})
@@ -130,9 +138,16 @@ class InMemoryQueue implements Partial {
}
const pushMessage = () => {
- this._messages.push(newJob(this._name, data, opts))
+ const message: JobMessage = {
+ id: newid(),
+ timestamp: Date.now(),
+ queue: this as unknown as Queue,
+ data,
+ opts,
+ }
+ this._messages.push(message)
this._addCount++
- this._emitter.emit("message")
+ this._emitter.emit("message", message)
}
const delay = opts?.delay
@@ -158,13 +173,6 @@ class InMemoryQueue implements Partial {
console.log(cronJobId)
}
- /**
- * Implemented for tests
- */
- async getRepeatableJobs() {
- return []
- }
-
async removeJobs(_pattern: string) {
// no-op
}
@@ -176,13 +184,31 @@ class InMemoryQueue implements Partial {
return []
}
- async getJob() {
+ async getJob(id: JobId) {
+ for (const message of this._messages) {
+ if (message.id === id) {
+ return message as Job
+ }
+ }
return null
}
- on() {
- // do nothing
- return this as any
+ on(event: string, callback: (...args: any[]) => void): Queue {
+ // @ts-expect-error - this callback can be one of many types
+ this._emitter.on(event, callback)
+ return this as unknown as Queue
+ }
+
+ async count() {
+ return this._messages.length
+ }
+
+ async getCompletedCount() {
+ return this._runCount
+ }
+
+ async getRepeatableJobs() {
+ return this._messages.map(job => jobToJobInformation(job as Job))
}
}
diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts
index 334f1efdd4..7791ecb28b 100644
--- a/packages/backend-core/src/sql/sql.ts
+++ b/packages/backend-core/src/sql/sql.ts
@@ -388,7 +388,7 @@ class InternalBuilder {
}
}
- if (typeof input === "string") {
+ if (typeof input === "string" && schema.type === FieldType.DATETIME) {
if (isInvalidISODateString(input)) {
return null
}
diff --git a/packages/backend-core/tests/core/utilities/mocks/licenses.ts b/packages/backend-core/tests/core/utilities/mocks/licenses.ts
index 5ba6fb36a1..436e915b81 100644
--- a/packages/backend-core/tests/core/utilities/mocks/licenses.ts
+++ b/packages/backend-core/tests/core/utilities/mocks/licenses.ts
@@ -1,5 +1,12 @@
-import { Feature, License, Quotas } from "@budibase/types"
+import {
+ Feature,
+ License,
+ MonthlyQuotaName,
+ QuotaType,
+ QuotaUsageType,
+} from "@budibase/types"
import cloneDeep from "lodash/cloneDeep"
+import merge from "lodash/merge"
let CLOUD_FREE_LICENSE: License
let UNLIMITED_LICENSE: License
@@ -27,18 +34,19 @@ export function initInternal(opts: {
export interface UseLicenseOpts {
features?: Feature[]
- quotas?: Quotas
+ monthlyQuotas?: [MonthlyQuotaName, number][]
}
// LICENSES
export const useLicense = (license: License, opts?: UseLicenseOpts) => {
- if (opts) {
- if (opts.features) {
- license.features.push(...opts.features)
- }
- if (opts.quotas) {
- license.quotas = opts.quotas
+ if (opts?.features) {
+ license.features.push(...opts.features)
+ }
+ if (opts?.monthlyQuotas) {
+ for (const [name, value] of opts.monthlyQuotas) {
+ license.quotas[QuotaType.USAGE][QuotaUsageType.MONTHLY][name].value =
+ value
}
}
@@ -57,12 +65,9 @@ export const useCloudFree = () => {
// FEATURES
-const useFeature = (feature: Feature) => {
+const useFeature = (feature: Feature, extra?: Partial) => {
const license = cloneDeep(getCachedLicense() || UNLIMITED_LICENSE)
- const opts: UseLicenseOpts = {
- features: [feature],
- }
-
+ const opts: UseLicenseOpts = merge({ features: [feature] }, extra)
return useLicense(license, opts)
}
@@ -102,8 +107,12 @@ export const useAppBuilders = () => {
return useFeature(Feature.APP_BUILDERS)
}
-export const useBudibaseAI = () => {
- return useFeature(Feature.BUDIBASE_AI)
+export const useBudibaseAI = (opts?: { monthlyQuota?: number }) => {
+ return useFeature(Feature.BUDIBASE_AI, {
+ monthlyQuotas: [
+ [MonthlyQuotaName.BUDIBASE_AI_CREDITS, opts?.monthlyQuota || 1000],
+ ],
+ })
}
export const useAICustomConfigs = () => {
diff --git a/packages/bbui/package.json b/packages/bbui/package.json
index 2caad20bf6..6809f1ffa5 100644
--- a/packages/bbui/package.json
+++ b/packages/bbui/package.json
@@ -80,7 +80,7 @@
"dayjs": "^1.10.8",
"easymde": "^2.16.1",
"svelte-dnd-action": "^0.9.8",
- "svelte-portal": "^1.0.0"
+ "svelte-portal": "^2.2.1"
},
"resolutions": {
"loader-utils": "1.4.1"
diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte
index 2401354fbb..633023a94a 100644
--- a/packages/bbui/src/ActionButton/ActionButton.svelte
+++ b/packages/bbui/src/ActionButton/ActionButton.svelte
@@ -1,25 +1,25 @@
-
+
+
+
+
+
diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte
index c47840ea83..1e951b00cb 100644
--- a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte
+++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte
@@ -19,7 +19,7 @@
$: success = !error && !empty
$: highlightedResult = highlight(expressionResult)
$: highlightedLogs = expressionLogs.map(l => ({
- log: highlight(l.log.join(", ")),
+ log: l.log.map(part => highlight(part)).join(", "),
line: l.line,
type: l.type,
}))
@@ -95,7 +95,9 @@
{#if empty}
Your expression will be evaluated here
{:else if error}
- {formatError(expressionError)}
+
+ {formatError(expressionError)}
+
{:else}
{#each highlightedLogs as logLine}
@@ -118,13 +120,17 @@
{@html logLine.log}
{#if logLine.line}
- :{logLine.line}
+ :{logLine.line}
{/if}
{/each}
-
- {@html highlightedResult}
+
+
+ {@html highlightedResult}
+
{/if}
@@ -169,29 +175,33 @@
.header.error::before {
background: var(--error-bg);
}
+ .error-msg {
+ padding-top: var(--spacing-m);
+ }
.body {
flex: 1 1 auto;
- padding: var(--spacing-m) var(--spacing-l);
font-family: var(--font-mono);
+ margin: 0 var(--spacing-m);
font-size: 12px;
overflow-y: auto;
overflow-x: hidden;
- white-space: pre-line;
- word-wrap: break-word;
+ word-wrap: anywhere;
height: 0;
}
.output-lines {
display: flex;
flex-direction: column;
- gap: var(--spacing-xs);
}
.line {
- border-bottom: var(--border-light);
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: end;
- padding: var(--spacing-s);
+ padding: var(--spacing-m) 0;
+ word-wrap: anywhere;
+ }
+ .line:not(:first-of-type) {
+ border-top: var(--border-light);
}
.icon-log {
display: flex;
diff --git a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/GridColumnConfiguration.svelte b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/GridColumnConfiguration.svelte
index 46aea2a6c4..fb3856d517 100644
--- a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/GridColumnConfiguration.svelte
+++ b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/GridColumnConfiguration.svelte
@@ -61,6 +61,7 @@
anchor={primaryDisplayColumnAnchor}
item={columns.primary}
on:change={e => columns.update(e.detail)}
+ {bindings}
/>
diff --git a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/PrimaryColumnFieldSetting.svelte b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/PrimaryColumnFieldSetting.svelte
index c4c2b3eafb..b9f7ab976b 100644
--- a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/PrimaryColumnFieldSetting.svelte
+++ b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/PrimaryColumnFieldSetting.svelte
@@ -8,6 +8,7 @@
export let item
export let anchor
+ export let bindings
let draggableStore = writable({
selected: null,
@@ -48,6 +49,7 @@
componentInstance={item}
{parseSettings}
on:change
+ {bindings}
>
diff --git a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.js b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.js
index d3f081756d..638c41e0ec 100644
--- a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.js
+++ b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.js
@@ -69,6 +69,7 @@ const toGridFormat = draggableListColumns => {
active: entry.active,
width: entry.width,
conditions: entry.conditions,
+ format: entry.format,
}))
}
@@ -85,6 +86,7 @@ const toDraggableListFormat = (gridFormatColumns, createComponent, schema) => {
columnType: column.columnType || schema[column.field].type,
width: column.width,
conditions: column.conditions,
+ format: column.format,
},
{}
)
diff --git a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte
index 65b3ed9395..8ba32c47cf 100644
--- a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte
+++ b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte
@@ -5,7 +5,6 @@
runtimeToReadableBinding,
} from "@/dataBinding"
import { builderStore } from "@/stores/builder"
- import { onDestroy } from "svelte"
export let label = ""
export let labelHidden = false
@@ -26,16 +25,16 @@
export let wide
let highlightType
+ let domElement
$: highlightedProp = $builderStore.highlightedSetting
$: allBindings = getAllBindings(bindings, componentBindings, nested)
$: safeValue = getSafeValue(value, defaultValue, allBindings)
$: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
- $: if (!Array.isArray(value)) {
- highlightType =
- highlightedProp?.key === key ? `highlighted-${highlightedProp?.type}` : ""
- }
+ $: isHighlighted = highlightedProp?.key === key
+
+ $: highlightType = isHighlighted ? `highlighted-${highlightedProp?.type}` : ""
const getAllBindings = (bindings, componentBindings, nested) => {
if (!nested) {
@@ -76,14 +75,18 @@
: enriched
}
- onDestroy(() => {
- if (highlightedProp) {
- builderStore.highlightSetting(null)
- }
- })
+ function scrollToElement(element) {
+ element?.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ })
+ }
+
+ $: highlightedProp && isHighlighted && scrollToElement(domElement)
+ import { onMount } from "svelte"
+
+ import { screenStore } from "@/stores/builder"
+ import ScreensPopover from "@/components/common/ScreensPopover.svelte"
+ import type { ScreenUsage } from "@budibase/types"
+
+ export let sourceId: string
+
+ let screens: ScreenUsage[] = []
+ let popover: any
+
+ export function show() {
+ popover?.show()
+ }
+
+ export function hide() {
+ popover?.hide()
+ }
+
+ onMount(async () => {
+ let response = await screenStore.usageInScreens(sourceId)
+ screens = response?.screens
+ })
+
+
+
diff --git a/packages/builder/src/components/integration/QueryViewer.svelte b/packages/builder/src/components/integration/QueryViewer.svelte
index 0f2ed24177..7a1410e53e 100644
--- a/packages/builder/src/components/integration/QueryViewer.svelte
+++ b/packages/builder/src/components/integration/QueryViewer.svelte
@@ -23,6 +23,7 @@
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
import QueryViewerSavePromptModal from "./QueryViewerSavePromptModal.svelte"
import { Utils } from "@budibase/frontend-core"
+ import ConnectedQueryScreens from "./ConnectedQueryScreens.svelte"
export let query
let queryHash
@@ -170,6 +171,7 @@