forked from tsai/budibase
16 changed files with 210 additions and 324 deletions
@ -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,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,170 @@ |
|||
import { quotas, StaticQuotaName, QuotaUsageType } from "@budibase/pro" |
|||
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: any = { |
|||
POST: 1, |
|||
DELETE: -1, |
|||
} |
|||
|
|||
const DOMAIN_MAP: any = { |
|||
rows: { |
|||
name: StaticQuotaName.ROWS, |
|||
type: QuotaUsageType.STATIC, |
|||
}, |
|||
applications: { |
|||
name: StaticQuotaName.APPS, |
|||
type: QuotaUsageType.STATIC, |
|||
}, |
|||
} |
|||
|
|||
function getQuotaInfo(url: string) { |
|||
for (let domain of Object.keys(DOMAIN_MAP)) { |
|||
if (url.indexOf(domain) !== -1) { |
|||
return DOMAIN_MAP[domain] |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = async (ctx: any, next: any) => { |
|||
if (!quotas.useQuotas()) { |
|||
return next() |
|||
} |
|||
|
|||
let usage = METHOD_MAP[ctx.req.method] |
|||
const quotaInfo = getQuotaInfo(ctx.req.url) |
|||
if (usage == null || quotaInfo == 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`) |
|||
} |
|||
} |
|||
} |
|||
try { |
|||
await performRequest(ctx, next, quotaInfo, usage) |
|||
} catch (err) { |
|||
ctx.throw(400, err) |
|||
} |
|||
} |
|||
|
|||
const performRequest = async ( |
|||
ctx: any, |
|||
next: any, |
|||
quotaInfo: any, |
|||
usage: number |
|||
) => { |
|||
const usageContext = { |
|||
skipNext: false, |
|||
skipUsage: false, |
|||
[StaticQuotaName.APPS]: {}, |
|||
} |
|||
|
|||
const quotaName = quotaInfo.name |
|||
|
|||
if (usage === -1) { |
|||
if (PRE_DELETE[quotaName]) { |
|||
await PRE_DELETE[quotaName](ctx, usageContext) |
|||
} |
|||
} else { |
|||
if (PRE_CREATE[quotaName]) { |
|||
await PRE_CREATE[quotaName](ctx, usageContext) |
|||
} |
|||
} |
|||
|
|||
// run the request
|
|||
if (!usageContext.skipNext) { |
|||
await quotas.updateUsage(usage, quotaName, quotaInfo.type, { dryRun: true }) |
|||
await next() |
|||
} |
|||
|
|||
if (usage === -1) { |
|||
if (POST_DELETE[quotaName]) { |
|||
await POST_DELETE[quotaName](ctx, usageContext) |
|||
} |
|||
} else { |
|||
if (POST_CREATE[quotaName]) { |
|||
await POST_CREATE[quotaName](ctx) |
|||
} |
|||
} |
|||
|
|||
// update the usage
|
|||
if (!usageContext.skipUsage) { |
|||
await quotas.updateUsage(usage, quotaName, quotaInfo.type) |
|||
} |
|||
} |
|||
|
|||
const appPreDelete = async (ctx: any, usageContext: any) => { |
|||
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[StaticQuotaName.APPS] = { rowCount: rows.length } |
|||
} |
|||
} |
|||
|
|||
const appPostDelete = async (ctx: any, usageContext: any) => { |
|||
// delete the app rows from usage
|
|||
const rowCount = usageContext[StaticQuotaName.ROWS].rowCount |
|||
if (rowCount) { |
|||
await quotas.updateUsage( |
|||
-rowCount, |
|||
StaticQuotaName.ROWS, |
|||
QuotaUsageType.STATIC |
|||
) |
|||
} |
|||
} |
|||
|
|||
const appPostCreate = async (ctx: any) => { |
|||
// 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 quotas.updateUsage( |
|||
rowCount, |
|||
StaticQuotaName.ROWS, |
|||
QuotaUsageType.STATIC |
|||
) |
|||
} |
|||
} |
|||
|
|||
const PRE_DELETE: any = { |
|||
[StaticQuotaName.APPS]: appPreDelete, |
|||
} |
|||
|
|||
const POST_DELETE: any = { |
|||
[StaticQuotaName.APPS]: appPostDelete, |
|||
} |
|||
|
|||
const PRE_CREATE: any = {} |
|||
|
|||
const POST_CREATE: any = { |
|||
[StaticQuotaName.APPS]: appPostCreate, |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
Loading…
Reference in new issue