Merge branch 'develop' of github.com:Budibase/budibase into cypress-testing

This commit is contained in:
mike12345567 2022-01-21 13:37:05 +00:00
commit 7f1dab078b
50 changed files with 792 additions and 316 deletions

View File

@ -99,7 +99,9 @@ spec:
- name: PLATFORM_URL - name: PLATFORM_URL
value: {{ .Values.globals.platformUrl | quote }} value: {{ .Values.globals.platformUrl | quote }}
- name: USE_QUOTAS - name: USE_QUOTAS
value: "1" value: {{ .Values.globals.useQuotas | quote }}
- name: EXCLUDE_QUOTAS_TENANTS
value: {{ .Values.globals.excludeQuotasTenants | quote }}
- name: ACCOUNT_PORTAL_URL - name: ACCOUNT_PORTAL_URL
value: {{ .Values.globals.accountPortalUrl | quote }} value: {{ .Values.globals.accountPortalUrl | quote }}
- name: ACCOUNT_PORTAL_API_KEY - name: ACCOUNT_PORTAL_API_KEY

View File

@ -93,6 +93,8 @@ globals:
logLevel: info logLevel: info
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
useQuotas: "0"
excludeQuotasTenants: "" # comma seperated list of tenants to exclude from quotas
accountPortalUrl: "" accountPortalUrl: ""
accountPortalApiKey: "" accountPortalApiKey: ""
cookieDomain: "" cookieDomain: ""
@ -239,7 +241,8 @@ couchdb:
hosts: hosts:
- chart-example.local - chart-example.local
path: / path: /
annotations: [] annotations:
[]
# kubernetes.io/ingress.class: nginx # kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true" # kubernetes.io/tls-acme: "true"
tls: tls:

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.44-alpha.7", "version": "1.0.46-alpha.3",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -0,0 +1 @@
module.exports = require("./src/migrations")

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.0.44-alpha.7", "version": "1.0.46-alpha.3",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js", "main": "src/index.js",
"author": "Budibase", "author": "Budibase",

View File

@ -21,6 +21,7 @@ exports.StaticDatabases = {
name: "global-db", name: "global-db",
docs: { docs: {
apiKeys: "apikeys", apiKeys: "apikeys",
usageQuota: "usage_quota",
}, },
}, },
// contains information about tenancy and so on // contains information about tenancy and so on
@ -28,7 +29,6 @@ exports.StaticDatabases = {
name: "global-info", name: "global-info",
docs: { docs: {
tenants: "tenants", tenants: "tenants",
usageQuota: "usage_quota",
}, },
}, },
} }

View File

@ -450,7 +450,7 @@ async function getScopedConfig(db, params) {
function generateNewUsageQuotaDoc() { function generateNewUsageQuotaDoc() {
return { return {
_id: StaticDatabases.PLATFORM_INFO.docs.usageQuota, _id: StaticDatabases.GLOBAL.docs.usageQuota,
quotaReset: Date.now() + 2592000000, quotaReset: Date.now() + 2592000000,
usageQuota: { usageQuota: {
automationRuns: 0, automationRuns: 0,

View File

@ -14,4 +14,5 @@ module.exports = {
cache: require("../cache"), cache: require("../cache"),
auth: require("../auth"), auth: require("../auth"),
constants: require("../constants"), constants: require("../constants"),
migrations: require("../migrations"),
} }

View File

@ -1,5 +1,5 @@
const { DocumentTypes } = require("../db/constants") const { DocumentTypes } = require("../db/constants")
const { getGlobalDB } = require("../tenancy") const { getGlobalDB, getTenantId } = require("../tenancy")
exports.MIGRATION_DBS = { exports.MIGRATION_DBS = {
GLOBAL_DB: "GLOBAL_DB", GLOBAL_DB: "GLOBAL_DB",
@ -7,11 +7,13 @@ exports.MIGRATION_DBS = {
exports.MIGRATIONS = { exports.MIGRATIONS = {
USER_EMAIL_VIEW_CASING: "user_email_view_casing", USER_EMAIL_VIEW_CASING: "user_email_view_casing",
QUOTAS_1: "quotas_1",
} }
const DB_LOOKUP = { const DB_LOOKUP = {
[exports.MIGRATION_DBS.GLOBAL_DB]: [ [exports.MIGRATION_DBS.GLOBAL_DB]: [
exports.MIGRATIONS.USER_EMAIL_VIEW_CASING, exports.MIGRATIONS.USER_EMAIL_VIEW_CASING,
exports.MIGRATIONS.QUOTAS_1,
], ],
} }
@ -27,6 +29,7 @@ exports.getMigrationsDoc = async db => {
} }
exports.migrateIfRequired = async (migrationDb, migrationName, migrateFn) => { exports.migrateIfRequired = async (migrationDb, migrationName, migrateFn) => {
const tenantId = getTenantId()
try { try {
let db let db
if (migrationDb === exports.MIGRATION_DBS.GLOBAL_DB) { if (migrationDb === exports.MIGRATION_DBS.GLOBAL_DB) {
@ -47,15 +50,18 @@ exports.migrateIfRequired = async (migrationDb, migrationName, migrateFn) => {
return return
} }
console.log(`Performing migration: ${migrationName}`) console.log(`[Tenant: ${tenantId}] Performing migration: ${migrationName}`)
await migrateFn() await migrateFn()
console.log(`Migration complete: ${migrationName}`) console.log(`[Tenant: ${tenantId}] Migration complete: ${migrationName}`)
// mark as complete // mark as complete
doc[migrationName] = Date.now() doc[migrationName] = Date.now()
await db.put(doc) await db.put(doc)
} catch (err) { } catch (err) {
console.error(`Error performing migration: ${migrationName}: `, err) console.error(
`[Tenant: ${tenantId}] Error performing migration: ${migrationName}: `,
err
)
throw err throw err
} }
} }

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": "1.0.44-alpha.7", "version": "1.0.46-alpha.3",
"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",

View File

@ -35,7 +35,13 @@ Cypress.Commands.add("login", () => {
Cypress.Commands.add("createApp", name => { Cypress.Commands.add("createApp", name => {
cy.visit(`localhost:${Cypress.env("PORT")}/builder`) cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
cy.wait(500) cy.wait(500)
cy.contains(/Start from scratch/).dblclick() cy.request(`${Cypress.config().baseUrl}api/applications?status=all`)
.its("body")
.then(body => {
if (body.length > 0) {
cy.get(".spectrum-Button").contains("Create app").click({ force: true })
}
})
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Modal").within(() => {
cy.get("input").eq(0).type(name).should("have.value", name).blur() cy.get("input").eq(0).type(name).should("have.value", name).blur()
cy.get(".spectrum-ButtonGroup").contains("Create app").click() cy.get(".spectrum-ButtonGroup").contains("Create app").click()

View File

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

View File

@ -6,6 +6,7 @@ const apiCall =
method => method =>
async (url, body, headers = { "Content-Type": "application/json" }) => { async (url, body, headers = { "Content-Type": "application/json" }) => {
headers["x-budibase-app-id"] = svelteGet(store).appId headers["x-budibase-app-id"] = svelteGet(store).appId
headers["x-budibase-api-version"] = "1"
const json = headers["Content-Type"] === "application/json" const json = headers["Content-Type"] === "application/json"
const resp = await fetch(url, { const resp = await fetch(url, {
method: method, method: method,

View File

@ -53,16 +53,23 @@
} }
// Create table // Create table
const table = await tables.save(newTable) let table
notifications.success(`Table ${name} created successfully.`) try {
analytics.captureEvent(Events.TABLE.CREATED, { name }) table = await tables.save(newTable)
notifications.success(`Table ${name} created successfully.`)
analytics.captureEvent(Events.TABLE.CREATED, { name })
// Navigate to new table // Navigate to new table
const currentUrl = $url() const currentUrl = $url()
const path = currentUrl.endsWith("data") const path = currentUrl.endsWith("data")
? `./table/${table._id}` ? `./table/${table._id}`
: `../../table/${table._id}` : `../../table/${table._id}`
$goto(path) $goto(path)
} catch (e) {
notifications.error(e)
// reload in case the table was created
await tables.fetch()
}
} }
</script> </script>

View File

@ -55,8 +55,8 @@
<div> <div>
<BindingBuilder <BindingBuilder
bind:customParams={parameters.queryParams} bind:customParams={parameters.queryParams}
bindings={query.parameters} queryBindings={query.parameters}
bind:bindableOptions={bindings} bind:bindings
/> />
<IntegrationQueryEditor <IntegrationQueryEditor
height={200} height={200}

View File

@ -169,8 +169,8 @@
{#if getQueryParams(value).length > 0} {#if getQueryParams(value).length > 0}
<BindingBuilder <BindingBuilder
bind:customParams={tmpQueryParams} bind:customParams={tmpQueryParams}
bindings={getQueryParams(value)} queryBindings={getQueryParams(value)}
bind:bindableOptions={bindings} bind:bindings
/> />
{/if} {/if}
<IntegrationQueryEditor <IntegrationQueryEditor

View File

@ -7,27 +7,26 @@
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let bindable = true export let bindable = true
export let queryBindings = []
export let bindings = [] export let bindings = []
export let bindableOptions = []
export let customParams = {} export let customParams = {}
function newQueryBinding() { function newQueryBinding() {
bindings = [...bindings, {}] queryBindings = [...queryBindings, {}]
} }
$: console.log(bindings)
function deleteQueryBinding(idx) { function deleteQueryBinding(idx) {
bindings.splice(idx, 1) queryBindings.splice(idx, 1)
bindings = bindings queryBindings = queryBindings
} }
// This is necessary due to the way readable and writable bindings are stored. // This is necessary due to the way readable and writable bindings are stored.
// The readable binding in the UI gets converted to a UUID value that the client understands // The readable binding in the UI gets converted to a UUID value that the client understands
// for parsing, then converted back so we can display it the readable form in the UI // for parsing, then converted back so we can display it the readable form in the UI
function onBindingChange(param, valueToParse) { function onBindingChange(param, valueToParse) {
customParams[param] = readableToRuntimeBinding( customParams[param] = readableToRuntimeBinding(bindings, valueToParse)
bindableOptions,
valueToParse
)
} }
</script> </script>
@ -49,7 +48,7 @@
{/if} {/if}
</Body> </Body>
<div class="bindings" class:bindable> <div class="bindings" class:bindable>
{#each bindings as binding, idx} {#each queryBindings as binding, idx}
<Input <Input
placeholder="Binding Name" placeholder="Binding Name"
thin thin
@ -69,10 +68,10 @@
thin thin
on:change={evt => onBindingChange(binding.name, evt.detail)} on:change={evt => onBindingChange(binding.name, evt.detail)}
value={runtimeToReadableBinding( value={runtimeToReadableBinding(
bindableOptions, bindings,
customParams?.[binding.name] customParams?.[binding.name]
)} )}
{bindableOptions} {bindings}
/> />
{:else} {:else}
<Icon hoverable name="Close" on:click={() => deleteQueryBinding(idx)} /> <Icon hoverable name="Close" on:click={() => deleteQueryBinding(idx)} />

View File

@ -120,7 +120,7 @@
config={integrationInfo.extra} config={integrationInfo.extra}
/> />
{/if} {/if}
<BindingBuilder bind:bindings={query.parameters} bindable={false} /> <BindingBuilder bind:queryBindings={query.parameters} bindable={false} />
{/if} {/if}
</div> </div>
{#if shouldShowQueryConfig} {#if shouldShowQueryConfig}

View File

@ -11,7 +11,6 @@
import { capitalise } from "helpers" import { capitalise } from "helpers"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { APP_NAME_REGEX } from "constants" import { APP_NAME_REGEX } from "constants"
import TemplateList from "./TemplateList.svelte"
export let template export let template
export let inline export let inline
@ -147,58 +146,34 @@
} }
</script> </script>
{#if showTemplateSelection} <ModalContent
<ModalContent title={"Name your app"}
title={"Get started quickly"} confirmText={template?.fromFile ? "Import app" : "Create app"}
showConfirmButton={false} onConfirm={createNewApp}
size="L" onCancel={inline ? onCancel : null}
onConfirm={() => { cancelText={inline ? "Back" : undefined}
template = {} showCloseIcon={!inline}
return false disabled={!valid}
}} >
showCancelButton={!inline} {#if template?.fromFile}
showCloseIcon={!inline} <Dropzone
> error={$touched.file && $errors.file}
<TemplateList gallery={false}
onSelect={(selected, { useImport } = {}) => { label="File to import"
if (!selected) { value={[$values.file]}
template = useImport ? { fromFile: true } : {} on:change={e => {
return $values.file = e.detail?.[0]
} $touched.file = true
template = selected
}} }}
/> />
</ModalContent> {/if}
{:else} <Input
<ModalContent bind:value={$values.name}
title={"Name your app"} error={$touched.name && $errors.name}
confirmText={template?.fromFile ? "Import app" : "Create app"} on:blur={() => ($touched.name = true)}
onConfirm={createNewApp} label="Name"
onCancel={inline ? onCancel : null} placeholder={$auth.user.firstName
cancelText={inline ? "Back" : undefined} ? `${$auth.user.firstName}'s app`
showCloseIcon={!inline} : "My app"}
disabled={!valid} />
> </ModalContent>
{#if template?.fromFile}
<Dropzone
error={$touched.file && $errors.file}
gallery={false}
label="File to import"
value={[$values.file]}
on:change={e => {
$values.file = e.detail?.[0]
$touched.file = true
}}
/>
{/if}
<Input
bind:value={$values.name}
error={$touched.name && $errors.name}
on:blur={() => ($touched.name = true)}
label="Name"
placeholder={$auth.user.firstName
? `${$auth.user.firstName}'s app`
: "My app"}
/>
</ModalContent>
{/if}

View File

@ -1,69 +0,0 @@
<script>
import { Heading, Layout, Icon } from "@budibase/bbui"
export let onSelect
</script>
<Layout gap="XS" noPadding>
<div class="template start-from-scratch" on:click={() => onSelect(null)}>
<div
class="background-icon"
style={`background: rgb(50, 50, 50); color: white;`}
>
<Icon name="Add" />
</div>
<Heading size="XS">Start from scratch</Heading>
<p class="detail">BLANK</p>
</div>
<div
class="template import"
on:click={() => onSelect(null, { useImport: true })}
>
<div
class="background-icon"
style={`background: rgb(50, 50, 50); color: white;`}
>
<Icon name="Add" />
</div>
<Heading size="XS">Import an app</Heading>
<p class="detail">BLANK</p>
</div>
</Layout>
<style>
.background-icon {
padding: 10px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
width: 18px;
color: white;
}
.template {
min-height: 60px;
display: grid;
grid-gap: var(--layout-s);
grid-template-columns: auto 1fr auto;
border: 1px solid #494949;
align-items: center;
cursor: pointer;
border-radius: 4px;
background: var(--background-alt);
padding: 8px 16px;
}
.detail {
text-align: right;
}
.start-from-scratch {
background: var(--spectrum-global-color-gray-50);
margin-top: 20px;
}
.import {
background: var(--spectrum-global-color-gray-50);
}
</style>

View File

@ -11,6 +11,7 @@
notifications, notifications,
Body, Body,
Search, Search,
Icon,
} from "@budibase/bbui" } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte" import CreateAppModal from "components/start/CreateAppModal.svelte"
@ -27,6 +28,7 @@
import AppRow from "components/start/AppRow.svelte" import AppRow from "components/start/AppRow.svelte"
import { AppStatus } from "constants" import { AppStatus } from "constants"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
import Logo from "assets/bb-space-man.svg"
let sortBy = "name" let sortBy = "name"
let template let template
@ -78,6 +80,7 @@
} }
const initiateAppCreation = () => { const initiateAppCreation = () => {
template = {}
creationModal.show() creationModal.show()
creatingApp = true creatingApp = true
} }
@ -300,14 +303,26 @@
<div class="buttons"> <div class="buttons">
{#if cloud} {#if cloud}
<Button icon="Export" quiet secondary on:click={initiateAppsExport}> <Button
size="L"
icon="Export"
quiet
secondary
on:click={initiateAppsExport}
>
Export apps Export apps
</Button> </Button>
{/if} {/if}
<Button icon="Import" quiet secondary on:click={initiateAppImport}> <Button
icon="Import"
size="L"
quiet
secondary
on:click={initiateAppImport}
>
Import app Import app
</Button> </Button>
<Button icon="Add" cta on:click={initiateAppCreation}> <Button size="L" icon="Add" cta on:click={initiateAppCreation}>
Create app Create app
</Button> </Button>
</div> </div>
@ -389,9 +404,24 @@
{#if !enrichedApps.length && !creatingApp && loaded} {#if !enrichedApps.length && !creatingApp && loaded}
<div class="empty-wrapper"> <div class="empty-wrapper">
<Modal inline> <div class="centered">
<CreateAppModal {template} inline={true} /> <div class="main">
</Modal> <Layout gap="S" justifyItems="center">
<img class="img-size" alt="logo" src={Logo} />
<div class="new-screen-text">
<Detail size="M">Create a business app in minutes!</Detail>
</div>
<Button on:click={() => initiateAppCreation()} size="M" cta>
<div class="new-screen-button">
<div class="background-icon" style="color: white;">
<Icon name="Add" />
</div>
Create App
</div></Button
>
</Layout>
</div>
</div>
</div> </div>
{/if} {/if}
@ -474,12 +504,15 @@
} }
.grid { .grid {
height: 200px;
display: grid; display: grid;
overflow: hidden;
grid-gap: var(--spacing-xl); grid-gap: var(--spacing-xl);
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
grid-template-rows: minmax(70px, 1fr) minmax(100px, 1fr) minmax(0px, 0);
} }
.template-card { .template-card {
height: 80px; height: 70px;
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
border: 1px solid var(--spectrum-global-color-gray-300); border: 1px solid var(--spectrum-global-color-gray-300);
cursor: pointer; cursor: pointer;
@ -533,4 +566,42 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.centered {
width: calc(100% - 350px);
height: calc(100% - 100px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
width: 300px;
}
.new-screen-text {
width: 160px;
text-align: center;
color: #2c2c2c;
font-weight: 600;
}
.new-screen-button {
margin-left: 5px;
height: 20px;
width: 100px;
display: flex;
align-items: center;
}
.img-size {
width: 160px;
height: 160px;
}
.background-icon {
margin-top: 4px;
margin-right: 4px;
}
</style> </style>

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.0.44-alpha.7", "version": "1.0.46-alpha.3",
"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,9 +19,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.44-alpha.7", "@budibase/bbui": "^1.0.46-alpha.3",
"@budibase/standard-components": "^0.9.139", "@budibase/standard-components": "^0.9.139",
"@budibase/string-templates": "^1.0.44-alpha.7", "@budibase/string-templates": "^1.0.46-alpha.3",
"regexparam": "^1.3.0", "regexparam": "^1.3.0",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5" "svelte-spa-router": "^3.0.5"

View File

@ -110,12 +110,6 @@ export default class DataFetch {
*/ */
async getInitialData() { async getInitialData() {
const { datasource, filter, sortColumn, paginate } = this.options const { datasource, filter, sortColumn, paginate } = this.options
const tableId = datasource?.tableId
// Ensure table ID exists
if (!tableId) {
return
}
// Fetch datasource definition and determine feature flags // Fetch datasource definition and determine feature flags
const definition = await this.constructor.getDefinition(datasource) const definition = await this.constructor.getDefinition(datasource)

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.44-alpha.7", "version": "1.0.46-alpha.3",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -70,9 +70,9 @@
"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": "^1.0.44-alpha.7", "@budibase/backend-core": "^1.0.46-alpha.3",
"@budibase/client": "^1.0.44-alpha.7", "@budibase/client": "^1.0.46-alpha.3",
"@budibase/string-templates": "^1.0.44-alpha.7", "@budibase/string-templates": "^1.0.46-alpha.3",
"@bull-board/api": "^3.7.0", "@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0", "@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",

View File

@ -8,6 +8,7 @@ const {
getTable, getTable,
handleDataImport, handleDataImport,
} = require("./utils") } = require("./utils")
const usageQuota = require("../../../utilities/usageQuota")
exports.save = async function (ctx) { exports.save = async function (ctx) {
const appId = ctx.appId const appId = ctx.appId
@ -119,6 +120,7 @@ exports.destroy = async function (ctx) {
}) })
) )
await db.bulkDocs(rows.rows.map(row => ({ ...row.doc, _deleted: true }))) await db.bulkDocs(rows.rows.map(row => ({ ...row.doc, _deleted: true })))
await usageQuota.update(usageQuota.Properties.ROW, -rows.rows.length)
// update linked rows // update linked rows
await linkRows.updateLinks({ await linkRows.updateLinks({

View File

@ -16,6 +16,7 @@ const {
} = require("../../../integrations/utils") } = require("../../../integrations/utils")
const { getViews, saveView } = require("../view/utils") const { getViews, saveView } = require("../view/utils")
const viewTemplate = require("../view/viewBuilder") const viewTemplate = require("../view/viewBuilder")
const usageQuota = require("../../../utilities/usageQuota")
exports.checkForColumnUpdates = async (db, oldTable, updatedTable) => { exports.checkForColumnUpdates = async (db, oldTable, updatedTable) => {
let updatedRows = [] let updatedRows = []
@ -112,7 +113,11 @@ exports.handleDataImport = async (appId, user, table, dataImport) => {
finalData.push(row) finalData.push(row)
} }
await usageQuota.update(usageQuota.Properties.ROW, finalData.length, {
dryRun: true,
})
await db.bulkDocs(finalData) await db.bulkDocs(finalData)
await usageQuota.update(usageQuota.Properties.ROW, finalData.length)
let response = await db.put(table) let response = await db.put(table)
table._rev = response._rev table._rev = response._rev
return table return table

View File

@ -1,6 +1,5 @@
const rowController = require("../../api/controllers/row") const rowController = require("../../api/controllers/row")
const automationUtils = require("../automationUtils") const automationUtils = require("../automationUtils")
const env = require("../../environment")
const usage = require("../../utilities/usageQuota") const usage = require("../../utilities/usageQuota")
const { buildCtx } = require("./utils") const { buildCtx } = require("./utils")
@ -83,10 +82,9 @@ exports.run = async function ({ inputs, appId, emitter }) {
inputs.row.tableId, inputs.row.tableId,
inputs.row inputs.row
) )
if (env.USE_QUOTAS) { await usage.update(usage.Properties.ROW, 1, { dryRun: true })
await usage.update(usage.Properties.ROW, 1)
}
await rowController.save(ctx) await rowController.save(ctx)
await usage.update(usage.Properties.ROW, 1)
return { return {
row: inputs.row, row: inputs.row,
response: ctx.body, response: ctx.body,

View File

@ -1,5 +1,4 @@
const rowController = require("../../api/controllers/row") const rowController = require("../../api/controllers/row")
const env = require("../../environment")
const usage = require("../../utilities/usageQuota") const usage = require("../../utilities/usageQuota")
const { buildCtx } = require("./utils") const { buildCtx } = require("./utils")
const automationUtils = require("../automationUtils") const automationUtils = require("../automationUtils")
@ -74,9 +73,7 @@ exports.run = async function ({ inputs, appId, emitter }) {
}) })
try { try {
if (env.isProd()) { await usage.update(usage.Properties.ROW, -1)
await usage.update(usage.Properties.ROW, -1)
}
await rowController.destroy(ctx) await rowController.destroy(ctx)
return { return {
response: ctx.body, response: ctx.body,

View File

@ -38,6 +38,7 @@ module.exports = {
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
USE_QUOTAS: process.env.USE_QUOTAS, USE_QUOTAS: process.env.USE_QUOTAS,
EXCLUDE_QUOTAS_TENANTS: process.env.EXCLUDE_QUOTAS_TENANTS,
REDIS_URL: process.env.REDIS_URL, REDIS_URL: process.env.REDIS_URL,
REDIS_PASSWORD: process.env.REDIS_PASSWORD, REDIS_PASSWORD: process.env.REDIS_PASSWORD,
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,

View File

@ -1,11 +1,5 @@
jest.mock("../../db") jest.mock("../../db")
jest.mock("../../utilities/usageQuota") jest.mock("../../utilities/usageQuota")
jest.mock("../../environment", () => ({
isTest: () => true,
isProd: () => false,
isDev: () => true,
_set: () => {},
}))
jest.mock("@budibase/backend-core/tenancy", () => ({ jest.mock("@budibase/backend-core/tenancy", () => ({
getTenantId: () => "testing123" getTenantId: () => "testing123"
})) }))
@ -29,9 +23,10 @@ class TestConfiguration {
}, },
req: { req: {
method: "POST", method: "POST",
url: "/rows" url: "/applications"
} }
} }
usageQuota.useQuotas = () => true
} }
executeMiddleware() { executeMiddleware() {
@ -113,7 +108,6 @@ describe("usageQuota middleware", () => {
it("calculates and persists the correct usage quota for the relevant action", async () => { it("calculates and persists the correct usage quota for the relevant action", async () => {
config.setUrl("/rows") config.setUrl("/rows")
config.setProd(true)
await config.executeMiddleware() await config.executeMiddleware()
@ -121,20 +115,20 @@ describe("usageQuota middleware", () => {
expect(config.next).toHaveBeenCalled() expect(config.next).toHaveBeenCalled()
}) })
it("calculates the correct file size from a file upload call and adds it to quota", async () => { // it("calculates the correct file size from a file upload call and adds it to quota", async () => {
config.setUrl("/upload") // config.setUrl("/upload")
config.setProd(true) // config.setProd(true)
config.setFiles([ // config.setFiles([
{ // {
size: 100 // size: 100
}, // },
{ // {
size: 10000 // size: 10000
}, // },
]) // ])
await config.executeMiddleware() // await config.executeMiddleware()
expect(usageQuota.update).toHaveBeenCalledWith("storage", 10100) // expect(usageQuota.update).toHaveBeenCalledWith("storage", 10100)
expect(config.next).toHaveBeenCalled() // expect(config.next).toHaveBeenCalled()
}) // })
}) })

View File

@ -1,14 +1,11 @@
const CouchDB = require("../db") const CouchDB = require("../db")
const usageQuota = require("../utilities/usageQuota") const usageQuota = require("../utilities/usageQuota")
const env = require("../environment") const { getUniqueRows } = require("../utilities/usageQuota/rows")
const { getTenantId } = require("@budibase/backend-core/tenancy")
const { const {
isExternalTable, isExternalTable,
isRowId: isExternalRowId, isRowId: isExternalRowId,
} = require("../integrations/utils") } = require("../integrations/utils")
const migration = require("../migrations/usageQuotas")
// tenants without limits
const EXCLUDED_TENANTS = ["bb", "default", "bbtest", "bbstaging"]
// currently only counting new writes and deletes // currently only counting new writes and deletes
const METHOD_MAP = { const METHOD_MAP = {
@ -18,13 +15,13 @@ const METHOD_MAP = {
const DOMAIN_MAP = { const DOMAIN_MAP = {
rows: usageQuota.Properties.ROW, rows: usageQuota.Properties.ROW,
upload: usageQuota.Properties.UPLOAD, // upload: usageQuota.Properties.UPLOAD, // doesn't work yet
views: usageQuota.Properties.VIEW, // views: usageQuota.Properties.VIEW, // doesn't work yet
users: usageQuota.Properties.USER, // users: usageQuota.Properties.USER, // doesn't work yet
applications: usageQuota.Properties.APPS, applications: usageQuota.Properties.APPS,
// this will not be updated by endpoint calls // this will not be updated by endpoint calls
// instead it will be updated by triggerInfo // instead it will be updated by triggerInfo
automationRuns: usageQuota.Properties.AUTOMATION, // automationRuns: usageQuota.Properties.AUTOMATION, // doesn't work yet
} }
function getProperty(url) { function getProperty(url) {
@ -36,10 +33,7 @@ function getProperty(url) {
} }
module.exports = async (ctx, next) => { module.exports = async (ctx, next) => {
const tenantId = getTenantId() if (!usageQuota.useQuotas()) {
// if in development or a self hosted cloud usage quotas should not be executed
if (env.isDev() || env.SELF_HOSTED || EXCLUDED_TENANTS.includes(tenantId)) {
return next() return next()
} }
@ -80,9 +74,93 @@ module.exports = async (ctx, next) => {
usage = files.map(file => file.size).reduce((total, size) => total + size) usage = files.map(file => file.size).reduce((total, size) => total + size)
} }
try { try {
await usageQuota.update(property, usage) await migration.run()
return next() await performRequest(ctx, next, property, usage)
} catch (err) { } catch (err) {
ctx.throw(400, err) ctx.throw(400, err)
} }
} }
const performRequest = async (ctx, next, property, usage) => {
const usageContext = {
skipNext: false,
skipUsage: false,
[usageQuota.Properties.APPS]: {},
}
if (usage === -1) {
if (PRE_DELETE[property]) {
await PRE_DELETE[property](ctx, usageContext)
}
} else {
if (PRE_CREATE[property]) {
await PRE_CREATE[property](ctx, usageContext)
}
}
// run the request
if (!usageContext.skipNext) {
await usageQuota.update(property, usage, { dryRun: true })
await next()
}
if (usage === -1) {
if (POST_DELETE[property]) {
await POST_DELETE[property](ctx, usageContext)
}
} else {
if (POST_CREATE[property]) {
await POST_CREATE[property](ctx, usageContext)
}
}
// update the usage
if (!usageContext.skipUsage) {
await usageQuota.update(property, usage)
}
}
const appPreDelete = async (ctx, usageContext) => {
if (ctx.query.unpublish) {
// don't run usage decrement for unpublish
usageContext.skipUsage = true
return
}
// store the row count to delete
const rows = await getUniqueRows([ctx.appId])
if (rows.length) {
usageContext[usageQuota.Properties.APPS] = { rowCount: rows.length }
}
}
const appPostDelete = async (ctx, usageContext) => {
// delete the app rows from usage
const rowCount = usageContext[usageQuota.Properties.APPS].rowCount
if (rowCount) {
await usageQuota.update(usageQuota.Properties.ROW, -rowCount)
}
}
const appPostCreate = async ctx => {
// app import & template creation
if (ctx.request.body.useTemplate === "true") {
const rows = await getUniqueRows([ctx.response.body.appId])
const rowCount = rows ? rows.length : 0
await usageQuota.update(usageQuota.Properties.ROW, rowCount)
}
}
const PRE_DELETE = {
[usageQuota.Properties.APPS]: appPreDelete,
}
const POST_DELETE = {
[usageQuota.Properties.APPS]: appPostDelete,
}
const PRE_CREATE = {}
const POST_CREATE = {
[usageQuota.Properties.APPS]: appPostCreate,
}

View File

@ -0,0 +1,27 @@
const env = require("../../../environment")
const TestConfig = require("../../../tests/utilities/TestConfiguration")
const syncApps = jest.fn()
const syncRows = jest.fn()
jest.mock("../../usageQuotas/syncApps", () => ({ run: syncApps }) )
jest.mock("../../usageQuotas/syncRows", () => ({ run: syncRows }) )
const migrations = require("../../usageQuotas")
describe("run", () => {
let config = new TestConfig(false)
beforeEach(async () => {
await config.init()
env._set("USE_QUOTAS", 1)
})
afterAll(config.end)
it("runs the required migrations", async () => {
await migrations.run()
expect(syncApps).toHaveBeenCalledTimes(1)
expect(syncRows).toHaveBeenCalledTimes(1)
})
})

View File

@ -0,0 +1,37 @@
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
const TestConfig = require("../../../tests/utilities/TestConfiguration")
const { getUsageQuotaDoc, update, Properties } = require("../../../utilities/usageQuota")
const syncApps = require("../../usageQuotas/syncApps")
const env = require("../../../environment")
describe("syncApps", () => {
let config = new TestConfig(false)
beforeEach(async () => {
await config.init()
env._set("USE_QUOTAS", 1)
})
afterAll(config.end)
it("runs successfully", async () => {
// create the usage quota doc and mock usages
const db = getGlobalDB()
await getUsageQuotaDoc(db)
await update(Properties.APPS, 3)
let usageDoc = await getUsageQuotaDoc(db)
expect(usageDoc.usageQuota.apps).toEqual(3)
// create an extra app to test the migration
await config.createApp("quota-test")
// migrate
await syncApps.run()
// assert the migration worked
usageDoc = await getUsageQuotaDoc(db)
expect(usageDoc.usageQuota.apps).toEqual(2)
})
})

View File

@ -0,0 +1,43 @@
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
const TestConfig = require("../../../tests/utilities/TestConfiguration")
const { getUsageQuotaDoc, update, Properties } = require("../../../utilities/usageQuota")
const syncRows = require("../../usageQuotas/syncRows")
const env = require("../../../environment")
describe("syncRows", () => {
let config = new TestConfig(false)
beforeEach(async () => {
await config.init()
env._set("USE_QUOTAS", 1)
})
afterAll(config.end)
it("runs successfully", async () => {
// create the usage quota doc and mock usages
const db = getGlobalDB()
await getUsageQuotaDoc(db)
await update(Properties.ROW, 300)
let usageDoc = await getUsageQuotaDoc(db)
expect(usageDoc.usageQuota.rows).toEqual(300)
// app 1
await config.createTable()
await config.createRow()
// app 2
await config.createApp()
await config.createTable()
await config.createRow()
await config.createRow()
// migrate
await syncRows.run()
// assert the migration worked
usageDoc = await getUsageQuotaDoc(db)
expect(usageDoc.usageQuota.rows).toEqual(3)
})
})

View File

@ -0,0 +1,24 @@
const {
MIGRATIONS,
MIGRATION_DBS,
migrateIfRequired,
} = require("@budibase/backend-core/migrations")
const { useQuotas } = require("../../utilities/usageQuota")
const syncApps = require("./syncApps")
const syncRows = require("./syncRows")
exports.run = async () => {
if (!useQuotas()) {
return
}
// Jan 2022
await migrateIfRequired(
MIGRATION_DBS.GLOBAL_DB,
MIGRATIONS.QUOTAS_1,
async () => {
await syncApps.run()
await syncRows.run()
}
)
}

View File

@ -0,0 +1,18 @@
const { getGlobalDB, getTenantId } = require("@budibase/backend-core/tenancy")
const { getAllApps } = require("@budibase/backend-core/db")
const CouchDB = require("../../db")
const { getUsageQuotaDoc } = require("../../utilities/usageQuota")
exports.run = async () => {
const db = getGlobalDB()
// get app count
const devApps = await getAllApps(CouchDB, { dev: true })
const appCount = devApps ? devApps.length : 0
// sync app count
const tenantId = getTenantId()
console.log(`[Tenant: ${tenantId}] Syncing app count: ${appCount}`)
const usageDoc = await getUsageQuotaDoc(db)
usageDoc.usageQuota.apps = appCount
await db.put(usageDoc)
}

View File

@ -0,0 +1,21 @@
const { getGlobalDB, getTenantId } = require("@budibase/backend-core/tenancy")
const { getAllApps } = require("@budibase/backend-core/db")
const CouchDB = require("../../db")
const { getUsageQuotaDoc } = require("../../utilities/usageQuota")
const { getUniqueRows } = require("../../utilities/usageQuota/rows")
exports.run = async () => {
const db = getGlobalDB()
// get all rows in all apps
const allApps = await getAllApps(CouchDB, { all: true })
const appIds = allApps ? allApps.map(app => app.appId) : []
const rows = await getUniqueRows(appIds)
const rowCount = rows ? rows.length : 0
// sync row count
const tenantId = getTenantId()
console.log(`[Tenant: ${tenantId}] Syncing row count: ${rowCount}`)
const usageDoc = await getUsageQuotaDoc(db)
usageDoc.usageQuota.rows = rowCount
await db.put(usageDoc)
}

View File

@ -0,0 +1,72 @@
const getTenantId = jest.fn()
jest.mock("@budibase/backend-core/tenancy", () => ({
getTenantId
}))
const usageQuota = require("../../usageQuota")
const env = require("../../../environment")
class TestConfiguration {
constructor() {
this.enableQuotas()
}
enableQuotas = () => {
env.USE_QUOTAS = 1
}
disableQuotas = () => {
env.USE_QUOTAS = null
}
setTenantId = (tenantId) => {
getTenantId.mockReturnValue(tenantId)
}
setExcludedTenants = (tenants) => {
env.EXCLUDE_QUOTAS_TENANTS = tenants
}
reset = () => {
this.disableQuotas()
this.setExcludedTenants(null)
}
}
describe("usageQuota", () => {
let config
beforeEach(() => {
config = new TestConfiguration()
})
afterEach(() => {
config.reset()
})
describe("useQuotas", () => {
it("works when no settings have been provided", () => {
config.reset()
expect(usageQuota.useQuotas()).toBe(false)
})
it("honours USE_QUOTAS setting", () => {
config.disableQuotas()
expect(usageQuota.useQuotas()).toBe(false)
config.enableQuotas()
expect(usageQuota.useQuotas()).toBe(true)
})
it("honours EXCLUDE_QUOTAS_TENANTS setting", () => {
config.setTenantId("test")
// tenantId is in the list
config.setExcludedTenants("test, test2, test2")
expect(usageQuota.useQuotas()).toBe(false)
config.setExcludedTenants("test,test2,test2")
expect(usageQuota.useQuotas()).toBe(false)
// tenantId is not in the list
config.setTenantId("other")
expect(usageQuota.useQuotas()).toBe(true)
})
})
})

View File

@ -1,73 +0,0 @@
const env = require("../environment")
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
const {
StaticDatabases,
generateNewUsageQuotaDoc,
} = require("@budibase/backend-core/db")
function getNewQuotaReset() {
return Date.now() + 2592000000
}
exports.Properties = {
ROW: "rows",
UPLOAD: "storage",
VIEW: "views",
USER: "users",
AUTOMATION: "automationRuns",
APPS: "apps",
EMAILS: "emails",
}
async function getUsageQuotaDoc(db) {
let quota
try {
quota = await db.get(StaticDatabases.PLATFORM_INFO.docs.usageQuota)
} catch (err) {
// doc doesn't exist. Create it
quota = await db.post(generateNewUsageQuotaDoc())
}
return quota
}
/**
* Given a specified tenantId this will add to the usage object for the specified property.
* @param {string} property The property which is to be added to (within the nested usageQuota object).
* @param {number} usage The amount (this can be negative) to adjust the number by.
* @returns {Promise<void>} When this completes the API key will now be up to date - the quota period may have
* also been reset after this call.
*/
exports.update = async (property, usage) => {
if (!env.USE_QUOTAS) {
return
}
try {
const db = getGlobalDB()
const quota = await getUsageQuotaDoc(db)
// Check if the quota needs reset
if (Date.now() >= quota.quotaReset) {
quota.quotaReset = getNewQuotaReset()
for (let prop of Object.keys(quota.usageQuota)) {
quota.usageQuota[prop] = 0
}
}
// increment the quota
quota.usageQuota[property] += usage
if (quota.usageQuota[property] > quota.usageLimits[property]) {
throw new Error(
`You have exceeded your usage quota of ${quota.usageLimits[property]} ${property}.`
)
}
// update the usage quotas
await db.put(quota)
} catch (err) {
console.error(`Error updating usage quotas for ${property}`, err)
throw err
}
}

View File

@ -0,0 +1,92 @@
const env = require("../../environment")
const { getGlobalDB, getTenantId } = require("@budibase/backend-core/tenancy")
const {
StaticDatabases,
generateNewUsageQuotaDoc,
} = require("@budibase/backend-core/db")
exports.useQuotas = () => {
// check if quotas are enabled
if (env.USE_QUOTAS) {
// check if there are any tenants without limits
if (env.EXCLUDE_QUOTAS_TENANTS) {
const excludedTenants = env.EXCLUDE_QUOTAS_TENANTS.replace(
/\s/g,
""
).split(",")
const tenantId = getTenantId()
if (excludedTenants.includes(tenantId)) {
return false
}
}
return true
}
return false
}
exports.Properties = {
ROW: "rows",
UPLOAD: "storage", // doesn't work yet
VIEW: "views", // doesn't work yet
USER: "users", // doesn't work yet
AUTOMATION: "automationRuns", // doesn't work yet
APPS: "apps",
EMAILS: "emails", // doesn't work yet
}
exports.getUsageQuotaDoc = async db => {
let quota
try {
quota = await db.get(StaticDatabases.GLOBAL.docs.usageQuota)
} catch (err) {
// doc doesn't exist. Create it
quota = generateNewUsageQuotaDoc()
const response = await db.put(quota)
quota._rev = response.rev
}
return quota
}
/**
* Given a specified tenantId this will add to the usage object for the specified property.
* @param {string} property The property which is to be added to (within the nested usageQuota object).
* @param {number} usage The amount (this can be negative) to adjust the number by.
* @returns {Promise<void>} When this completes the API key will now be up to date - the quota period may have
* also been reset after this call.
*/
exports.update = async (property, usage, opts = { dryRun: false }) => {
if (!exports.useQuotas()) {
return
}
try {
const db = getGlobalDB()
const quota = await exports.getUsageQuotaDoc(db)
// increment the quota
quota.usageQuota[property] += usage
if (
quota.usageQuota[property] > quota.usageLimits[property] &&
usage > 0 // allow for decrementing usage when the quota is already exceeded
) {
throw new Error(
`You have exceeded your usage quota of ${quota.usageLimits[property]} ${property}.`
)
}
if (quota.usageQuota[property] < 0) {
// never go negative if the quota has previously been exceeded
quota.usageQuota[property] = 0
}
// update the usage quotas
if (!opts.dryRun) {
await db.put(quota)
}
} catch (err) {
console.error(`Error updating usage quotas for ${property}`, err)
throw err
}
}

View File

@ -0,0 +1,74 @@
const { getRowParams, USER_METDATA_PREFIX } = require("../../db/utils")
const CouchDB = require("../../db")
const { isDevAppID, getDevelopmentAppID } = require("@budibase/backend-core/db")
const ROW_EXCLUSIONS = [USER_METDATA_PREFIX]
const getAppPairs = appIds => {
// collect the app ids into dev / prod pairs
// keyed by the dev app id
const pairs = {}
for (let appId of appIds) {
const devId = getDevelopmentAppID(appId)
if (!pairs[devId]) {
pairs[devId] = {}
}
if (isDevAppID(appId)) {
pairs[devId].devId = appId
} else {
pairs[devId].prodId = appId
}
}
return pairs
}
const getAppRows = async appId => {
const appDb = new CouchDB(appId)
const response = await appDb.allDocs(
getRowParams(null, null, {
include_docs: false,
})
)
return response.rows
.map(r => r.id)
.filter(id => {
for (let exclusion of ROW_EXCLUSIONS) {
if (id.startsWith(exclusion)) {
return false
}
}
return true
})
}
/**
* Return a set of all rows in the given app ids.
* The returned rows will be unique on a per dev/prod app basis.
* Rows duplicates may exist across apps due to data import so they are not filtered out.
*/
exports.getUniqueRows = async appIds => {
let uniqueRows = []
const pairs = getAppPairs(appIds)
for (let pair of Object.values(pairs)) {
let appRows = []
for (let appId of [pair.devId, pair.prodId]) {
if (!appId) {
continue
}
try {
appRows.push(await getAppRows(appId))
} catch (e) {
console.error(e)
// don't error out if we can't count the app rows, just continue
}
}
// ensure uniqueness on a per app pair basis
// this can't be done on all rows because app import results in
// duplicate row ids across apps
uniqueRows = uniqueRows.concat(...new Set(appRows))
}
return uniqueRows
}

View File

@ -0,0 +1,18 @@
// UNUSED CODE
// Preserved for future use
/* eslint-disable no-unused-vars */
function getNewQuotaReset() {
return Date.now() + 2592000000
}
function resetQuotasIfRequired(quota) {
// Check if the quota needs reset
if (Date.now() >= quota.quotaReset) {
quota.quotaReset = getNewQuotaReset()
for (let prop of Object.keys(quota.usageQuota)) {
quota.usageQuota[prop] = 0
}
}
}

View File

@ -71,7 +71,7 @@ exports.getDeployedApps = async () => {
for (let [key, value] of Object.entries(json)) { for (let [key, value] of Object.entries(json)) {
if (value.url) { if (value.url) {
value.url = value.url.toLowerCase() value.url = value.url.toLowerCase()
apps[key] = value apps[key.toLowerCase()] = value
} }
} }
return apps return apps

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "1.0.44-alpha.7", "version": "1.0.46-alpha.3",
"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,5 +1,6 @@
const { atob } = require("../utilities") const { atob } = require("../utilities")
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")
const { LITERAL_MARKER } = require("../helpers/constants")
// The method of executing JS scripts depends on the bundle being built. // The method of executing JS scripts depends on the bundle being built.
// This setter is used in the entrypoint (either index.cjs or index.mjs). // This setter is used in the entrypoint (either index.cjs or index.mjs).
@ -46,8 +47,9 @@ module.exports.processJS = (handlebars, context) => {
$: path => getContextValue(path, cloneDeep(context)), $: path => getContextValue(path, cloneDeep(context)),
} }
// Create a sandbox with out context and run the JS // Create a sandbox with our context and run the JS
return runJS(js, sandboxContext) const res = { data: runJS(js, sandboxContext) }
return `{{${LITERAL_MARKER} js_result-${JSON.stringify(res)}}}`
} catch (error) { } catch (error) {
return "Error while executing JS" return "Error while executing JS"
} }

View File

@ -36,6 +36,11 @@ module.exports.processors = [
return value === "true" return value === "true"
case "object": case "object":
return JSON.parse(value) return JSON.parse(value)
case "js_result":
// We use the literal helper to process the result of JS expressions
// as we want to be able to return any types.
// We wrap the value in an abject to be able to use undefined properly.
return JSON.parse(value).data
} }
return value return value
}), }),

View File

@ -7,7 +7,7 @@ const processJS = (js, context) => {
describe("Test the JavaScript helper", () => { describe("Test the JavaScript helper", () => {
it("should execute a simple expression", () => { it("should execute a simple expression", () => {
const output = processJS(`return 1 + 2`) const output = processJS(`return 1 + 2`)
expect(output).toBe("3") expect(output).toBe(3)
}) })
it("should be able to use primitive bindings", () => { it("should be able to use primitive bindings", () => {
@ -50,6 +50,52 @@ describe("Test the JavaScript helper", () => {
expect(output).toBe("shazbat") expect(output).toBe("shazbat")
}) })
it("should be able to return an object", () => {
const output = processJS(`return $("foo")`, {
foo: {
bar: {
baz: "shazbat",
},
},
})
expect(output.bar.baz).toBe("shazbat")
})
it("should be able to return an array", () => {
const output = processJS(`return $("foo")`, {
foo: ["a", "b", "c"],
})
expect(output[2]).toBe("c")
})
it("should be able to return null", () => {
const output = processJS(`return $("foo")`, {
foo: null,
})
expect(output).toBe(null)
})
it("should be able to return undefined", () => {
const output = processJS(`return $("foo")`, {
foo: undefined,
})
expect(output).toBe(undefined)
})
it("should be able to return 0", () => {
const output = processJS(`return $("foo")`, {
foo: 0,
})
expect(output).toBe(0)
})
it("should be able to return an empty string", () => {
const output = processJS(`return $("foo")`, {
foo: "",
})
expect(output).toBe("")
})
it("should be able to use a deep array binding", () => { it("should be able to use a deep array binding", () => {
const output = processJS(`return $("foo.0.bar")`, { const output = processJS(`return $("foo.0.bar")`, {
foo: [ foo: [

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/worker", "name": "@budibase/worker",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.44-alpha.7", "version": "1.0.46-alpha.3",
"description": "Budibase background service", "description": "Budibase background service",
"main": "src/index.js", "main": "src/index.js",
"repository": { "repository": {
@ -29,8 +29,8 @@
"author": "Budibase", "author": "Budibase",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@budibase/backend-core": "^1.0.44-alpha.7", "@budibase/backend-core": "^1.0.46-alpha.3",
"@budibase/string-templates": "^1.0.44-alpha.7", "@budibase/string-templates": "^1.0.46-alpha.3",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"@sentry/node": "^6.0.0", "@sentry/node": "^6.0.0",
"@techpass/passport-openidconnect": "^0.3.0", "@techpass/passport-openidconnect": "^0.3.0",

View File

@ -73,16 +73,14 @@ exports.adminUser = async ctx => {
if (!env.SELF_HOSTED) { if (!env.SELF_HOSTED) {
// could be a scenario where it exists, make sure its clean // could be a scenario where it exists, make sure its clean
try { try {
const usageQuota = await db.get( const usageQuota = await db.get(StaticDatabases.GLOBAL.docs.usageQuota)
StaticDatabases.PLATFORM_INFO.docs.usageQuota
)
if (usageQuota) { if (usageQuota) {
await db.remove(usageQuota._id, usageQuota._rev) await db.remove(usageQuota._id, usageQuota._rev)
} }
} catch (err) { } catch (err) {
// don't worry about errors // don't worry about errors
} }
await db.post(generateNewUsageQuotaDoc()) await db.put(generateNewUsageQuotaDoc())
} }
if (response.rows.some(row => row.doc.admin)) { if (response.rows.some(row => row.doc.admin)) {