mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
106 changed files with 4001 additions and 8527 deletions
@ -0,0 +1,11 @@ |
|||
class BudibaseError extends Error { |
|||
constructor(message, type, code) { |
|||
super(message) |
|||
this.type = type |
|||
this.code = code |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
BudibaseError, |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
const licensing = require("./licensing") |
|||
|
|||
const codes = { |
|||
...licensing.codes, |
|||
} |
|||
|
|||
const types = { |
|||
...licensing.types, |
|||
} |
|||
|
|||
const context = { |
|||
...licensing.context, |
|||
} |
|||
|
|||
const getPublicError = err => { |
|||
let error |
|||
if (err.code || err.type) { |
|||
// add generic error information
|
|||
error = { |
|||
code: err.code, |
|||
type: err.type, |
|||
} |
|||
|
|||
if (err.code && context[err.code]) { |
|||
error = { |
|||
...error, |
|||
// get any additional context from this error
|
|||
...context[err.code](err), |
|||
} |
|||
} |
|||
} |
|||
|
|||
return error |
|||
} |
|||
|
|||
module.exports = { |
|||
codes, |
|||
types, |
|||
UsageLimitError: licensing.UsageLimitError, |
|||
getPublicError, |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
const { BudibaseError } = require("./base") |
|||
|
|||
const types = { |
|||
LICENSE_ERROR: "license_error", |
|||
} |
|||
|
|||
const codes = { |
|||
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded", |
|||
} |
|||
|
|||
const context = { |
|||
[codes.USAGE_LIMIT_EXCEEDED]: err => { |
|||
return { |
|||
limitName: err.limitName, |
|||
} |
|||
}, |
|||
} |
|||
|
|||
class UsageLimitError extends BudibaseError { |
|||
constructor(message, limitName) { |
|||
super(message, types.LICENSE_ERROR, codes.USAGE_LIMIT_EXCEEDED) |
|||
this.limitName = limitName |
|||
this.status = 400 |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
types, |
|||
codes, |
|||
context, |
|||
UsageLimitError, |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
const env = require("../environment") |
|||
const tenancy = require("../tenancy") |
|||
|
|||
/** |
|||
* Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant. |
|||
* The env var is formatted as: |
|||
* tenant1:feature1:feature2,tenant2:feature1 |
|||
*/ |
|||
const getFeatureFlags = () => { |
|||
if (!env.TENANT_FEATURE_FLAGS) { |
|||
return |
|||
} |
|||
|
|||
const tenantFeatureFlags = {} |
|||
|
|||
env.TENANT_FEATURE_FLAGS.split(",").forEach(tenantToFeatures => { |
|||
const [tenantId, ...features] = tenantToFeatures.split(":") |
|||
|
|||
features.forEach(feature => { |
|||
if (!tenantFeatureFlags[tenantId]) { |
|||
tenantFeatureFlags[tenantId] = [] |
|||
} |
|||
tenantFeatureFlags[tenantId].push(feature) |
|||
}) |
|||
}) |
|||
|
|||
return tenantFeatureFlags |
|||
} |
|||
|
|||
const TENANT_FEATURE_FLAGS = getFeatureFlags() |
|||
|
|||
exports.isEnabled = featureFlag => { |
|||
const tenantId = tenancy.getTenantId() |
|||
|
|||
return ( |
|||
TENANT_FEATURE_FLAGS && |
|||
TENANT_FEATURE_FLAGS[tenantId] && |
|||
TENANT_FEATURE_FLAGS[tenantId].includes(featureFlag) |
|||
) |
|||
} |
|||
|
|||
exports.getTenantFeatureFlags = tenantId => { |
|||
if (TENANT_FEATURE_FLAGS && TENANT_FEATURE_FLAGS[tenantId]) { |
|||
return TENANT_FEATURE_FLAGS[tenantId] |
|||
} |
|||
|
|||
return [] |
|||
} |
|||
|
|||
exports.FeatureFlag = { |
|||
LICENSING: "LICENSING", |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,14 @@ |
|||
import { auth } from "../stores/portal" |
|||
import { get } from "svelte/store" |
|||
|
|||
export const FEATURE_FLAGS = { |
|||
LICENSING: "LICENSING", |
|||
} |
|||
|
|||
export const isEnabled = featureFlag => { |
|||
const user = get(auth).user |
|||
if (user?.featureFlags?.includes(featureFlag)) { |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
@ -0,0 +1,151 @@ |
|||
<script> |
|||
import { |
|||
Layout, |
|||
Heading, |
|||
Body, |
|||
Divider, |
|||
Link, |
|||
Button, |
|||
Input, |
|||
Label, |
|||
notifications, |
|||
} from "@budibase/bbui" |
|||
import { auth, admin } from "stores/portal" |
|||
import { redirect } from "@roxi/routify" |
|||
import { processStringSync } from "@budibase/string-templates" |
|||
import { API } from "api" |
|||
import { onMount } from "svelte" |
|||
|
|||
$: license = $auth.user.license |
|||
$: upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade` |
|||
|
|||
$: activateDisabled = !licenseKey || licenseKeyDisabled |
|||
|
|||
let licenseInfo |
|||
|
|||
let licenseKeyDisabled = false |
|||
let licenseKeyType = "text" |
|||
let licenseKey = "" |
|||
|
|||
// Make sure page can't be visited directly in cloud |
|||
$: { |
|||
if ($admin.cloud) { |
|||
$redirect("../../portal") |
|||
} |
|||
} |
|||
|
|||
const activate = async () => { |
|||
await API.activateLicenseKey({ licenseKey }) |
|||
await auth.getSelf() |
|||
await setLicenseInfo() |
|||
notifications.success("Successfully activated") |
|||
} |
|||
|
|||
const refresh = async () => { |
|||
try { |
|||
await API.refreshLicense() |
|||
await auth.getSelf() |
|||
notifications.success("Refreshed license") |
|||
} catch (err) { |
|||
console.error(err) |
|||
notifications.error("Error refreshing license") |
|||
} |
|||
} |
|||
|
|||
// deactivate the license key field if there is a license key set |
|||
$: { |
|||
if (licenseInfo?.licenseKey) { |
|||
licenseKey = "**********************************************" |
|||
licenseKeyType = "password" |
|||
licenseKeyDisabled = true |
|||
activateDisabled = true |
|||
} |
|||
} |
|||
|
|||
const setLicenseInfo = async () => { |
|||
licenseInfo = await API.getLicenseInfo() |
|||
} |
|||
|
|||
onMount(async () => { |
|||
await setLicenseInfo() |
|||
}) |
|||
</script> |
|||
|
|||
{#if $auth.isAdmin} |
|||
<Layout noPadding> |
|||
<Layout gap="XS" noPadding> |
|||
<Heading size="M">Upgrade</Heading> |
|||
<Body size="M"> |
|||
{#if license.plan.type === "free"} |
|||
Upgrade your budibase installation to unlock additional features. To |
|||
subscribe to a plan visit your <Link size="L" href={upgradeUrl} |
|||
>Account</Link |
|||
>. |
|||
{:else} |
|||
To manage your plan visit your <Link size="L" href={upgradeUrl} |
|||
>Account</Link |
|||
>. |
|||
{/if} |
|||
</Body> |
|||
</Layout> |
|||
<Divider size="S" /> |
|||
<Layout gap="XS" noPadding> |
|||
<Heading size="S">Activate</Heading> |
|||
<Body size="S">Enter your license key below to activate your plan</Body> |
|||
</Layout> |
|||
<Layout noPadding> |
|||
<div class="fields"> |
|||
<div class="field"> |
|||
<Label size="L">License Key</Label> |
|||
<Input |
|||
thin |
|||
bind:value={licenseKey} |
|||
type={licenseKeyType} |
|||
disabled={licenseKeyDisabled} |
|||
/> |
|||
</div> |
|||
</div> |
|||
<div> |
|||
<Button cta on:click={activate} disabled={activateDisabled} |
|||
>Activate</Button |
|||
> |
|||
</div> |
|||
</Layout> |
|||
<Divider size="S" /> |
|||
<Layout gap="L" noPadding> |
|||
<Layout gap="S" noPadding> |
|||
<Heading size="S">Plan</Heading> |
|||
<Layout noPadding gap="XXS"> |
|||
<Body size="S">You are currently on the {license.plan.type} plan</Body |
|||
> |
|||
<Body size="XS"> |
|||
{processStringSync( |
|||
"Updated {{ duration time 'millisecond' }} ago", |
|||
{ |
|||
time: |
|||
new Date().getTime() - |
|||
new Date(license.refreshedAt).getTime(), |
|||
} |
|||
)} |
|||
</Body> |
|||
</Layout> |
|||
</Layout> |
|||
<div> |
|||
<Button secondary on:click={refresh}>Refresh</Button> |
|||
</div> |
|||
</Layout> |
|||
</Layout> |
|||
{/if} |
|||
|
|||
<style> |
|||
.fields { |
|||
display: grid; |
|||
grid-gap: var(--spacing-m); |
|||
} |
|||
.field { |
|||
display: grid; |
|||
grid-template-columns: 100px 1fr; |
|||
grid-gap: var(--spacing-l); |
|||
align-items: center; |
|||
} |
|||
</style> |
|||
File diff suppressed because it is too large
@ -1,14 +0,0 @@ |
|||
{ |
|||
// Use IntelliSense to learn about possible attributes. |
|||
// Hover to view descriptions of existing attributes. |
|||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 |
|||
"version": "0.2.0", |
|||
"configurations": [ |
|||
{ |
|||
"type": "node", |
|||
"request": "launch", |
|||
"name": "Publish Dev", |
|||
"program": "${workspaceFolder}/scripts/publishDev.js" |
|||
} |
|||
] |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,30 @@ |
|||
export const buildLicensingEndpoints = API => ({ |
|||
/** |
|||
* Activates a self hosted license key |
|||
*/ |
|||
activateLicenseKey: async data => { |
|||
return API.post({ |
|||
url: `/api/global/license/activate`, |
|||
body: data, |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* Get the license info - metadata about the license including the |
|||
* obfuscated license key. |
|||
*/ |
|||
getLicenseInfo: async () => { |
|||
return API.get({ |
|||
url: "/api/global/license/info", |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* Refreshes the license cache |
|||
*/ |
|||
refreshLicense: async () => { |
|||
return API.post({ |
|||
url: "/api/global/license/refresh", |
|||
}) |
|||
}, |
|||
}) |
|||
@ -1,5 +1,5 @@ |
|||
{ |
|||
"watch": ["src", "../backend-core"], |
|||
"watch": ["src", "../backend-core", "../../../budibase-pro/packages/pro"], |
|||
"ext": "js,ts,json", |
|||
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"], |
|||
"exec": "ts-node src/index.ts" |
|||
|
|||
@ -1,62 +0,0 @@ |
|||
const authRoutes = require("./auth") |
|||
const layoutRoutes = require("./layout") |
|||
const screenRoutes = require("./screen") |
|||
const userRoutes = require("./user") |
|||
const applicationRoutes = require("./application") |
|||
const tableRoutes = require("./table") |
|||
const rowRoutes = require("./row") |
|||
const viewRoutes = require("./view") |
|||
const staticRoutes = require("./static") |
|||
const componentRoutes = require("./component") |
|||
const automationRoutes = require("./automation") |
|||
const webhookRoutes = require("./webhook") |
|||
const roleRoutes = require("./role") |
|||
const deployRoutes = require("./deploy") |
|||
const apiKeysRoutes = require("./apikeys") |
|||
const templatesRoutes = require("./templates") |
|||
const analyticsRoutes = require("./analytics") |
|||
const routingRoutes = require("./routing") |
|||
const integrationRoutes = require("./integration") |
|||
const permissionRoutes = require("./permission") |
|||
const datasourceRoutes = require("./datasource") |
|||
const queryRoutes = require("./query") |
|||
const backupRoutes = require("./backup") |
|||
const metadataRoutes = require("./metadata") |
|||
const devRoutes = require("./dev") |
|||
const cloudRoutes = require("./cloud") |
|||
const migrationRoutes = require("./migrations") |
|||
const publicRoutes = require("./public") |
|||
|
|||
exports.mainRoutes = [ |
|||
authRoutes, |
|||
deployRoutes, |
|||
layoutRoutes, |
|||
screenRoutes, |
|||
userRoutes, |
|||
applicationRoutes, |
|||
automationRoutes, |
|||
viewRoutes, |
|||
componentRoutes, |
|||
roleRoutes, |
|||
apiKeysRoutes, |
|||
templatesRoutes, |
|||
analyticsRoutes, |
|||
webhookRoutes, |
|||
routingRoutes, |
|||
integrationRoutes, |
|||
permissionRoutes, |
|||
datasourceRoutes, |
|||
queryRoutes, |
|||
backupRoutes, |
|||
metadataRoutes, |
|||
devRoutes, |
|||
cloudRoutes, |
|||
// these need to be handled last as they still use /api/:tableId
|
|||
// this could be breaking as koa may recognise other routes as this
|
|||
tableRoutes, |
|||
rowRoutes, |
|||
migrationRoutes, |
|||
] |
|||
|
|||
exports.publicRoutes = publicRoutes |
|||
exports.staticRoutes = staticRoutes |
|||
@ -0,0 +1,60 @@ |
|||
import authRoutes from "./auth" |
|||
import layoutRoutes from "./layout" |
|||
import screenRoutes from "./screen" |
|||
import userRoutes from "./user" |
|||
import applicationRoutes from "./application" |
|||
import tableRoutes from "./table" |
|||
import rowRoutes from "./row" |
|||
import viewRoutes from "./view" |
|||
import componentRoutes from "./component" |
|||
import automationRoutes from "./automation" |
|||
import webhookRoutes from "./webhook" |
|||
import roleRoutes from "./role" |
|||
import deployRoutes from "./deploy" |
|||
import apiKeysRoutes from "./apikeys" |
|||
import templatesRoutes from "./templates" |
|||
import analyticsRoutes from "./analytics" |
|||
import routingRoutes from "./routing" |
|||
import integrationRoutes from "./integration" |
|||
import permissionRoutes from "./permission" |
|||
import datasourceRoutes from "./datasource" |
|||
import queryRoutes from "./query" |
|||
import backupRoutes from "./backup" |
|||
import metadataRoutes from "./metadata" |
|||
import devRoutes from "./dev" |
|||
import cloudRoutes from "./cloud" |
|||
import migrationRoutes from "./migrations" |
|||
|
|||
export { default as staticRoutes } from "./static" |
|||
export { default as publicRoutes } from "./public" |
|||
|
|||
export const mainRoutes = [ |
|||
authRoutes, |
|||
deployRoutes, |
|||
layoutRoutes, |
|||
screenRoutes, |
|||
userRoutes, |
|||
applicationRoutes, |
|||
automationRoutes, |
|||
viewRoutes, |
|||
componentRoutes, |
|||
roleRoutes, |
|||
apiKeysRoutes, |
|||
templatesRoutes, |
|||
analyticsRoutes, |
|||
webhookRoutes, |
|||
routingRoutes, |
|||
integrationRoutes, |
|||
permissionRoutes, |
|||
datasourceRoutes, |
|||
queryRoutes, |
|||
backupRoutes, |
|||
metadataRoutes, |
|||
devRoutes, |
|||
cloudRoutes, |
|||
// these need to be handled last as they still use /api/:tableId
|
|||
// this could be breaking as koa may recognise other routes as this
|
|||
tableRoutes, |
|||
rowRoutes, |
|||
migrationRoutes, |
|||
] |
|||
@ -1,134 +0,0 @@ |
|||
jest.mock("../../db") |
|||
jest.mock("../../utilities/usageQuota") |
|||
jest.mock("@budibase/backend-core/tenancy", () => ({ |
|||
getTenantId: () => "testing123" |
|||
})) |
|||
|
|||
const usageQuotaMiddleware = require("../usageQuota") |
|||
const usageQuota = require("../../utilities/usageQuota") |
|||
const CouchDB = require("../../db") |
|||
const env = require("../../environment") |
|||
|
|||
class TestConfiguration { |
|||
constructor() { |
|||
this.throw = jest.fn() |
|||
this.next = jest.fn() |
|||
this.middleware = usageQuotaMiddleware |
|||
this.ctx = { |
|||
throw: this.throw, |
|||
next: this.next, |
|||
appId: "test", |
|||
request: { |
|||
body: {} |
|||
}, |
|||
req: { |
|||
method: "POST", |
|||
url: "/applications" |
|||
} |
|||
} |
|||
usageQuota.useQuotas = () => true |
|||
} |
|||
|
|||
executeMiddleware() { |
|||
return this.middleware(this.ctx, this.next) |
|||
} |
|||
|
|||
setProd(bool) { |
|||
if (bool) { |
|||
env.isDev = () => false |
|||
env.isProd = () => true |
|||
this.ctx.user = { tenantId: "test" } |
|||
} else { |
|||
env.isDev = () => true |
|||
env.isProd = () => false |
|||
} |
|||
} |
|||
|
|||
setMethod(method) { |
|||
this.ctx.req.method = method |
|||
} |
|||
|
|||
setUrl(url) { |
|||
this.ctx.req.url = url |
|||
} |
|||
|
|||
setBody(body) { |
|||
this.ctx.request.body = body |
|||
} |
|||
|
|||
setFiles(files) { |
|||
this.ctx.request.files = { file: files } |
|||
} |
|||
} |
|||
|
|||
describe("usageQuota middleware", () => { |
|||
let config |
|||
|
|||
beforeEach(() => { |
|||
config = new TestConfiguration() |
|||
}) |
|||
|
|||
it("skips the middleware if there is no usage property or method", async () => { |
|||
await config.executeMiddleware() |
|||
expect(config.next).toHaveBeenCalled() |
|||
}) |
|||
|
|||
it("passes through to next middleware if document already exists", async () => { |
|||
config.setProd(true) |
|||
config.setBody({ |
|||
_id: "test", |
|||
_rev: "test", |
|||
}) |
|||
|
|||
CouchDB.mockImplementationOnce(() => ({ |
|||
get: async () => true |
|||
})) |
|||
|
|||
await config.executeMiddleware() |
|||
|
|||
expect(config.next).toHaveBeenCalled() |
|||
}) |
|||
|
|||
it("throws if request has _id, but the document no longer exists", async () => { |
|||
config.setBody({ |
|||
_id: "123", |
|||
_rev: "test", |
|||
}) |
|||
config.setProd(true) |
|||
|
|||
CouchDB.mockImplementationOnce(() => ({ |
|||
get: async () => { |
|||
throw new Error() |
|||
} |
|||
})) |
|||
|
|||
await config.executeMiddleware() |
|||
expect(config.throw).toHaveBeenCalledWith(404, `${config.ctx.request.body._id} does not exist`) |
|||
}) |
|||
|
|||
it("calculates and persists the correct usage quota for the relevant action", async () => { |
|||
config.setUrl("/rows") |
|||
|
|||
await config.executeMiddleware() |
|||
|
|||
expect(usageQuota.update).toHaveBeenCalledWith("rows", 1) |
|||
expect(config.next).toHaveBeenCalled() |
|||
}) |
|||
|
|||
// it("calculates the correct file size from a file upload call and adds it to quota", async () => {
|
|||
// config.setUrl("/upload")
|
|||
// config.setProd(true)
|
|||
// config.setFiles([
|
|||
// {
|
|||
// size: 100
|
|||
// },
|
|||
// {
|
|||
// size: 10000
|
|||
// },
|
|||
// ])
|
|||
// await config.executeMiddleware()
|
|||
|
|||
// expect(usageQuota.update).toHaveBeenCalledWith("storage", 10100)
|
|||
// expect(config.next).toHaveBeenCalled()
|
|||
// })
|
|||
}) |
|||
@ -1,164 +0,0 @@ |
|||
const usageQuota = require("../utilities/usageQuota") |
|||
const { getUniqueRows } = require("../utilities/usageQuota/rows") |
|||
const { |
|||
isExternalTable, |
|||
isRowId: isExternalRowId, |
|||
} = require("../integrations/utils") |
|||
const { getAppDB } = require("@budibase/backend-core/context") |
|||
|
|||
// currently only counting new writes and deletes
|
|||
const METHOD_MAP = { |
|||
POST: 1, |
|||
DELETE: -1, |
|||
} |
|||
|
|||
const DOMAIN_MAP = { |
|||
rows: usageQuota.Properties.ROW, |
|||
// upload: usageQuota.Properties.UPLOAD, // doesn't work yet
|
|||
// views: usageQuota.Properties.VIEW, // doesn't work yet
|
|||
// users: usageQuota.Properties.USER, // doesn't work yet
|
|||
applications: usageQuota.Properties.APPS, |
|||
// this will not be updated by endpoint calls
|
|||
// instead it will be updated by triggerInfo
|
|||
// automationRuns: usageQuota.Properties.AUTOMATION, // doesn't work yet
|
|||
} |
|||
|
|||
function getProperty(url) { |
|||
for (let domain of Object.keys(DOMAIN_MAP)) { |
|||
if (url.indexOf(domain) !== -1) { |
|||
return DOMAIN_MAP[domain] |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = async (ctx, next) => { |
|||
if (!usageQuota.useQuotas()) { |
|||
return next() |
|||
} |
|||
|
|||
let usage = METHOD_MAP[ctx.req.method] |
|||
const property = getProperty(ctx.req.url) |
|||
if (usage == null || property == null) { |
|||
return next() |
|||
} |
|||
// post request could be a save of a pre-existing entry
|
|||
if (ctx.request.body && ctx.request.body._id && ctx.request.body._rev) { |
|||
const usageId = ctx.request.body._id |
|||
try { |
|||
if (ctx.appId) { |
|||
const db = getAppDB() |
|||
await db.get(usageId) |
|||
} |
|||
return next() |
|||
} catch (err) { |
|||
if ( |
|||
isExternalTable(usageId) || |
|||
(ctx.request.body.tableId && |
|||
isExternalTable(ctx.request.body.tableId)) || |
|||
isExternalRowId(usageId) |
|||
) { |
|||
return next() |
|||
} else { |
|||
ctx.throw(404, `${usageId} does not exist`) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// update usage for uploads to be the total size
|
|||
if (property === usageQuota.Properties.UPLOAD) { |
|||
const files = |
|||
ctx.request.files.file.length > 1 |
|||
? Array.from(ctx.request.files.file) |
|||
: [ctx.request.files.file] |
|||
usage = files.map(file => file.size).reduce((total, size) => total + size) |
|||
} |
|||
try { |
|||
await performRequest(ctx, next, property, usage) |
|||
} catch (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, |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
const { createUserBuildersView } = require("@budibase/backend-core/db") |
|||
import * as syncDevelopers from "./usageQuotas/syncDevelopers" |
|||
|
|||
/** |
|||
* Date: |
|||
* March 2022 |
|||
* |
|||
* Description: |
|||
* Create the builder users view and sync the developer count |
|||
*/ |
|||
|
|||
export const run = async (db: any) => { |
|||
await createUserBuildersView(db) |
|||
await syncDevelopers.run() |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
import * as syncPublishedApps from "./usageQuotas/syncPublishedApps" |
|||
|
|||
/** |
|||
* Date: |
|||
* March 2022 |
|||
* |
|||
* Description: |
|||
* Sync the published apps count |
|||
*/ |
|||
|
|||
export const run = async (db: any) => { |
|||
await syncPublishedApps.run() |
|||
} |
|||
@ -1,8 +1,3 @@ |
|||
const { useQuotas } = require("../../../utilities/usageQuota") |
|||
|
|||
export const runQuotaMigration = async (migration: Function) => { |
|||
if (!useQuotas()) { |
|||
return |
|||
} |
|||
await migration() |
|||
} |
|||
|
|||
@ -0,0 +1,19 @@ |
|||
import { getTenantId } from "@budibase/backend-core/tenancy" |
|||
import { utils } from "@budibase/backend-core" |
|||
import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro" |
|||
|
|||
export const run = async () => { |
|||
// get developer count
|
|||
const developerCount = await utils.getBuildersCount() |
|||
|
|||
// sync developer count
|
|||
const tenantId = getTenantId() |
|||
console.log( |
|||
`[Tenant: ${tenantId}] Syncing developer count: ${developerCount}` |
|||
) |
|||
await quotas.setUsage( |
|||
developerCount, |
|||
StaticQuotaName.DEVELOPERS, |
|||
QuotaUsageType.STATIC |
|||
) |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
import { getTenantId } from "@budibase/backend-core/tenancy" |
|||
import { getAllApps } from "@budibase/backend-core/db" |
|||
import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro" |
|||
|
|||
export const run = async () => { |
|||
// get app count
|
|||
const opts: any = { dev: false } |
|||
const prodApps = await getAllApps(opts) |
|||
const prodAppCount = prodApps ? prodApps.length : 0 |
|||
|
|||
// sync app count
|
|||
const tenantId = getTenantId() |
|||
console.log( |
|||
`[Tenant: ${tenantId}] Syncing published app count: ${prodAppCount}` |
|||
) |
|||
await quotas.setUsage( |
|||
prodAppCount, |
|||
StaticQuotaName.PUBLISHED_APPS, |
|||
QuotaUsageType.STATIC |
|||
) |
|||
} |
|||
@ -1,37 +0,0 @@ |
|||
const { getGlobalDB } = require("@budibase/backend-core/tenancy") |
|||
const TestConfig = require("../../../../tests/utilities/TestConfiguration") |
|||
const { getUsageQuotaDoc, update, Properties } = require("../../../../utilities/usageQuota") |
|||
const syncApps = require("../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) |
|||
}) |
|||
}) |
|||
|
|||
@ -0,0 +1,32 @@ |
|||
import TestConfig from "../../../../tests/utilities/TestConfiguration" |
|||
import * as syncApps from "../syncApps" |
|||
import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro" |
|||
|
|||
describe("syncApps", () => { |
|||
let config = new TestConfig(false) |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
}) |
|||
|
|||
afterAll(config.end) |
|||
|
|||
it("runs successfully", async () => { |
|||
// create the usage quota doc and mock usages
|
|||
await quotas.getQuotaUsage() |
|||
await quotas.setUsage(3, StaticQuotaName.APPS, QuotaUsageType.STATIC) |
|||
|
|||
let usageDoc = await quotas.getQuotaUsage() |
|||
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 quotas.getQuotaUsage() |
|||
expect(usageDoc.usageQuota.apps).toEqual(2) |
|||
}) |
|||
}) |
|||
@ -1,43 +0,0 @@ |
|||
const { getGlobalDB } = require("@budibase/backend-core/tenancy") |
|||
const TestConfig = require("../../../../tests/utilities/TestConfiguration") |
|||
const { getUsageQuotaDoc, update, Properties } = require("../../../../utilities/usageQuota") |
|||
const syncRows = require("../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("second-app") |
|||
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) |
|||
}) |
|||
}) |
|||
|
|||
@ -0,0 +1,38 @@ |
|||
import TestConfig from "../../../../tests/utilities/TestConfiguration" |
|||
import * as syncRows from "../syncRows" |
|||
import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro" |
|||
|
|||
describe("syncRows", () => { |
|||
let config = new TestConfig(false) |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
}) |
|||
|
|||
afterAll(config.end) |
|||
|
|||
it("runs successfully", async () => { |
|||
// create the usage quota doc and mock usages
|
|||
await quotas.getQuotaUsage() |
|||
await quotas.setUsage(300, StaticQuotaName.ROWS, QuotaUsageType.STATIC) |
|||
|
|||
let usageDoc = await quotas.getQuotaUsage() |
|||
expect(usageDoc.usageQuota.rows).toEqual(300) |
|||
|
|||
// app 1
|
|||
await config.createTable() |
|||
await config.createRow() |
|||
// app 2
|
|||
await config.createApp("second-app") |
|||
await config.createTable() |
|||
await config.createRow() |
|||
await config.createRow() |
|||
|
|||
// migrate
|
|||
await syncRows.run() |
|||
|
|||
// assert the migration worked
|
|||
usageDoc = await quotas.getQuotaUsage() |
|||
expect(usageDoc.usageQuota.rows).toEqual(3) |
|||
}) |
|||
}) |
|||
@ -1,3 +1,7 @@ |
|||
declare module "@budibase/backend-core" |
|||
declare module "@budibase/backend-core/tenancy" |
|||
declare module "@budibase/backend-core/db" |
|||
declare module "@budibase/backend-core/context" |
|||
declare module "@budibase/backend-core/cache" |
|||
declare module "@budibase/backend-core/permissions" |
|||
declare module "@budibase/backend-core/roles" |
|||
|
|||
@ -1,72 +0,0 @@ |
|||
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) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -1,93 +0,0 @@ |
|||
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. |
|||
* @param {object} opts optional - options such as dryRun, to check what update will do. |
|||
* @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 |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
{ |
|||
// Used for building with tsc |
|||
"extends": "./tsconfig.json", |
|||
"exclude": [ |
|||
"node_modules", |
|||
"**/*.json", |
|||
"**/*.spec.js", |
|||
"**/*.spec.ts" |
|||
] |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,30 @@ |
|||
import { licensing, quotas } from "@budibase/pro" |
|||
|
|||
export const activate = async (ctx: any) => { |
|||
const { licenseKey } = ctx.request.body |
|||
if (!licenseKey) { |
|||
ctx.throw(400, "licenseKey is required") |
|||
} |
|||
|
|||
await licensing.activateLicenseKey(licenseKey) |
|||
ctx.status = 200 |
|||
} |
|||
|
|||
export const refresh = async (ctx: any) => { |
|||
await licensing.cache.refresh() |
|||
ctx.status = 200 |
|||
} |
|||
|
|||
export const getInfo = async (ctx: any) => { |
|||
const licenseInfo = await licensing.getLicenseInfo() |
|||
if (licenseInfo) { |
|||
licenseInfo.licenseKey = "*" |
|||
ctx.body = licenseInfo |
|||
} |
|||
ctx.status = 200 |
|||
} |
|||
|
|||
export const getQuotaUsage = async (ctx: any) => { |
|||
const usage = await quotas.getQuotaUsage() |
|||
ctx.body = usage |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue