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", |
"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,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