Merge remote-tracking branch 'origin/develop' into feature/portal-pending-users-section

This commit is contained in:
Dean 2023-05-02 14:45:31 +01:00
commit 41006d363a
77 changed files with 1086 additions and 234 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "2.5.6-alpha.30", "version": "2.5.6-alpha.42",
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"packages": ["packages/*"], "packages": ["packages/*"],

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "2.5.6-alpha.30", "version": "2.5.6-alpha.42",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js", "main": "dist/src/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
@ -24,7 +24,7 @@
"dependencies": { "dependencies": {
"@budibase/nano": "10.1.2", "@budibase/nano": "10.1.2",
"@budibase/pouchdb-replication-stream": "1.2.10", "@budibase/pouchdb-replication-stream": "1.2.10",
"@budibase/types": "2.5.6-alpha.30", "@budibase/types": "2.5.6-alpha.42",
"@shopify/jest-koa-mocks": "5.0.1", "@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2", "@techpass/passport-openidconnect": "0.3.2",
"aws-cloudfront-sign": "2.2.0", "aws-cloudfront-sign": "2.2.0",

View File

@ -3,7 +3,6 @@ import {
Event, Event,
LicenseActivatedEvent, LicenseActivatedEvent,
LicensePlanChangedEvent, LicensePlanChangedEvent,
LicenseTierChangedEvent,
PlanType, PlanType,
Account, Account,
LicensePortalOpenedEvent, LicensePortalOpenedEvent,
@ -11,22 +10,22 @@ import {
LicenseCheckoutOpenedEvent, LicenseCheckoutOpenedEvent,
LicensePaymentFailedEvent, LicensePaymentFailedEvent,
LicensePaymentRecoveredEvent, LicensePaymentRecoveredEvent,
PriceDuration,
} from "@budibase/types" } from "@budibase/types"
async function tierChanged(account: Account, from: number, to: number) { async function planChanged(
const properties: LicenseTierChangedEvent = { account: Account,
accountId: account.accountId, from: PlanType,
to, to: PlanType,
from, quantity: number | undefined,
} duration: PriceDuration | undefined
await publishEvent(Event.LICENSE_TIER_CHANGED, properties) ) {
}
async function planChanged(account: Account, from: PlanType, to: PlanType) {
const properties: LicensePlanChangedEvent = { const properties: LicensePlanChangedEvent = {
accountId: account.accountId, accountId: account.accountId,
to, to,
from, from,
quantity,
duration,
} }
await publishEvent(Event.LICENSE_PLAN_CHANGED, properties) await publishEvent(Event.LICENSE_PLAN_CHANGED, properties)
} }
@ -74,7 +73,6 @@ async function paymentRecovered(account: Account) {
} }
export default { export default {
tierChanged,
planChanged, planChanged,
activated, activated,
checkoutOpened, checkoutOpened,

View File

@ -123,7 +123,6 @@ beforeAll(async () => {
jest.spyOn(events.plugin, "imported") jest.spyOn(events.plugin, "imported")
jest.spyOn(events.plugin, "deleted") jest.spyOn(events.plugin, "deleted")
jest.spyOn(events.license, "tierChanged")
jest.spyOn(events.license, "planChanged") jest.spyOn(events.license, "planChanged")
jest.spyOn(events.license, "activated") jest.spyOn(events.license, "activated")
jest.spyOn(events.license, "checkoutOpened") jest.spyOn(events.license, "checkoutOpened")

View File

@ -7,16 +7,29 @@ import {
PlanType, PlanType,
PriceDuration, PriceDuration,
PurchasedPlan, PurchasedPlan,
PurchasedPrice,
Quotas, Quotas,
Subscription, Subscription,
} from "@budibase/types" } from "@budibase/types"
export function price(): PurchasedPrice {
return {
amount: 10000,
amountMonthly: 10000,
currency: "usd",
duration: PriceDuration.MONTHLY,
priceId: "price_123",
dayPasses: undefined,
isPerUser: true,
}
}
export const plan = (type: PlanType = PlanType.FREE): PurchasedPlan => { export const plan = (type: PlanType = PlanType.FREE): PurchasedPlan => {
return { return {
type, type,
usesInvoicing: false, usesInvoicing: false,
minUsers: 1,
model: PlanModel.PER_USER, model: PlanModel.PER_USER,
price: price(),
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "2.5.6-alpha.30", "version": "2.5.6-alpha.42",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",
@ -38,8 +38,8 @@
], ],
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "1.2.1", "@adobe/spectrum-css-workflow-icons": "1.2.1",
"@budibase/shared-core": "2.5.6-alpha.30", "@budibase/shared-core": "2.5.6-alpha.42",
"@budibase/string-templates": "2.5.6-alpha.30", "@budibase/string-templates": "2.5.6-alpha.42",
"@spectrum-css/accordion": "3.0.24", "@spectrum-css/accordion": "3.0.24",
"@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actionbutton": "1.0.1",
"@spectrum-css/actiongroup": "1.0.1", "@spectrum-css/actiongroup": "1.0.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "2.5.6-alpha.30", "version": "2.5.6-alpha.42",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -58,11 +58,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "2.5.6-alpha.30", "@budibase/bbui": "2.5.6-alpha.42",
"@budibase/client": "2.5.6-alpha.30", "@budibase/frontend-core": "2.5.6-alpha.42",
"@budibase/frontend-core": "2.5.6-alpha.30", "@budibase/shared-core": "2.5.6-alpha.42",
"@budibase/shared-core": "2.5.6-alpha.30", "@budibase/string-templates": "2.5.6-alpha.42",
"@budibase/string-templates": "2.5.6-alpha.30",
"@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1",

View File

@ -134,6 +134,7 @@ export const getFrontendStore = () => {
previousTopNavPath: {}, previousTopNavPath: {},
version: application.version, version: application.version,
revertableVersion: application.revertableVersion, revertableVersion: application.revertableVersion,
upgradableVersion: application.upgradableVersion,
navigation: application.navigation || {}, navigation: application.navigation || {},
usedPlugins: application.usedPlugins || [], usedPlugins: application.usedPlugins || [],
})) }))

View File

@ -9,7 +9,6 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import { API } from "api" import { API } from "api"
import clientPackage from "@budibase/client/package.json"
export function show() { export function show() {
updateModal.show() updateModal.show()
@ -25,9 +24,9 @@
$: appId = $store.appId $: appId = $store.appId
$: updateAvailable = $: updateAvailable =
clientPackage.version && $store.upgradableVersion &&
$store.version && $store.version &&
clientPackage.version !== $store.version $store.upgradableVersion !== $store.version
$: revertAvailable = $store.revertableVersion != null $: revertAvailable = $store.revertableVersion != null
const refreshAppPackage = async () => { const refreshAppPackage = async () => {
@ -46,7 +45,7 @@
// Don't wait for the async refresh, since this causes modal flashing // Don't wait for the async refresh, since this causes modal flashing
refreshAppPackage() refreshAppPackage()
notifications.success( notifications.success(
`App updated successfully to version ${clientPackage.version}` `App updated successfully to version ${$store.upgradableVersion}`
) )
} catch (err) { } catch (err) {
notifications.error(`Error updating app: ${err}`) notifications.error(`Error updating app: ${err}`)
@ -91,7 +90,7 @@
{#if updateAvailable} {#if updateAvailable}
<Body size="S"> <Body size="S">
This app is currently using version <b>{$store.version}</b>, but version This app is currently using version <b>{$store.version}</b>, but version
<b>{clientPackage.version}</b> is available. Updates can contain new features, <b>{$store.upgradableVersion}</b> is available. Updates can contain new features,
performance improvements and bug fixes. performance improvements and bug fixes.
</Body> </Body>
{:else} {:else}

View File

@ -27,7 +27,7 @@
onMount(() => { onMount(() => {
unlimited = isUnlimited() unlimited = isUnlimited()
percentage = getPercentage() percentage = getPercentage()
if (warnWhenFull && percentage === 100) { if (warnWhenFull && percentage >= 100) {
showWarning = true showWarning = true
} }
}) })

View File

@ -35,7 +35,7 @@
} }
</script> </script>
<Panel title={$selectedLayout?.name} icon="Experience" borderLeft> <Panel title={$selectedLayout?.name} icon="Experience" borderLeft wide>
<Layout paddingX="L" paddingY="XL" gap="S"> <Layout paddingX="L" paddingY="XL" gap="S">
<Banner type="warning" showCloseButton={false}> <Banner type="warning" showCloseButton={false}>
Custom layouts are being deprecated. They will be removed in a future Custom layouts are being deprecated. They will be removed in a future

View File

@ -9,7 +9,7 @@
} }
</script> </script>
<Panel borderLeft title="Navigation" icon="InfoOutline"> <Panel borderLeft title="Navigation" icon="InfoOutline" wide>
<Layout paddingX="L" paddingY="XL" gap="S"> <Layout paddingX="L" paddingY="XL" gap="S">
{#if $selectedScreen.layoutId} {#if $selectedScreen.layoutId}
<Banner <Banner

View File

@ -149,6 +149,7 @@
title={$selectedScreen.routing.route} title={$selectedScreen.routing.route}
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"} icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
borderLeft borderLeft
wide
> >
<Layout gap="S" paddingX="L" paddingY="XL"> <Layout gap="S" paddingX="L" paddingY="XL">
{#if $selectedScreen.layoutId} {#if $selectedScreen.layoutId}

View File

@ -3,7 +3,7 @@
import { Body, Layout } from "@budibase/bbui" import { Body, Layout } from "@budibase/bbui"
</script> </script>
<Panel borderLeft title="Theme" icon="InfoOutline"> <Panel borderLeft title="Theme" icon="InfoOutline" wide>
<Layout paddingX="L" paddingY="XL"> <Layout paddingX="L" paddingY="XL">
<Body size="S"> <Body size="S">
Your theme is set across all the screens within your app. Your theme is set across all the screens within your app.

View File

@ -43,12 +43,18 @@
} }
$: quotaUsage = $licensing.quotaUsage $: quotaUsage = $licensing.quotaUsage
$: license = $auth.user?.license $: license = $auth.user?.license
$: plan = license?.plan
$: usesInvoicing = plan?.usesInvoicing
$: accountPortalAccess = $auth?.user?.accountPortalAccess $: accountPortalAccess = $auth?.user?.accountPortalAccess
$: quotaReset = quotaUsage?.quotaReset $: quotaReset = quotaUsage?.quotaReset
$: canManagePlan = $: canManagePlan =
($admin.cloud && accountPortalAccess) || (!$admin.cloud && $auth.isAdmin) ($admin.cloud && accountPortalAccess) || (!$admin.cloud && $auth.isAdmin)
$: showButton = !usesInvoicing && accountPortalAccess
const setMonthlyUsage = () => { const setMonthlyUsage = () => {
monthlyUsage = [] monthlyUsage = []
if (quotaUsage.monthly) { if (quotaUsage.monthly) {
@ -121,7 +127,7 @@
const setTextRows = () => { const setTextRows = () => {
textRows = [] textRows = []
if (cancelAt) { if (cancelAt && !usesInvoicing) {
textRows.push({ message: "Subscription has been cancelled" }) textRows.push({ message: "Subscription has been cancelled" })
textRows.push({ textRows.push({
message: `${getDaysRemaining(cancelAt)} days remaining`, message: `${getDaysRemaining(cancelAt)} days remaining`,
@ -213,7 +219,7 @@
description="YOUR CURRENT PLAN" description="YOUR CURRENT PLAN"
title={planTitle()} title={planTitle()}
{primaryActionText} {primaryActionText}
primaryAction={accountPortalAccess ? goToAccountPortal : undefined} primaryAction={showButton ? goToAccountPortal : undefined}
{textRows} {textRows}
> >
<div class="content"> <div class="content">
@ -224,33 +230,23 @@
<Usage {usage} warnWhenFull={WARN_USAGE.includes(usage.name)} /> <Usage {usage} warnWhenFull={WARN_USAGE.includes(usage.name)} />
</div> </div>
{/each} {/each}
<Layout gap="XS" noPadding>
<Heading size="S">Monthly limits</Heading>
<div class="detail">
<TooltipWrapper tooltip={new Date(quotaReset)}>
<Detail size="M">
Resets in {daysRemainingInMonth} days
</Detail>
</TooltipWrapper>
</div>
</Layout>
<Layout noPadding gap="M">
{#each monthlyUsage as usage}
<Usage {usage} warnWhenFull={WARN_USAGE.includes(usage.name)} />
{/each}
</Layout>
</Layout> </Layout>
</div> </div>
{#if monthlyUsage.length}
<div class="column">
<Layout noPadding gap="M">
<Layout gap="XS" noPadding>
<Heading size="S">Monthly limits</Heading>
<div class="detail">
<TooltipWrapper tooltip={new Date(quotaReset)}>
<Detail size="M">
Resets in {daysRemainingInMonth} days
</Detail>
</TooltipWrapper>
</div>
</Layout>
<Layout noPadding gap="M">
{#each monthlyUsage as usage}
<Usage
{usage}
warnWhenFull={WARN_USAGE.includes(usage.name)}
/>
{/each}
</Layout>
</Layout>
</div>
{/if}
</div> </div>
</DashCard> </DashCard>
</Layout> </Layout>

View File

@ -176,7 +176,7 @@
<Heading>Backups</Heading> <Heading>Backups</Heading>
{#if !$licensing.backupsEnabled} {#if !$licensing.backupsEnabled}
<Tags> <Tags>
<Tag icon="LockClosed">Pro plan</Tag> <Tag icon="LockClosed">Premium</Tag>
</Tags> </Tags>
{/if} {/if}
</div> </div>

View File

@ -13,7 +13,6 @@
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import clientPackage from "@budibase/client/package.json"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
import { users, auth, apps, groups, overview } from "stores/portal" import { users, auth, apps, groups, overview } from "stores/portal"
import { fetchData } from "@budibase/frontend-core" import { fetchData } from "@budibase/frontend-core"
@ -40,7 +39,7 @@
}, },
}, },
}) })
$: updateAvailable = clientPackage.version !== $store.version $: updateAvailable = $store.upgradableVersion !== $store.version
$: isPublished = app?.status === AppStatus.DEPLOYED $: isPublished = app?.status === AppStatus.DEPLOYED
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy $: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
$: appEditorText = appEditor?.firstName || appEditor?.email $: appEditorText = appEditor?.firstName || appEditor?.email
@ -172,8 +171,8 @@
<Heading size="XS">{$store.version}</Heading> <Heading size="XS">{$store.version}</Heading>
{#if updateAvailable} {#if updateAvailable}
<div class="version-status"> <div class="version-status">
New version <strong>{clientPackage.version}</strong> is available New version <strong>{$store.upgradableVersion}</strong> is
- available -
<Link <Link
on:click={() => { on:click={() => {
$goto("./version") $goto("./version")

View File

@ -1,12 +1,11 @@
<script> <script>
import { Layout, Heading, Body, Divider, Button } from "@budibase/bbui" import { Layout, Heading, Body, Divider, Button } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import clientPackage from "@budibase/client/package.json"
import VersionModal from "components/deploy/VersionModal.svelte" import VersionModal from "components/deploy/VersionModal.svelte"
let versionModal let versionModal
$: updateAvailable = clientPackage.version !== $store.version $: updateAvailable = $store.upgradableVersion !== $store.version
</script> </script>
<Layout noPadding> <Layout noPadding>
@ -18,7 +17,7 @@
{#if updateAvailable} {#if updateAvailable}
<Body> <Body>
The app is currently using version <strong>{$store.version}</strong> The app is currently using version <strong>{$store.version}</strong>
but version <strong>{clientPackage.version}</strong> is available. but version <strong>{$store.upgradableVersion}</strong> is available.
<br /> <br />
Updates can contain new features, performance improvements and bug fixes. Updates can contain new features, performance improvements and bug fixes.
</Body> </Body>

View File

@ -378,7 +378,7 @@
</div> </div>
{#if !$licensing.enforceableSSO} {#if !$licensing.enforceableSSO}
<Tags> <Tags>
<Tag icon="LockClosed">Enterprise plan</Tag> <Tag icon="LockClosed">Enterprise</Tag>
</Tags> </Tags>
{/if} {/if}
</div> </div>

View File

@ -213,7 +213,7 @@
{/if} {/if}
{#if isCloud && !brandingEnabled} {#if isCloud && !brandingEnabled}
<Tags> <Tags>
<Tag icon="LockClosed">Pro</Tag> <Tag icon="LockClosed">Premium</Tag>
</Tags> </Tags>
{/if} {/if}
</div> </div>

View File

@ -94,7 +94,7 @@
<Heading size="M">Groups</Heading> <Heading size="M">Groups</Heading>
{#if !$licensing.groupsEnabled} {#if !$licensing.groupsEnabled}
<Tags> <Tags>
<Tag icon="LockClosed">Pro plan</Tag> <Tag icon="LockClosed">Business</Tag>
</Tags> </Tags>
{/if} {/if}
</div> </div>

View File

@ -25,7 +25,7 @@
$: invalidEmails = [] $: invalidEmails = []
$: userCount = $licensing.userCount + userEmails.length $: userCount = $licensing.userCount + userEmails.length
$: willExceed = userCount > $licensing.userLimit $: willExceed = licensing.willExceedUserLimit(userCount)
$: importDisabled = $: importDisabled =
!userEmails.length || !validEmails(userEmails) || !usersRole || willExceed !userEmails.length || !validEmails(userEmails) || !usersRole || willExceed

View File

@ -114,11 +114,13 @@ export function createUsersStore() {
const getUserRole = ({ admin, builder }) => const getUserRole = ({ admin, builder }) =>
admin?.global ? "admin" : builder?.global ? "developer" : "appUser" admin?.global ? "admin" : builder?.global ? "developer" : "appUser"
const refreshUsage = fn => async args => { const refreshUsage =
const response = await fn(args) fn =>
await licensing.setQuotaUsage() async (...args) => {
return response const response = await fn(...args)
} await licensing.setQuotaUsage()
return response
}
return { return {
subscribe, subscribe,
@ -133,7 +135,7 @@ export function createUsersStore() {
updateInvite, updateInvite,
getUserCountByApp, getUserCountByApp,
// any operation that adds or deletes users // any operation that adds or deletes users
acceptInvite: refreshUsage(acceptInvite), acceptInvite,
create: refreshUsage(create), create: refreshUsage(create),
save: refreshUsage(save), save: refreshUsage(save),
bulkDelete: refreshUsage(bulkDelete), bulkDelete: refreshUsage(bulkDelete),

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "2.5.6-alpha.30", "version": "2.5.6-alpha.42",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "dist/index.js", "main": "dist/index.js",
"bin": { "bin": {
@ -29,9 +29,9 @@
"outputPath": "build" "outputPath": "build"
}, },
"dependencies": { "dependencies": {
"@budibase/backend-core": "2.5.6-alpha.30", "@budibase/backend-core": "2.5.6-alpha.42",
"@budibase/string-templates": "2.5.6-alpha.30", "@budibase/string-templates": "2.5.6-alpha.42",
"@budibase/types": "2.5.6-alpha.30", "@budibase/types": "2.5.6-alpha.42",
"axios": "0.21.2", "axios": "0.21.2",
"chalk": "4.1.0", "chalk": "4.1.0",
"cli-progress": "3.11.2", "cli-progress": "3.11.2",

View File

@ -5225,36 +5225,5 @@
"type": "schema", "type": "schema",
"suffix": "repeater" "suffix": "repeater"
} }
},
"spreadsheet": {
"name": "Spreadsheet",
"icon": "ViewGrid",
"settings": [
{
"key": "table",
"type": "table",
"label": "Table"
},
{
"type": "filter",
"label": "Filtering",
"key": "filter"
},
{
"type": "field/sortable",
"label": "Sort Column",
"key": "sortColumn"
},
{
"type": "select",
"label": "Sort Order",
"key": "sortOrder",
"options": [
"Ascending",
"Descending"
],
"defaultValue": "Ascending"
}
]
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "2.5.6-alpha.30", "version": "2.5.6-alpha.42",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,11 +19,11 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "2.5.6-alpha.30", "@budibase/bbui": "2.5.6-alpha.42",
"@budibase/frontend-core": "2.5.6-alpha.30", "@budibase/frontend-core": "2.5.6-alpha.42",
"@budibase/shared-core": "2.5.6-alpha.30", "@budibase/shared-core": "2.5.6-alpha.42",
"@budibase/string-templates": "2.5.6-alpha.30", "@budibase/string-templates": "2.5.6-alpha.42",
"@budibase/types": "2.5.6-alpha.30", "@budibase/types": "2.5.6-alpha.42",
"@spectrum-css/button": "^3.0.3", "@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3", "@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3", "@spectrum-css/divider": "^1.0.3",

View File

@ -1,13 +1,13 @@
{ {
"name": "@budibase/frontend-core", "name": "@budibase/frontend-core",
"version": "2.5.6-alpha.30", "version": "2.5.6-alpha.42",
"description": "Budibase frontend core libraries used in builder and client", "description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"dependencies": { "dependencies": {
"@budibase/bbui": "2.5.6-alpha.30", "@budibase/bbui": "2.5.6-alpha.42",
"@budibase/shared-core": "2.5.6-alpha.30", "@budibase/shared-core": "2.5.6-alpha.42",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"socket.io-client": "^4.6.1", "socket.io-client": "^4.6.1",

View File

@ -19,6 +19,7 @@
export let updateValue = rows.actions.updateValue export let updateValue = rows.actions.updateValue
export let invertX = false export let invertX = false
export let invertY = false export let invertY = false
export let contentLines = 1
const emptyError = writable(null) const emptyError = writable(null)
@ -84,5 +85,7 @@
{readonly} {readonly}
{invertY} {invertY}
{invertX} {invertX}
{contentLines}
/> />
<slot />
</GridCell> </GridCell>

View File

@ -117,6 +117,9 @@
.cell.error { .cell.error {
--cell-color: var(--spectrum-global-color-red-500); --cell-color: var(--spectrum-global-color-red-500);
} }
.cell.readonly {
--cell-color: var(--spectrum-global-color-gray-600);
}
.cell:not(.focused) { .cell:not(.focused) {
user-select: none; user-select: none;
} }

View File

@ -37,6 +37,8 @@
$: sortedBy = column.name === $sort.column $: sortedBy = column.name === $sort.column
$: canMoveLeft = orderable && idx > 0 $: canMoveLeft = orderable && idx > 0
$: canMoveRight = orderable && idx < $renderedColumns.length - 1 $: canMoveRight = orderable && idx < $renderedColumns.length - 1
$: ascendingLabel = column.schema?.type === "number" ? "low-high" : "A-Z"
$: descendingLabel = column.schema?.type === "number" ? "high-low" : "Z-A"
const editColumn = () => { const editColumn = () => {
dispatch("edit-column", column.schema) dispatch("edit-column", column.schema)
@ -179,14 +181,14 @@
on:click={sortAscending} on:click={sortAscending}
disabled={column.name === $sort.column && $sort.order === "ascending"} disabled={column.name === $sort.column && $sort.order === "ascending"}
> >
Sort A-Z Sort {ascendingLabel}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
icon="SortOrderDown" icon="SortOrderDown"
on:click={sortDescending} on:click={sortDescending}
disabled={column.name === $sort.column && $sort.order === "descending"} disabled={column.name === $sort.column && $sort.order === "descending"}
> >
Sort Z-A Sort {descendingLabel}
</MenuItem> </MenuItem>
<MenuItem disabled={!canMoveLeft} icon="ChevronLeft" on:click={moveLeft}> <MenuItem disabled={!canMoveLeft} icon="ChevronLeft" on:click={moveLeft}>
Move left Move left

View File

@ -1,5 +1,6 @@
<script> <script>
import { onMount, tick } from "svelte" import { onMount, tick } from "svelte"
import { clickOutside } from "@budibase/bbui"
export let value export let value
export let focused = false export let focused = false
@ -60,6 +61,7 @@
on:change={handleChange} on:change={handleChange}
on:wheel|stopPropagation on:wheel|stopPropagation
spellcheck="false" spellcheck="false"
use:clickOutside={close}
/> />
{:else} {:else}
<div class="long-form-cell" on:click={editable ? open : null} class:editable> <div class="long-form-cell" on:click={editable ? open : null} class:editable>

View File

@ -2,6 +2,13 @@
import TextCell from "./TextCell.svelte" import TextCell from "./TextCell.svelte"
export let api export let api
export let onChange
const numberOnChange = value => {
const float = parseFloat(value)
const newValue = isNaN(float) ? null : float
onChange(newValue)
}
</script> </script>
<TextCell {...$$props} bind:api type="number" /> <TextCell {...$$props} onChange={numberOnChange} bind:api type="number" />

View File

@ -1,5 +1,5 @@
<script> <script>
import { Icon } from "@budibase/bbui" import { Icon, clickOutside } from "@budibase/bbui"
import { getColor } from "../lib/utils" import { getColor } from "../lib/utils"
import { onMount } from "svelte" import { onMount } from "svelte"
@ -12,6 +12,7 @@
export let api export let api
export let invertX = false export let invertX = false
export let invertY = false export let invertY = false
export let contentLines = 1
let isOpen = false let isOpen = false
let focusedOptionIdx = null let focusedOptionIdx = null
@ -86,7 +87,11 @@
class:open class:open
on:click|self={editable ? open : null} on:click|self={editable ? open : null}
> >
<div class="values" on:click={editable ? open : null}> <div
class="values"
class:wrap={contentLines > 1}
on:click={editable ? open : null}
>
{#each values as val} {#each values as val}
{@const color = getOptionColor(val)} {@const color = getOptionColor(val)}
{#if color} {#if color}
@ -113,6 +118,7 @@
class:invertX class:invertX
class:invertY class:invertY
on:wheel={e => e.stopPropagation()} on:wheel={e => e.stopPropagation()}
use:clickOutside={close}
> >
{#each options as option, idx} {#each options as option, idx}
{@const color = getOptionColor(option)} {@const color = getOptionColor(option)}
@ -160,6 +166,9 @@
grid-row-gap: var(--cell-padding); grid-row-gap: var(--cell-padding);
overflow: hidden; overflow: hidden;
padding: var(--cell-padding); padding: var(--cell-padding);
flex-wrap: nowrap;
}
.values.wrap {
flex-wrap: wrap; flex-wrap: wrap;
} }
.text { .text {

View File

@ -18,7 +18,7 @@
<script> <script>
import { getColor } from "../lib/utils" import { getColor } from "../lib/utils"
import { onMount, getContext } from "svelte" import { onMount, getContext } from "svelte"
import { Icon, Input, ProgressCircle } from "@budibase/bbui" import { Icon, Input, ProgressCircle, clickOutside } from "@budibase/bbui"
import { debounce } from "../../../utils/utils" import { debounce } from "../../../utils/utils"
export let value export let value
@ -29,6 +29,7 @@
export let onChange export let onChange
export let invertX = false export let invertX = false
export let invertY = false export let invertY = false
export let contentLines = 1
const { API, dispatch } = getContext("grid") const { API, dispatch } = getContext("grid")
const color = getColor(0) const color = getColor(0)
@ -243,7 +244,11 @@
<div class="wrapper" class:editable class:focused style="--color:{color};"> <div class="wrapper" class:editable class:focused style="--color:{color};">
<div class="container"> <div class="container">
<div class="values" on:wheel={e => (focused ? e.stopPropagation() : null)}> <div
class="values"
class:wrap={editable || contentLines > 1}
on:wheel={e => (focused ? e.stopPropagation() : null)}
>
{#each value || [] as relationship, idx} {#each value || [] as relationship, idx}
{#if relationship.primaryDisplay} {#if relationship.primaryDisplay}
<div class="badge"> <div class="badge">
@ -279,7 +284,13 @@
</div> </div>
{#if isOpen} {#if isOpen}
<div class="dropdown" class:invertX class:invertY on:wheel|stopPropagation> <div
class="dropdown"
class:invertX
class:invertY
on:wheel|stopPropagation
use:clickOutside={close}
>
<div class="search"> <div class="search">
<Input <Input
autofocus autofocus
@ -376,6 +387,9 @@
grid-row-gap: var(--cell-padding); grid-row-gap: var(--cell-padding);
overflow: hidden; overflow: hidden;
padding: var(--cell-padding); padding: var(--cell-padding);
flex-wrap: nowrap;
}
.values.wrap {
flex-wrap: wrap; flex-wrap: wrap;
} }
.count { .count {

View File

@ -3,16 +3,13 @@
import { ActionButton, Popover, Select } from "@budibase/bbui" import { ActionButton, Popover, Select } from "@budibase/bbui"
const { sort, columns, stickyColumn } = getContext("grid") const { sort, columns, stickyColumn } = getContext("grid")
const orderOptions = [
{ label: "A-Z", value: "ascending" },
{ label: "Z-A", value: "descending" },
]
let open = false let open = false
let anchor let anchor
$: columnOptions = getColumnOptions($stickyColumn, $columns) $: columnOptions = getColumnOptions($stickyColumn, $columns)
$: checkValidSortColumn($sort.column, $stickyColumn, $columns) $: checkValidSortColumn($sort.column, $stickyColumn, $columns)
$: orderOptions = getOrderOptions($sort.column, columnOptions)
const getColumnOptions = (stickyColumn, columns) => { const getColumnOptions = (stickyColumn, columns) => {
let options = [] let options = []
@ -20,6 +17,7 @@
options.push({ options.push({
label: stickyColumn.label || stickyColumn.name, label: stickyColumn.label || stickyColumn.name,
value: stickyColumn.name, value: stickyColumn.name,
type: stickyColumn.schema?.type,
}) })
} }
return [ return [
@ -27,10 +25,25 @@
...columns.map(col => ({ ...columns.map(col => ({
label: col.label || col.name, label: col.label || col.name,
value: col.name, value: col.name,
type: col.schema?.type,
})), })),
] ]
} }
const getOrderOptions = (column, columnOptions) => {
const type = columnOptions.find(col => col.value === column)?.type
return [
{
label: type === "number" ? "Low-high" : "A-Z",
value: "ascending",
},
{
label: type === "number" ? "High-low" : "Z-A",
value: "descending",
},
]
}
const updateSortColumn = e => { const updateSortColumn = e => {
sort.update(state => ({ sort.update(state => ({
...state, ...state,

View File

@ -15,6 +15,7 @@
selectedCellMap, selectedCellMap,
focusedRow, focusedRow,
columnHorizontalInversionIndex, columnHorizontalInversionIndex,
contentLines,
} = getContext("grid") } = getContext("grid")
$: rowSelected = !!$selectedRows[row._id] $: rowSelected = !!$selectedRows[row._id]
@ -44,6 +45,7 @@
focused={$focusedCellId === cellId} focused={$focusedCellId === cellId}
selectedUser={$selectedCellMap[cellId]} selectedUser={$selectedCellMap[cellId]}
width={column.width} width={column.width}
contentLines={$contentLines}
/> />
{/each} {/each}
</div> </div>

View File

@ -0,0 +1,53 @@
<script>
export let keybind
export let padded = false
export let overlay = false
$: parsedKeys = parseKeys(keybind)
const parseKeys = keybind => {
return keybind?.split("+").map(key => {
if (key.toLowerCase() === "ctrl") {
return navigator.platform.startsWith("Mac") ? "⌘" : key
} else if (key.toLowerCase() === "enter") {
return "↵"
}
return key
})
}
</script>
<div class="keys" class:padded class:overlay>
{#each parsedKeys as key}
<div class="key">
{key}
</div>
{/each}
</div>
<style>
.keys {
display: flex;
flex-direction: row;
gap: 3px;
}
.keys.padded {
padding: var(--cell-padding);
}
.key {
padding: 2px 6px;
font-size: 12px;
font-weight: 600;
background-color: var(--spectrum-global-color-gray-200);
color: var(--spectrum-global-color-gray-700);
border-radius: 4px;
text-align: center;
display: grid;
place-items: center;
text-transform: uppercase;
}
.overlay .key {
background: rgba(255, 255, 255, 0.2);
color: #eee;
}
</style>

View File

@ -7,6 +7,7 @@
import { GutterWidth } from "../lib/constants" import { GutterWidth } from "../lib/constants"
import { NewRowID } from "../lib/constants" import { NewRowID } from "../lib/constants"
import GutterCell from "../cells/GutterCell.svelte" import GutterCell from "../cells/GutterCell.svelte"
import KeyboardShortcut from "./KeyboardShortcut.svelte"
const { const {
hoveredRowId, hoveredRowId,
@ -27,13 +28,14 @@
columnHorizontalInversionIndex, columnHorizontalInversionIndex,
} = getContext("grid") } = getContext("grid")
let visible = false
let isAdding = false let isAdding = false
let newRow = {} let newRow = {}
let offset = 0 let offset = 0
$: firstColumn = $stickyColumn || $renderedColumns[0] $: firstColumn = $stickyColumn || $renderedColumns[0]
$: width = GutterWidth + ($stickyColumn?.width || 0) $: width = GutterWidth + ($stickyColumn?.width || 0)
$: $tableId, (isAdding = false) $: $tableId, (visible = false)
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows) $: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
const shouldInvertY = (offset, inversionIndex, rows) => { const shouldInvertY = (offset, inversionIndex, rows) => {
@ -45,7 +47,8 @@
const addRow = async () => { const addRow = async () => {
// Blur the active cell and tick to let final value updates propagate // Blur the active cell and tick to let final value updates propagate
$focusedCellAPI?.blur() isAdding = true
$focusedCellId = null
await tick() await tick()
// Create row // Create row
@ -60,17 +63,19 @@
$focusedCellId = `${savedRow._id}-${firstColumn.name}` $focusedCellId = `${savedRow._id}-${firstColumn.name}`
} }
} }
isAdding = false
} }
const clear = () => { const clear = () => {
isAdding = false isAdding = false
visible = false
$focusedCellId = null $focusedCellId = null
$hoveredRowId = null $hoveredRowId = null
document.removeEventListener("keydown", handleKeyPress) document.removeEventListener("keydown", handleKeyPress)
} }
const startAdding = async () => { const startAdding = async () => {
if (isAdding) { if (visible) {
return return
} }
@ -95,7 +100,7 @@
// Update state and select initial cell // Update state and select initial cell
newRow = {} newRow = {}
isAdding = true visible = true
$hoveredRowId = NewRowID $hoveredRowId = NewRowID
if (firstColumn) { if (firstColumn) {
$focusedCellId = `${NewRowID}-${firstColumn.name}` $focusedCellId = `${NewRowID}-${firstColumn.name}`
@ -115,7 +120,7 @@
} }
const handleKeyPress = e => { const handleKeyPress = e => {
if (!isAdding) { if (!visible) {
return return
} }
if (e.key === "Escape") { if (e.key === "Escape") {
@ -137,7 +142,7 @@
</script> </script>
<!-- Only show new row functionality if we have any columns --> <!-- Only show new row functionality if we have any columns -->
{#if isAdding} {#if visible}
<div <div
class="container" class="container"
class:floating={offset > 0} class:floating={offset > 0}
@ -148,6 +153,9 @@
<div class="sticky-column" transition:fade={{ duration: 130 }}> <div class="sticky-column" transition:fade={{ duration: 130 }}>
<GutterCell on:expand={addViaModal} rowHovered> <GutterCell on:expand={addViaModal} rowHovered>
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" /> <Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
{#if isAdding}
<div in:fade={{ duration: 130 }} class="loading-overlay" />
{/if}
</GutterCell> </GutterCell>
{#if $stickyColumn} {#if $stickyColumn}
{@const cellId = `${NewRowID}-${$stickyColumn.name}`} {@const cellId = `${NewRowID}-${$stickyColumn.name}`}
@ -161,7 +169,14 @@
{updateValue} {updateValue}
rowIdx={0} rowIdx={0}
{invertY} {invertY}
/> >
{#if $stickyColumn?.schema?.autocolumn}
<div class="readonly-overlay">Can't edit auto column</div>
{/if}
{#if isAdding}
<div in:fade={{ duration: 130 }} class="loading-overlay" />
{/if}
</DataCell>
{/if} {/if}
</div> </div>
<div class="normal-columns" transition:fade={{ duration: 130 }}> <div class="normal-columns" transition:fade={{ duration: 130 }}>
@ -181,15 +196,32 @@
rowIdx={0} rowIdx={0}
invertX={columnIdx >= $columnHorizontalInversionIndex} invertX={columnIdx >= $columnHorizontalInversionIndex}
{invertY} {invertY}
/> >
{#if column?.schema?.autocolumn}
<div class="readonly-overlay">Can't edit auto column</div>
{/if}
{#if isAdding}
<div in:fade={{ duration: 130 }} class="loading-overlay" />
{/if}
</DataCell>
{/key} {/key}
{/each} {/each}
</div> </div>
</GridScrollWrapper> </GridScrollWrapper>
</div> </div>
<div class="buttons" transition:fade={{ duration: 130 }}> <div class="buttons" transition:fade={{ duration: 130 }}>
<Button size="M" cta on:click={addRow}>Save</Button> <Button size="M" cta on:click={addRow} disabled={isAdding}>
<Button size="M" secondary newStyles on:click={clear}>Cancel</Button> <div class="button-with-keys">
Save
<KeyboardShortcut overlay keybind="Ctrl+Enter" />
</div>
</Button>
<Button size="M" secondary newStyles on:click={clear}>
<div class="button-with-keys">
Cancel
<KeyboardShortcut overlay keybind="Esc" />
</div>
</Button>
</div> </div>
</div> </div>
{/if} {/if}
@ -240,6 +272,14 @@
top: calc(var(--row-height) + var(--offset) + 24px); top: calc(var(--row-height) + var(--offset) + 24px);
left: var(--gutter-width); left: var(--gutter-width);
} }
.button-with-keys {
display: flex;
gap: 6px;
align-items: center;
}
.button-with-keys :global(> div) {
padding-top: 2px;
}
/* Sticky column styles */ /* Sticky column styles */
.sticky-column { .sticky-column {
@ -262,4 +302,33 @@
width: 0; width: 0;
display: flex; display: flex;
} }
/* Readonly cell overlay */
.readonly-overlay {
position: absolute;
top: 0;
left: 0;
height: var(--row-height);
width: 100%;
padding: var(--cell-padding);
font-style: italic;
color: var(--spectrum-global-color-gray-600);
z-index: 1;
user-select: none;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* Overlay while row is being added */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
height: var(--row-height);
width: 100%;
z-index: 1;
background: var(--spectrum-global-color-gray-400);
opacity: 0.25;
}
</style> </style>

View File

@ -7,6 +7,7 @@
import HeaderCell from "../cells/HeaderCell.svelte" import HeaderCell from "../cells/HeaderCell.svelte"
import { GutterWidth, BlankRowID } from "../lib/constants" import { GutterWidth, BlankRowID } from "../lib/constants"
import GutterCell from "../cells/GutterCell.svelte" import GutterCell from "../cells/GutterCell.svelte"
import KeyboardShortcut from "./KeyboardShortcut.svelte"
const { const {
rows, rows,
@ -21,6 +22,7 @@
focusedRow, focusedRow,
scrollLeft, scrollLeft,
dispatch, dispatch,
contentLines,
} = getContext("grid") } = getContext("grid")
$: rowCount = $rows.length $: rowCount = $rows.length
@ -85,6 +87,7 @@
selectedUser={$selectedCellMap[cellId]} selectedUser={$selectedCellMap[cellId]}
width={$stickyColumn.width} width={$stickyColumn.width}
column={$stickyColumn} column={$stickyColumn}
contentLines={$contentLines}
/> />
{/if} {/if}
</div> </div>
@ -103,7 +106,9 @@
<GridCell <GridCell
width={$stickyColumn.width} width={$stickyColumn.width}
highlighted={$hoveredRowId === BlankRowID} highlighted={$hoveredRowId === BlankRowID}
/> >
<KeyboardShortcut padded keybind="Ctrl+Enter" />
</GridCell>
{/if} {/if}
</div> </div>
{/if} {/if}

View File

@ -15,8 +15,22 @@
selectedRows, selectedRows,
} = getContext("grid") } = getContext("grid")
const ignoredOriginSelectors = [
".spectrum-Modal",
"#builder-side-panel-container",
]
// Global key listener which intercepts all key events // Global key listener which intercepts all key events
const handleKeyDown = e => { const handleKeyDown = e => {
// Avoid processing events sourced from certain origins
if (e.target?.closest) {
for (let selector of ignoredOriginSelectors) {
if (e.target.closest(selector)) {
return
}
}
}
// If nothing selected avoid processing further key presses // If nothing selected avoid processing further key presses
if (!$focusedCellId) { if (!$focusedCellId) {
if (e.key === "Tab" || e.key?.startsWith("Arrow")) { if (e.key === "Tab" || e.key?.startsWith("Arrow")) {
@ -60,11 +74,6 @@
return return
} }
} }
// Avoid processing events sourced from modals
if (e.target?.closest?.(".spectrum-Modal")) {
return
}
e.preventDefault() e.preventDefault()
// Handle the key ourselves // Handle the key ourselves

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/sdk", "name": "@budibase/sdk",
"version": "2.5.6-alpha.30", "version": "2.5.6-alpha.42",
"description": "Budibase Public API SDK", "description": "Budibase Public API SDK",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "2.5.6-alpha.30", "version": "2.5.6-alpha.42",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -45,12 +45,12 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "10.0.3", "@apidevtools/swagger-parser": "10.0.3",
"@budibase/backend-core": "2.5.6-alpha.30", "@budibase/backend-core": "2.5.6-alpha.42",
"@budibase/client": "2.5.6-alpha.30", "@budibase/client": "2.5.6-alpha.42",
"@budibase/pro": "2.5.6-alpha.30", "@budibase/pro": "2.5.6-alpha.42",
"@budibase/shared-core": "2.5.6-alpha.30", "@budibase/shared-core": "2.5.6-alpha.42",
"@budibase/string-templates": "2.5.6-alpha.30", "@budibase/string-templates": "2.5.6-alpha.42",
"@budibase/types": "2.5.6-alpha.30", "@budibase/types": "2.5.6-alpha.42",
"@bull-board/api": "3.7.0", "@bull-board/api": "3.7.0",
"@bull-board/koa": "3.9.4", "@bull-board/koa": "3.9.4",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",

View File

@ -223,7 +223,7 @@ export async function fetchAppPackage(ctx: UserCtx) {
) )
ctx.body = { ctx.body = {
application, application: { ...application, upgradableVersion: envCore.VERSION },
screens, screens,
layouts, layouts,
clientLibPath, clientLibPath,

View File

@ -12,7 +12,15 @@ import { getIntegration } from "../../integrations"
import { getDatasourceAndQuery } from "./row/utils" import { getDatasourceAndQuery } from "./row/utils"
import { invalidateDynamicVariables } from "../../threads/utils" import { invalidateDynamicVariables } from "../../threads/utils"
import { db as dbCore, context, events } from "@budibase/backend-core" import { db as dbCore, context, events } from "@budibase/backend-core"
import { UserCtx, Datasource, Row } from "@budibase/types" import {
UserCtx,
Datasource,
Row,
CreateDatasourceResponse,
UpdateDatasourceResponse,
UpdateDatasourceRequest,
CreateDatasourceRequest,
} from "@budibase/types"
import sdk from "../../sdk" import sdk from "../../sdk"
export async function fetch(ctx: UserCtx) { export async function fetch(ctx: UserCtx) {
@ -146,7 +154,7 @@ async function invalidateVariables(
await invalidateDynamicVariables(toInvalidate) await invalidateDynamicVariables(toInvalidate)
} }
export async function update(ctx: UserCtx) { export async function update(ctx: UserCtx<any, UpdateDatasourceResponse>) {
const db = context.getAppDB() const db = context.getAppDB()
const datasourceId = ctx.params.datasourceId const datasourceId = ctx.params.datasourceId
let datasource = await sdk.datasources.get(datasourceId) let datasource = await sdk.datasources.get(datasourceId)
@ -187,15 +195,17 @@ export async function update(ctx: UserCtx) {
} }
} }
export async function save(ctx: UserCtx) { export async function save(
ctx: UserCtx<CreateDatasourceRequest, CreateDatasourceResponse>
) {
const db = context.getAppDB() const db = context.getAppDB()
const plus = ctx.request.body.datasource.plus const plus = ctx.request.body.datasource.plus
const fetchSchema = ctx.request.body.fetchSchema const fetchSchema = ctx.request.body.fetchSchema
const datasource = { const datasource = {
_id: generateDatasourceID({ plus }), _id: generateDatasourceID({ plus }),
type: plus ? DocumentType.DATASOURCE_PLUS : DocumentType.DATASOURCE,
...ctx.request.body.datasource, ...ctx.request.body.datasource,
type: plus ? DocumentType.DATASOURCE_PLUS : DocumentType.DATASOURCE,
} }
let schemaError = null let schemaError = null
@ -218,7 +228,7 @@ export async function save(ctx: UserCtx) {
} }
} }
const response: any = { const response: CreateDatasourceResponse = {
datasource: await sdk.datasources.removeSecretSingle(datasource), datasource: await sdk.datasources.removeSecretSingle(datasource),
} }
if (schemaError) { if (schemaError) {

View File

@ -27,6 +27,7 @@ export const isProdAppID = dbCore.isProdAppID
export const USER_METDATA_PREFIX = `${DocumentType.ROW}${SEPARATOR}${dbCore.InternalTable.USER_METADATA}${SEPARATOR}` export const USER_METDATA_PREFIX = `${DocumentType.ROW}${SEPARATOR}${dbCore.InternalTable.USER_METADATA}${SEPARATOR}`
export const LINK_USER_METADATA_PREFIX = `${DocumentType.LINK}${SEPARATOR}${dbCore.InternalTable.USER_METADATA}${SEPARATOR}` export const LINK_USER_METADATA_PREFIX = `${DocumentType.LINK}${SEPARATOR}${dbCore.InternalTable.USER_METADATA}${SEPARATOR}`
export const TABLE_ROW_PREFIX = `${DocumentType.ROW}${SEPARATOR}${DocumentType.TABLE}` export const TABLE_ROW_PREFIX = `${DocumentType.ROW}${SEPARATOR}${DocumentType.TABLE}`
export const AUTOMATION_LOG_PREFIX = `${DocumentType.AUTOMATION_LOG}${SEPARATOR}`
export const ViewName = dbCore.ViewName export const ViewName = dbCore.ViewName
export const InternalTables = dbCore.InternalTable export const InternalTables = dbCore.InternalTable
export const UNICODE_MAX = dbCore.UNICODE_MAX export const UNICODE_MAX = dbCore.UNICODE_MAX

View File

@ -349,7 +349,7 @@ describe("row api - postgres", () => {
}, },
plus: true, plus: true,
source: "POSTGRES", source: "POSTGRES",
type: "datasource", type: "datasource_plus",
_id: expect.any(String), _id: expect.any(String),
_rev: expect.any(String), _rev: expect.any(String),
createdAt: expect.any(String), createdAt: expect.any(String),

View File

@ -3,6 +3,7 @@ import { budibaseTempDir } from "../../../utilities/budibaseDir"
import { streamFile, createTempFolder } from "../../../utilities/fileSystem" import { streamFile, createTempFolder } from "../../../utilities/fileSystem"
import { ObjectStoreBuckets } from "../../../constants" import { ObjectStoreBuckets } from "../../../constants"
import { import {
AUTOMATION_LOG_PREFIX,
LINK_USER_METADATA_PREFIX, LINK_USER_METADATA_PREFIX,
TABLE_ROW_PREFIX, TABLE_ROW_PREFIX,
USER_METDATA_PREFIX, USER_METDATA_PREFIX,
@ -20,11 +21,15 @@ const uuid = require("uuid/v4")
const tar = require("tar") const tar = require("tar")
const MemoryStream = require("memorystream") const MemoryStream = require("memorystream")
type ExportOpts = { interface DBDumpOpts {
filter?: any filter?: any
exportPath?: string exportPath?: string
}
interface ExportOpts extends DBDumpOpts {
tar?: boolean tar?: boolean
excludeRows?: boolean excludeRows?: boolean
excludeLogs?: boolean
} }
function tarFilesToTmp(tmpDir: string, files: string[]) { function tarFilesToTmp(tmpDir: string, files: string[]) {
@ -49,7 +54,7 @@ function tarFilesToTmp(tmpDir: string, files: string[]) {
* a filter function or the name of the export. * a filter function or the name of the export.
* @return {*} either a readable stream or a string * @return {*} either a readable stream or a string
*/ */
export async function exportDB(dbName: string, opts: ExportOpts = {}) { export async function exportDB(dbName: string, opts: DBDumpOpts = {}) {
const exportOpts = { const exportOpts = {
filter: opts?.filter, filter: opts?.filter,
batch_size: 1000, batch_size: 1000,
@ -76,11 +81,14 @@ export async function exportDB(dbName: string, opts: ExportOpts = {}) {
}) })
} }
function defineFilter(excludeRows?: boolean) { function defineFilter(excludeRows?: boolean, excludeLogs?: boolean) {
const ids = [USER_METDATA_PREFIX, LINK_USER_METADATA_PREFIX] const ids = [USER_METDATA_PREFIX, LINK_USER_METADATA_PREFIX]
if (excludeRows) { if (excludeRows) {
ids.push(TABLE_ROW_PREFIX) ids.push(TABLE_ROW_PREFIX)
} }
if (excludeLogs) {
ids.push(AUTOMATION_LOG_PREFIX)
}
return (doc: any) => return (doc: any) =>
!ids.map(key => doc._id.includes(key)).reduce((prev, curr) => prev || curr) !ids.map(key => doc._id.includes(key)).reduce((prev, curr) => prev || curr)
} }
@ -130,8 +138,7 @@ export async function exportApp(appId: string, config?: ExportOpts) {
// enforce an export of app DB to the tmp path // enforce an export of app DB to the tmp path
const dbPath = join(tmpPath, DB_EXPORT_FILE) const dbPath = join(tmpPath, DB_EXPORT_FILE)
await exportDB(appId, { await exportDB(appId, {
...config, filter: defineFilter(config?.excludeRows, config?.excludeLogs),
filter: defineFilter(config?.excludeRows),
exportPath: dbPath, exportPath: dbPath,
}) })
// if tar requested, return where the tarball is // if tar requested, return where the tarball is
@ -155,6 +162,10 @@ export async function exportApp(appId: string, config?: ExportOpts) {
* @returns {*} a readable stream of the backup which is written in real time * @returns {*} a readable stream of the backup which is written in real time
*/ */
export async function streamExportApp(appId: string, excludeRows: boolean) { export async function streamExportApp(appId: string, excludeRows: boolean) {
const tmpPath = await exportApp(appId, { excludeRows, tar: true }) const tmpPath = await exportApp(appId, {
excludeRows,
excludeLogs: true,
tar: true,
})
return streamFile(tmpPath) return streamFile(tmpPath)
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/shared-core", "name": "@budibase/shared-core",
"version": "2.5.6-alpha.30", "version": "2.5.6-alpha.42",
"description": "Shared data utils", "description": "Shared data utils",
"main": "dist/cjs/src/index.js", "main": "dist/cjs/src/index.js",
"types": "dist/mjs/src/index.d.ts", "types": "dist/mjs/src/index.d.ts",
@ -20,7 +20,7 @@
"dev:builder": "yarn prebuild && concurrently \"tsc -p tsconfig.build.json --watch\" \"tsc -p tsconfig-cjs.build.json --watch\"" "dev:builder": "yarn prebuild && concurrently \"tsc -p tsconfig.build.json --watch\" \"tsc -p tsconfig-cjs.build.json --watch\""
}, },
"dependencies": { "dependencies": {
"@budibase/types": "2.5.6-alpha.30" "@budibase/types": "2.5.6-alpha.42"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^7.6.0", "concurrently": "^7.6.0",

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/types", "name": "@budibase/types",
"version": "2.5.6-alpha.30", "version": "2.5.6-alpha.42",
"description": "Budibase types", "description": "Budibase types",
"main": "dist/cjs/index.js", "main": "dist/cjs/index.js",
"types": "dist/mjs/index.d.ts", "types": "dist/mjs/index.d.ts",

View File

@ -0,0 +1,19 @@
import { Datasource } from "../../../documents"
export interface CreateDatasourceResponse {
datasource: Datasource
error?: any
}
export interface UpdateDatasourceResponse {
datasource: Datasource
}
export interface CreateDatasourceRequest {
datasource: Datasource
fetchSchema?: boolean
}
export interface UpdateDatasourceRequest extends Datasource {
datasource: Datasource
}

View File

@ -1 +1,2 @@
export * from "./backup" export * from "./backup"
export * from "./datasource"

View File

@ -39,6 +39,7 @@ export interface Account extends CreateAccount {
// licensing // licensing
tier: string // deprecated tier: string // deprecated
planType?: PlanType planType?: PlanType
/** @deprecated */
planTier?: number planTier?: number
license?: License license?: License
installId?: string installId?: string
@ -47,6 +48,7 @@ export interface Account extends CreateAccount {
stripeCustomerId?: string stripeCustomerId?: string
licenseKey?: string licenseKey?: string
licenseKeyActivatedAt?: number licenseKeyActivatedAt?: number
licenseRequestedAt?: number
licenseOverrides?: LicenseOverrides licenseOverrides?: LicenseOverrides
quotaUsage?: QuotaUsage quotaUsage?: QuotaUsage
} }

View File

@ -42,3 +42,10 @@ export interface PaginationValues {
page: string | number | null page: string | number | null
limit: number | null limit: number | null
} }
export interface PreviewQueryRequest extends Omit<Query, "parameters"> {
parameters: {}
flags?: {
urlName?: boolean
}
}

View File

@ -138,7 +138,6 @@ export enum Event {
// LICENSE // LICENSE
LICENSE_PLAN_CHANGED = "license:plan:changed", LICENSE_PLAN_CHANGED = "license:plan:changed",
LICENSE_TIER_CHANGED = "license:tier:changed",
LICENSE_ACTIVATED = "license:activated", LICENSE_ACTIVATED = "license:activated",
LICENSE_PAYMENT_FAILED = "license:payment:failed", LICENSE_PAYMENT_FAILED = "license:payment:failed",
LICENSE_PAYMENT_RECOVERED = "license:payment:recovered", LICENSE_PAYMENT_RECOVERED = "license:payment:recovered",
@ -328,7 +327,6 @@ export const AuditedEventFriendlyName: Record<Event, string | undefined> = {
// LICENSE - NOT AUDITED // LICENSE - NOT AUDITED
[Event.LICENSE_PLAN_CHANGED]: undefined, [Event.LICENSE_PLAN_CHANGED]: undefined,
[Event.LICENSE_TIER_CHANGED]: undefined,
[Event.LICENSE_ACTIVATED]: undefined, [Event.LICENSE_ACTIVATED]: undefined,
[Event.LICENSE_PAYMENT_FAILED]: undefined, [Event.LICENSE_PAYMENT_FAILED]: undefined,
[Event.LICENSE_PAYMENT_RECOVERED]: undefined, [Event.LICENSE_PAYMENT_RECOVERED]: undefined,

View File

@ -1,15 +1,15 @@
import { PlanType } from "../licensing" import { PlanType, PriceDuration } from "../licensing"
export interface LicenseTierChangedEvent {
accountId: string
from: number
to: number
}
export interface LicensePlanChangedEvent { export interface LicensePlanChangedEvent {
accountId: string accountId: string
from: PlanType from: PlanType
to: PlanType to: PlanType
// may not be on historical events
// free plans won't have a duration
duration: PriceDuration | undefined
// may not be on historical events
// free plans won't have a quantity
quantity: number | undefined
} }
export interface LicenseActivatedEvent { export interface LicenseActivatedEvent {

View File

@ -17,7 +17,6 @@ export enum PriceDuration {
export interface AvailablePlan { export interface AvailablePlan {
type: PlanType type: PlanType
maxUsers: number maxUsers: number
minUsers: number
prices: AvailablePrice[] prices: AvailablePrice[]
} }
@ -38,7 +37,6 @@ export interface PurchasedPlan {
type: PlanType type: PlanType
model: PlanModel model: PlanModel
usesInvoicing: boolean usesInvoicing: boolean
minUsers: number
price?: PurchasedPrice price?: PurchasedPrice
} }

View File

@ -55,12 +55,6 @@ export const isConstantQuota = (
return quotaType === QuotaType.CONSTANT return quotaType === QuotaType.CONSTANT
} }
export interface Minimums {
users: number
}
export type PlanMinimums = { [key in PlanType]: Minimums }
export type PlanQuotas = { [key in PlanType]: Quotas | undefined } export type PlanQuotas = { [key in PlanType]: Quotas | undefined }
export type MonthlyQuotas = { export type MonthlyQuotas = {

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/worker", "name": "@budibase/worker",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "2.5.6-alpha.30", "version": "2.5.6-alpha.42",
"description": "Budibase background service", "description": "Budibase background service",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -37,10 +37,10 @@
"author": "Budibase", "author": "Budibase",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@budibase/backend-core": "2.5.6-alpha.30", "@budibase/backend-core": "2.5.6-alpha.42",
"@budibase/pro": "2.5.6-alpha.30", "@budibase/pro": "2.5.6-alpha.42",
"@budibase/string-templates": "2.5.6-alpha.30", "@budibase/string-templates": "2.5.6-alpha.42",
"@budibase/types": "2.5.6-alpha.30", "@budibase/types": "2.5.6-alpha.42",
"@koa/router": "8.0.8", "@koa/router": "8.0.8",
"@sentry/node": "6.17.7", "@sentry/node": "6.17.7",
"@techpass/passport-openidconnect": "0.3.2", "@techpass/passport-openidconnect": "0.3.2",

View File

@ -424,7 +424,9 @@ export const inviteAccept = async (
if (err.code === ErrorCode.USAGE_LIMIT_EXCEEDED) { if (err.code === ErrorCode.USAGE_LIMIT_EXCEEDED) {
// explicitly re-throw limit exceeded errors // explicitly re-throw limit exceeded errors
ctx.throw(400, err) ctx.throw(400, err)
return
} }
console.warn("Error inviting user", err)
ctx.throw(400, "Unable to create new user, invitation invalid.") ctx.throw(400, "Unable to create new user, invitation invalid.")
} }
} }

View File

@ -18,8 +18,17 @@ export const saveMetadata = async (
if (existing) { if (existing) {
metadata._rev = existing._rev metadata._rev = existing._rev
} }
const res = await db.put(metadata) try {
metadata._rev = res.rev const res = await db.put(metadata)
metadata._rev = res.rev
} catch (e: any) {
// account can be updated frequently
// ignore 409
if (e.status !== 409) {
throw e
}
}
return metadata return metadata
}) })
} }

View File

@ -14,6 +14,20 @@ const env = {
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY, ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY,
BB_ADMIN_USER_EMAIL: process.env.BB_ADMIN_USER_EMAIL, BB_ADMIN_USER_EMAIL: process.env.BB_ADMIN_USER_EMAIL,
BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD, BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD,
POSTGRES_HOST: process.env.POSTGRES_HOST,
POSTGRES_PORT: process.env.POSTGRES_PORT,
POSTGRES_DB: process.env.POSTGRES_DB,
POSTGRES_USER: process.env.POSTGRES_USER,
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD,
MONGODB_CONNECTION_STRING: process.env.MONGODB_CONNECTION_STRING,
MONGODB_DB: process.env.MONGODB_DB,
REST_API_BASE_URL: process.env.REST_API_BASE_URL,
REST_API_KEY: process.env.REST_API_KEY,
MARIADB_HOST: process.env.MARIADB_HOST,
MARIADB_PORT: process.env.MARIADB_PORT,
MARIADB_DB: process.env.MARIADB_DB,
MARIADB_USER: process.env.MARIADB_USER,
MARIADB_PASSWORD: process.env.MARIADB_PASSWORD,
} }
export = env export = env

View File

@ -7,6 +7,10 @@ import ScreenAPI from "./apis/ScreenAPI"
import SelfAPI from "./apis/SelfAPI" import SelfAPI from "./apis/SelfAPI"
import TableAPI from "./apis/TableAPI" import TableAPI from "./apis/TableAPI"
import UserAPI from "./apis/UserAPI" import UserAPI from "./apis/UserAPI"
import DatasourcesAPI from "./apis/DatasourcesAPI"
import IntegrationsAPI from "./apis/IntegrationsAPI"
import QueriesAPI from "./apis/QueriesAPI"
import PermissionsAPI from "./apis/PermissionsAPI"
import BudibaseInternalAPIClient from "./BudibaseInternalAPIClient" import BudibaseInternalAPIClient from "./BudibaseInternalAPIClient"
import { State } from "../../types" import { State } from "../../types"
@ -22,6 +26,10 @@ export default class BudibaseInternalAPI {
self: SelfAPI self: SelfAPI
tables: TableAPI tables: TableAPI
users: UserAPI users: UserAPI
datasources: DatasourcesAPI
integrations: IntegrationsAPI
queries: QueriesAPI
permissions: PermissionsAPI
constructor(state: State) { constructor(state: State) {
this.client = new BudibaseInternalAPIClient(state) this.client = new BudibaseInternalAPIClient(state)
@ -35,5 +43,9 @@ export default class BudibaseInternalAPI {
this.self = new SelfAPI(this.client) this.self = new SelfAPI(this.client)
this.tables = new TableAPI(this.client) this.tables = new TableAPI(this.client)
this.users = new UserAPI(this.client) this.users = new UserAPI(this.client)
this.datasources = new DatasourcesAPI(this.client)
this.integrations = new IntegrationsAPI(this.client)
this.queries = new QueriesAPI(this.client)
this.permissions = new PermissionsAPI(this.client)
} }
} }

View File

@ -0,0 +1,95 @@
import { Response } from "node-fetch"
import {
Datasource,
CreateDatasourceResponse,
UpdateDatasourceResponse,
} from "@budibase/types"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
export default class DatasourcesAPI {
client: BudibaseInternalAPIClient
constructor(client: BudibaseInternalAPIClient) {
this.client = client
}
async getIntegrations(): Promise<[Response, any]> {
const [response, json] = await this.client.get(`/integrations`)
expect(response).toHaveStatusCode(200)
const integrationsCount = Object.keys(json).length
expect(integrationsCount).toBe(16)
return [response, json]
}
async getAll(): Promise<[Response, Datasource[]]> {
const [response, json] = await this.client.get(`/datasources`)
expect(response).toHaveStatusCode(200)
expect(json.length).toBeGreaterThan(0)
return [response, json]
}
async getTable(dataSourceId: string): Promise<[Response, Datasource]> {
const [response, json] = await this.client.get(
`/datasources/${dataSourceId}`
)
expect(response).toHaveStatusCode(200)
expect(json._id).toEqual(dataSourceId)
return [response, json]
}
async add(body: any): Promise<[Response, CreateDatasourceResponse]> {
const [response, json] = await this.client.post(`/datasources`, { body })
expect(response).toHaveStatusCode(200)
expect(json.datasource._id).toBeDefined()
expect(json.datasource._rev).toBeDefined()
return [response, json]
}
async update(body: any): Promise<[Response, UpdateDatasourceResponse]> {
const [response, json] = await this.client.put(`/datasources/${body._id}`, {
body,
})
expect(response).toHaveStatusCode(200)
expect(json.datasource._id).toBeDefined()
expect(json.datasource._rev).toBeDefined()
return [response, json]
}
async previewQuery(body: any): Promise<[Response, any]> {
const [response, json] = await this.client.post(`/queries/preview`, {
body,
})
expect(response).toHaveStatusCode(200)
return [response, json]
}
async saveQuery(body: any): Promise<[Response, any]> {
const [response, json] = await this.client.post(`/queries`, {
body,
})
expect(response).toHaveStatusCode(200)
return [response, json]
}
async getQuery(queryId: string): Promise<[Response, any]> {
const [response, json] = await this.client.get(`/queries/${queryId}`)
expect(response).toHaveStatusCode(200)
return [response, json]
}
async getQueryPermissions(queryId: string): Promise<[Response, any]> {
const [response, json] = await this.client.get(`/permissions/${queryId}`)
expect(response).toHaveStatusCode(200)
return [response, json]
}
async delete(dataSourceId: string, revId: string): Promise<Response> {
const [response, json] = await this.client.del(
`/datasources/${dataSourceId}/${revId}`
)
expect(response).toHaveStatusCode(200)
return response
}
}

View File

@ -0,0 +1,18 @@
import { Response } from "node-fetch"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
export default class IntegrationsAPI {
client: BudibaseInternalAPIClient
constructor(client: BudibaseInternalAPIClient) {
this.client = client
}
async getAll(): Promise<[Response, any]> {
const [response, json] = await this.client.get(`/integrations`)
expect(response).toHaveStatusCode(200)
const integrationsCount = Object.keys(json).length
expect(integrationsCount).toBeGreaterThan(0)
return [response, json]
}
}

View File

@ -0,0 +1,16 @@
import { Response } from "node-fetch"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
export default class PermissionsAPI {
client: BudibaseInternalAPIClient
constructor(client: BudibaseInternalAPIClient) {
this.client = client
}
async getAll(id: string): Promise<[Response, any]> {
const [response, json] = await this.client.get(`/permissions/${id}`)
expect(response).toHaveStatusCode(200)
return [response, json]
}
}

View File

@ -0,0 +1,33 @@
import { Response } from "node-fetch"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import { PreviewQueryRequest, Query } from "@budibase/types"
export default class DatasourcesAPI {
client: BudibaseInternalAPIClient
constructor(client: BudibaseInternalAPIClient) {
this.client = client
}
async preview(body: PreviewQueryRequest): Promise<[Response, any]> {
const [response, json] = await this.client.post(`/queries/preview`, {
body,
})
expect(response).toHaveStatusCode(200)
return [response, json]
}
async save(body: Query): Promise<[Response, any]> {
const [response, json] = await this.client.post(`/queries`, {
body,
})
expect(response).toHaveStatusCode(200)
return [response, json]
}
async get(queryId: string): Promise<[Response, any]> {
const [response, json] = await this.client.get(`/queries/${queryId}`)
expect(response).toHaveStatusCode(200)
return [response, json]
}
}

View File

@ -0,0 +1,71 @@
// Add information about the data source to the fixtures file from 1password
export const mongoDB = () => {
return {
datasource: {
name: "MongoDB",
source: "MONGODB",
type: "datasource",
config: {
connectionString: process.env.MONGODB_CONNECTION_STRING,
db: process.env.MONGODB_DB,
},
},
fetchSchema: false,
}
}
export const postgresSQL = () => {
return {
datasource: {
name: "PostgresSQL",
plus: true,
source: "POSTGRES",
type: "datasource",
config: {
database: process.env.POSTGRES_DB,
host: process.env.POSTGRES_HOST,
password: process.env.POSTGRES_PASSWORD,
port: process.env.POSTGRES_PORT,
schema: "public",
user: process.env.POSTGRES_USER,
},
},
fetchSchema: true,
}
}
export const mariaDB = () => {
return {
datasource: {
name: "MariaDB",
plus: true,
source: "MYSQL",
type: "datasource",
config: {
database: process.env.MARIADB_DB,
host: process.env.MARIADB_HOST,
password: process.env.MARIADB_PASSWORD,
port: process.env.MARIADB_PORT,
schema: "public",
user: process.env.MARIADB_USER,
},
},
fetchSchema: true,
}
}
export const restAPI = () => {
return {
datasource: {
name: "RestAPI",
source: "REST",
type: "datasource",
config: {
defaultHeaders: {},
rejectUnauthorized: true,
url: process.env.REST_API_BASE_URL,
},
},
fetchSchema: false,
}
}

View File

@ -4,3 +4,5 @@ export * as rows from "./rows"
export * as screens from "./screens" export * as screens from "./screens"
export * as tables from "./tables" export * as tables from "./tables"
export * as users from "./users" export * as users from "./users"
export * as datasources from "./datasources"
export * as queries from "./queries"

View File

@ -0,0 +1,123 @@
import { PreviewQueryRequest } from "@budibase/types"
const query = (datasourceId: string, fields: any): any => {
return {
datasourceId: datasourceId,
fields: fields,
name: "Query 1",
parameters: {},
queryVerb: "read",
schema: {},
transformer: "return data",
}
}
export const mariaDB = (datasourceId: string): PreviewQueryRequest => {
const fields = {
sql: "SELECT * FROM employees LIMIT 10;",
}
return query(datasourceId, fields)
}
export const mongoDB = (datasourceId: string): PreviewQueryRequest => {
const fields = {
extra: {
collection: "movies",
actionType: "find",
},
json: "",
}
return query(datasourceId, fields)
}
export const postgres = (datasourceId: string): PreviewQueryRequest => {
const fields = {
sql: "SELECT * FROM customers;",
}
return query(datasourceId, fields)
}
export const expectedSchemaFields = {
mariaDB: {
birth_date: "string",
emp_no: "number",
first_name: "string",
gender: "string",
hire_date: "string",
last_name: "string",
},
mongoDB: {
directors: "array",
genres: "array",
image: "string",
plot: "string",
rank: "number",
rating: "number",
release_date: "string",
running_time_secs: "number",
title: "string",
year: "number",
_id: "json",
},
postgres: {
address: "string",
city: "string",
company_name: "string",
contact_name: "string",
contact_title: "string",
country: "string",
customer_id: "string",
fax: "string",
phone: "string",
postal_code: "string",
region: "string",
},
restAPI: {
abilities: "array",
base_experience: "number",
forms: "array",
game_indices: "array",
height: "number",
held_items: "array",
id: "number",
is_default: "string",
location_area_encounters: "string",
moves: "array",
name: "string",
order: "number",
past_types: "array",
species: "json",
sprites: "json",
stats: "array",
types: "array",
weight: "number",
},
}
const request = (datasourceId: string, fields: any, flags: any): any => {
return {
datasourceId: datasourceId,
fields: fields,
flags: flags,
name: "Query 1",
parameters: {},
queryVerb: "read",
schema: {},
transformer: "return data",
}
}
export const restAPI = (datasourceId: string): PreviewQueryRequest => {
const fields = {
authConfigId: null,
bodyType: "none",
disabledHeaders: {},
headers: {},
pagination: {},
path: `${process.env.REST_API_BASE_URL}/pokemon/ditto`,
queryString: "",
}
const flags = {
urlName: true,
}
return request(datasourceId, fields, flags)
}

View File

@ -1,27 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
describe("Internal API - Data Sources", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Create an app with a data source", async () => {
// Create app
await config.createApp()
// Create Screen
const roleArray = ["BASIC", "POWER", "ADMIN", "PUBLIC"]
for (let role in roleArray) {
const [response, screen] = await config.api.screens.create(
fixtures.screens.generateScreen(roleArray[role])
)
}
})
})

View File

@ -0,0 +1,69 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
import { Query } from "@budibase/types"
describe("Internal API - Data Sources: MariaDB", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Create an app with a data source - MariaDB", async () => {
// Create app
await config.createApp()
// Get all integrations
await config.api.integrations.getAll()
// Add data source
const [dataSourceResponse, dataSourceJson] =
await config.api.datasources.add(fixtures.datasources.mariaDB())
// Update data source
const newDataSourceInfo = {
...dataSourceJson.datasource,
name: "MariaDB2",
}
const [updatedDataSourceResponse, updatedDataSourceJson] =
await config.api.datasources.update(newDataSourceInfo)
// Query data source
const [queryResponse, queryJson] = await config.api.queries.preview(
fixtures.queries.mariaDB(updatedDataSourceJson.datasource._id!)
)
expect(queryJson.rows.length).toEqual(10)
expect(queryJson.schemaFields).toEqual(
fixtures.queries.expectedSchemaFields.mariaDB
)
// Save query
const datasourcetoSave: Query = {
...fixtures.queries.mariaDB(updatedDataSourceJson.datasource._id!),
parameters: [],
}
const [saveQueryResponse, saveQueryJson] = await config.api.queries.save(
datasourcetoSave
)
// Get Query
const [getQueryResponse, getQueryJson] = await config.api.queries.get(
<string>saveQueryJson._id
)
// Get Query permissions
const [getQueryPermissionsResponse, getQueryPermissionsJson] =
await config.api.permissions.getAll(saveQueryJson._id!)
// Delete data source
const deleteResponse = await config.api.datasources.delete(
updatedDataSourceJson.datasource._id!,
updatedDataSourceJson.datasource._rev!
)
})
})

View File

@ -0,0 +1,69 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
import { Query } from "@budibase/types"
describe("Internal API - Data Sources: MongoDB", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Create an app with a data source - MongoDB", async () => {
// Create app
await config.createApp()
// Get all integrations
await config.api.integrations.getAll()
// Add data source
const [dataSourceResponse, dataSourceJson] =
await config.api.datasources.add(fixtures.datasources.mongoDB())
// Update data source
const newDataSourceInfo = {
...dataSourceJson.datasource,
name: "MongoDB2",
}
const [updatedDataSourceResponse, updatedDataSourceJson] =
await config.api.datasources.update(newDataSourceInfo)
// Query data source
const [queryResponse, queryJson] = await config.api.queries.preview(
fixtures.queries.mongoDB(updatedDataSourceJson.datasource._id!)
)
expect(queryJson.rows.length).toBeGreaterThan(10)
expect(queryJson.schemaFields).toEqual(
fixtures.queries.expectedSchemaFields.mongoDB
)
// Save query
const datasourcetoSave: Query = {
...fixtures.queries.mongoDB(updatedDataSourceJson.datasource._id!),
parameters: [],
}
const [saveQueryResponse, saveQueryJson] = await config.api.queries.save(
datasourcetoSave
)
// Get Query
const [getQueryResponse, getQueryJson] = await config.api.queries.get(
<string>saveQueryJson._id
)
// Get Query permissions
const [getQueryPermissionsResponse, getQueryPermissionsJson] =
await config.api.permissions.getAll(saveQueryJson._id!)
// Delete data source
const deleteResponse = await config.api.datasources.delete(
updatedDataSourceJson.datasource._id!,
updatedDataSourceJson.datasource._rev!
)
})
})

View File

@ -0,0 +1,69 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
import { Query } from "@budibase/types"
describe("Internal API - Data Sources: PostgresSQL", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Create an app with a data source - PostgresSQL", async () => {
// Create app
await config.createApp()
// Get all integrations
await config.api.integrations.getAll()
// Add data source
const [dataSourceResponse, dataSourceJson] =
await config.api.datasources.add(fixtures.datasources.postgresSQL())
// Update data source
const newDataSourceInfo = {
...dataSourceJson.datasource,
name: "PostgresSQL2",
}
const [updatedDataSourceResponse, updatedDataSourceJson] =
await config.api.datasources.update(newDataSourceInfo)
// Query data source
const [queryResponse, queryJson] = await config.api.queries.preview(
fixtures.queries.postgres(updatedDataSourceJson.datasource._id!)
)
expect(queryJson.rows.length).toEqual(91)
expect(queryJson.schemaFields).toEqual(
fixtures.queries.expectedSchemaFields.postgres
)
// Save query
const datasourcetoSave: Query = {
...fixtures.queries.postgres(updatedDataSourceJson.datasource._id!),
parameters: [],
}
const [saveQueryResponse, saveQueryJson] = await config.api.queries.save(
datasourcetoSave
)
// Get Query
const [getQueryResponse, getQueryJson] = await config.api.queries.get(
saveQueryJson._id!
)
// Get Query permissions
const [getQueryPermissionsResponse, getQueryPermissionsJson] =
await config.api.permissions.getAll(saveQueryJson._id!)
// Delete data source
const deleteResponse = await config.api.datasources.delete(
updatedDataSourceJson.datasource._id!,
updatedDataSourceJson.datasource._rev!
)
})
})

View File

@ -0,0 +1,69 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
import { Query } from "@budibase/types"
describe("Internal API - Data Sources: REST API", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Create an app with a data source - REST API", async () => {
// Create app
await config.createApp()
// Get all integrations
await config.api.integrations.getAll()
// Add data source
const [dataSourceResponse, dataSourceJson] =
await config.api.datasources.add(fixtures.datasources.restAPI())
// Update data source
const newDataSourceInfo = {
...dataSourceJson.datasource,
name: "RestAPI - Updated",
}
const [updatedDataSourceResponse, updatedDataSourceJson] =
await config.api.datasources.update(newDataSourceInfo)
// Query data source
const [queryResponse, queryJson] = await config.api.queries.preview(
fixtures.queries.restAPI(updatedDataSourceJson.datasource._id!)
)
expect(queryJson.rows.length).toEqual(1)
expect(queryJson.schemaFields).toEqual(
fixtures.queries.expectedSchemaFields.restAPI
)
// Save query
const datasourcetoSave: Query = {
...fixtures.queries.postgres(updatedDataSourceJson.datasource._id!),
parameters: [],
}
const [saveQueryResponse, saveQueryJson] = await config.api.queries.save(
datasourcetoSave
)
// Get Query
const [getQueryResponse, getQueryJson] = await config.api.queries.get(
saveQueryJson._id!
)
// Get Query permissions
const [getQueryPermissionsResponse, getQueryPermissionsJson] =
await config.api.permissions.getAll(saveQueryJson._id!)
// Delete data source
const deleteResponse = await config.api.datasources.delete(
updatedDataSourceJson.datasource._id!,
updatedDataSourceJson.datasource._rev!
)
})
})

View File

@ -8,6 +8,7 @@ export * from "./responseMessage"
export * from "./routing" export * from "./routing"
export * from "./state" export * from "./state"
export * from "./unpublishAppResponse" export * from "./unpublishAppResponse"
export * from "./addedDatasource"
// re-export public api types // re-export public api types
export * from "@budibase/server/api/controllers/public/mapping/types" export * from "@budibase/server/api/controllers/public/mapping/types"

View File

@ -1486,15 +1486,15 @@
pouchdb-promise "^6.0.4" pouchdb-promise "^6.0.4"
through2 "^2.0.0" through2 "^2.0.0"
"@budibase/pro@2.5.6-alpha.29": "@budibase/pro@2.5.6-alpha.41":
version "2.5.6-alpha.29" version "2.5.6-alpha.41"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.5.6-alpha.29.tgz#71414f68a296535ef53ffb0453352ea137c4aeab" resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.5.6-alpha.41.tgz#2632b398159211bf64bf6caff9753c877b6fc09d"
integrity sha512-tQuzMOo2WFxKvsUgYAfUEcLabRpmAD7hPlhBhCFzYasaXNbJiPhcwv4i52US0i0Wr2IXMb2X0d7fwa8tnbKzIA== integrity sha512-8Zfm+RtcXY77gdXXHti8QT+Vt+1ApuVqYL3KCkxBVbpzgQci/F3mipNXUX8bEy/y+QYAdgn6psDTWAqyr9VALg==
dependencies: dependencies:
"@budibase/backend-core" "2.5.6-alpha.29" "@budibase/backend-core" "2.5.6-alpha.41"
"@budibase/shared-core" "2.4.44-alpha.1" "@budibase/shared-core" "2.4.44-alpha.1"
"@budibase/string-templates" "2.4.44-alpha.1" "@budibase/string-templates" "2.4.44-alpha.1"
"@budibase/types" "2.5.6-alpha.29" "@budibase/types" "2.5.6-alpha.41"
"@koa/router" "8.0.8" "@koa/router" "8.0.8"
bull "4.10.1" bull "4.10.1"
joi "17.6.0" joi "17.6.0"