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", |
"ext": "js,ts,json", |
||||
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"], |
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"], |
||||
"exec": "ts-node src/index.ts" |
"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) => { |
export const runQuotaMigration = async (migration: Function) => { |
||||
if (!useQuotas()) { |
|
||||
return |
|
||||
} |
|
||||
await migration() |
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" |
||||
declare module "@budibase/backend-core/tenancy" |
declare module "@budibase/backend-core/tenancy" |
||||
declare module "@budibase/backend-core/db" |
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