forked from tsai/budibase
138 changed files with 4888 additions and 3454 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", |
||||
|
} |
||||
@ -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> |
||||
@ -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" |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
@ -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, |
||||
|
] |
||||
@ -0,0 +1,50 @@ |
|||||
|
exports.definition = { |
||||
|
name: "Looping", |
||||
|
icon: "Reuse", |
||||
|
tagline: "Loop the block", |
||||
|
description: "Loop", |
||||
|
stepId: "LOOP", |
||||
|
internal: true, |
||||
|
inputs: {}, |
||||
|
schema: { |
||||
|
inputs: { |
||||
|
properties: { |
||||
|
option: { |
||||
|
customType: "loopOption", |
||||
|
title: "Input type", |
||||
|
}, |
||||
|
binding: { |
||||
|
type: "string", |
||||
|
title: "Binding / Value", |
||||
|
}, |
||||
|
iterations: { |
||||
|
type: "number", |
||||
|
title: "Max loop iterations", |
||||
|
}, |
||||
|
failure: { |
||||
|
type: "string", |
||||
|
title: "Failure Condition", |
||||
|
}, |
||||
|
}, |
||||
|
required: ["type", "value", "iterations", "failure"], |
||||
|
}, |
||||
|
outputs: { |
||||
|
properties: { |
||||
|
items: { |
||||
|
customType: "item", |
||||
|
description: "The item currently being executed", |
||||
|
}, |
||||
|
success: { |
||||
|
type: "boolean", |
||||
|
description: "Whether the message loop was successfully", |
||||
|
}, |
||||
|
iterations: { |
||||
|
type: "number", |
||||
|
descriptions: "The amount of times the block ran", |
||||
|
}, |
||||
|
}, |
||||
|
required: ["success", "items", "iterations"], |
||||
|
}, |
||||
|
}, |
||||
|
type: "LOGIC", |
||||
|
} |
||||
@ -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() |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue