Published apps, automations and query count quotas

This commit is contained in:
Rory Powell 2022-03-20 01:13:54 +00:00
parent 661367333d
commit 795b48bfb0
22 changed files with 344 additions and 439 deletions

View File

@ -21,7 +21,7 @@ const getPublicError = err => {
type: err.type, type: err.type,
} }
if (err.code) { if (err.code && context[err.code]) {
error = { error = {
...error, ...error,
// get any additional context from this error // get any additional context from this error

View File

@ -28,7 +28,7 @@
aria-valuenow={$progress} aria-valuenow={$progress}
aria-valuemin="0" aria-valuemin="0"
aria-valuemax="100" aria-valuemax="100"
style={width ? `width: ${width}px;` : ""} style={width ? `width: ${width};` : ""}
> >
{#if $$slots} {#if $$slots}
<div <div

View File

@ -1,29 +1,29 @@
const env = require("../../environment") import env from "../../environment"
const packageJson = require("../../../package.json") import packageJson from "../../../package.json"
const { import {
createLinkView, createLinkView,
createRoutingView, createRoutingView,
createAllSearchIndex, createAllSearchIndex,
} = require("../../db/views/staticViews") } from "../../db/views/staticViews"
const { import {
getTemplateStream, getTemplateStream,
createApp, createApp,
deleteApp, deleteApp,
} = require("../../utilities/fileSystem") } from "../../utilities/fileSystem"
const { import {
generateAppID, generateAppID,
getLayoutParams, getLayoutParams,
getScreenParams, getScreenParams,
generateDevAppID, generateDevAppID,
DocumentTypes, DocumentTypes,
AppStatus, AppStatus,
} = require("../../db/utils") } from "../../db/utils"
const { const {
BUILTIN_ROLE_IDS, BUILTIN_ROLE_IDS,
AccessController, AccessController,
} = require("@budibase/backend-core/roles") } = require("@budibase/backend-core/roles")
const { BASE_LAYOUTS } = require("../../constants/layouts") import { BASE_LAYOUTS } from "../../constants/layouts"
const { cloneDeep } = require("lodash/fp") import { cloneDeep } from "lodash/fp"
const { processObject } = require("@budibase/string-templates") const { processObject } = require("@budibase/string-templates")
const { const {
getAllApps, getAllApps,
@ -31,24 +31,27 @@ const {
getProdAppID, getProdAppID,
Replication, Replication,
} = require("@budibase/backend-core/db") } = require("@budibase/backend-core/db")
const { USERS_TABLE_SCHEMA } = require("../../constants") import { USERS_TABLE_SCHEMA } from "../../constants"
const { removeAppFromUserRoles } = require("../../utilities/workerRequests") import { removeAppFromUserRoles } from "../../utilities/workerRequests"
const { clientLibraryPath, stringToReadStream } = require("../../utilities") import { clientLibraryPath, stringToReadStream } from "../../utilities"
const { getAllLocks } = require("../../utilities/redis") import { getAllLocks } from "../../utilities/redis"
const { import {
updateClientLibrary, updateClientLibrary,
backupClientLibrary, backupClientLibrary,
revertClientLibrary, revertClientLibrary,
} = require("../../utilities/fileSystem/clientLibrary") } from "../../utilities/fileSystem/clientLibrary"
const { getTenantId, isMultiTenant } = require("@budibase/backend-core/tenancy") const { getTenantId, isMultiTenant } = require("@budibase/backend-core/tenancy")
const { syncGlobalUsers } = require("./user") import { syncGlobalUsers } from "./user"
const { app: appCache } = require("@budibase/backend-core/cache") const { app: appCache } = require("@budibase/backend-core/cache")
const { cleanupAutomations } = require("../../automations/utils") import { cleanupAutomations } from "../../automations/utils"
const { const {
getAppDB, getAppDB,
getProdAppDB, getProdAppDB,
updateAppId, updateAppId,
} = require("@budibase/backend-core/context") } = require("@budibase/backend-core/context")
import { getUniqueRows } from "../../utilities/usageQuota/rows"
import { quotas } from "@budibase/pro"
import { errors } from "@budibase/backend-core"
const URL_REGEX_SLASH = /\/|\\/g const URL_REGEX_SLASH = /\/|\\/g
@ -61,7 +64,7 @@ async function getLayouts() {
include_docs: true, include_docs: true,
}) })
) )
).rows.map(row => row.doc) ).rows.map((row: any) => row.doc)
} }
async function getScreens() { async function getScreens() {
@ -72,16 +75,16 @@ async function getScreens() {
include_docs: true, include_docs: true,
}) })
) )
).rows.map(row => row.doc) ).rows.map((row: any) => row.doc)
} }
function getUserRoleId(ctx) { function getUserRoleId(ctx: any) {
return !ctx.user.role || !ctx.user.role._id return !ctx.user.role || !ctx.user.role._id
? BUILTIN_ROLE_IDS.PUBLIC ? BUILTIN_ROLE_IDS.PUBLIC
: ctx.user.role._id : ctx.user.role._id
} }
exports.getAppUrl = ctx => { export const getAppUrl = (ctx: any) => {
// construct the url // construct the url
let url let url
if (ctx.request.body.url) { if (ctx.request.body.url) {
@ -97,29 +100,34 @@ exports.getAppUrl = ctx => {
return url return url
} }
const checkAppUrl = (ctx, apps, url, currentAppId) => { const checkAppUrl = (ctx: any, apps: any, url: any, currentAppId?: string) => {
if (currentAppId) { if (currentAppId) {
apps = apps.filter(app => app.appId !== currentAppId) apps = apps.filter((app: any) => app.appId !== currentAppId)
} }
if (apps.some(app => app.url === url)) { if (apps.some((app: any) => app.url === url)) {
ctx.throw(400, "App URL is already in use.") ctx.throw(400, "App URL is already in use.")
} }
} }
const checkAppName = (ctx, apps, name, currentAppId) => { const checkAppName = (
ctx: any,
apps: any,
name: any,
currentAppId?: string
) => {
// TODO: Replace with Joi // TODO: Replace with Joi
if (!name) { if (!name) {
ctx.throw(400, "Name is required") ctx.throw(400, "Name is required")
} }
if (currentAppId) { if (currentAppId) {
apps = apps.filter(app => app.appId !== currentAppId) apps = apps.filter((app: any) => app.appId !== currentAppId)
} }
if (apps.some(app => app.name === name)) { if (apps.some((app: any) => app.name === name)) {
ctx.throw(400, "App name is already in use.") ctx.throw(400, "App name is already in use.")
} }
} }
async function createInstance(template) { async function createInstance(template: any) {
const tenantId = isMultiTenant() ? getTenantId() : null const tenantId = isMultiTenant() ? getTenantId() : null
const baseAppId = generateAppID(tenantId) const baseAppId = generateAppID(tenantId)
const appId = generateDevAppID(baseAppId) const appId = generateDevAppID(baseAppId)
@ -160,7 +168,7 @@ async function createInstance(template) {
return { _id: appId } return { _id: appId }
} }
exports.fetch = async ctx => { export const fetch = async (ctx: any) => {
const dev = ctx.query && ctx.query.status === AppStatus.DEV const dev = ctx.query && ctx.query.status === AppStatus.DEV
const all = ctx.query && ctx.query.status === AppStatus.ALL const all = ctx.query && ctx.query.status === AppStatus.ALL
const apps = await getAllApps({ dev, all }) const apps = await getAllApps({ dev, all })
@ -172,7 +180,7 @@ exports.fetch = async ctx => {
if (app.status !== "development") { if (app.status !== "development") {
continue continue
} }
const lock = locks.find(lock => lock.appId === app.appId) const lock = locks.find((lock: any) => lock.appId === app.appId)
if (lock) { if (lock) {
app.lockedBy = lock.user app.lockedBy = lock.user
} else { } else {
@ -185,7 +193,7 @@ exports.fetch = async ctx => {
ctx.body = apps ctx.body = apps
} }
exports.fetchAppDefinition = async ctx => { export const fetchAppDefinition = async (ctx: any) => {
const layouts = await getLayouts() const layouts = await getLayouts()
const userRoleId = getUserRoleId(ctx) const userRoleId = getUserRoleId(ctx)
const accessController = new AccessController() const accessController = new AccessController()
@ -200,7 +208,7 @@ exports.fetchAppDefinition = async ctx => {
} }
} }
exports.fetchAppPackage = async ctx => { export const fetchAppPackage = async (ctx: any) => {
const db = getAppDB() const db = getAppDB()
const application = await db.get(DocumentTypes.APP_METADATA) const application = await db.get(DocumentTypes.APP_METADATA)
const layouts = await getLayouts() const layouts = await getLayouts()
@ -221,7 +229,7 @@ exports.fetchAppPackage = async ctx => {
} }
} }
exports.create = async ctx => { const performAppCreate = async (ctx: any) => {
const apps = await getAllApps({ dev: true }) const apps = await getAllApps({ dev: true })
const name = ctx.request.body.name const name = ctx.request.body.name
checkAppName(ctx, apps, name) checkAppName(ctx, apps, name)
@ -229,7 +237,7 @@ exports.create = async ctx => {
checkAppUrl(ctx, apps, url) checkAppUrl(ctx, apps, url)
const { useTemplate, templateKey, templateString } = ctx.request.body const { useTemplate, templateKey, templateString } = ctx.request.body
const instanceConfig = { const instanceConfig: any = {
useTemplate, useTemplate,
key: templateKey, key: templateKey,
templateString, templateString,
@ -279,13 +287,41 @@ exports.create = async ctx => {
} }
await appCache.invalidateAppMetadata(appId, newApplication) await appCache.invalidateAppMetadata(appId, newApplication)
ctx.status = 200 return newApplication
}
const appPostCreate = async (ctx: any, appId: string) => {
// app import & template creation
if (ctx.request.body.useTemplate === "true") {
const rows = await getUniqueRows([appId])
const rowCount = rows ? rows.length : 0
if (rowCount) {
try {
await quotas.addRows(rowCount)
} catch (err: any) {
if (err.code && err.code === errors.codes.USAGE_LIMIT_EXCEEDED) {
// this import resulted in row usage exceeding the quota
// delete the app
// skip pre and post steps as no rows have been added to quotas yet
ctx.params.appId = appId
await destroyApp(ctx)
}
throw err
}
}
}
}
export const create = async (ctx: any) => {
const newApplication = await quotas.addApp(() => performAppCreate(ctx))
await appPostCreate(ctx, newApplication.appId)
ctx.body = newApplication ctx.body = newApplication
ctx.status = 200
} }
// This endpoint currently operates as a PATCH rather than a PUT // This endpoint currently operates as a PATCH rather than a PUT
// Thus name and url fields are handled only if present // Thus name and url fields are handled only if present
exports.update = async ctx => { export const update = async (ctx: any) => {
const apps = await getAllApps({ dev: true }) const apps = await getAllApps({ dev: true })
// validation // validation
const name = ctx.request.body.name const name = ctx.request.body.name
@ -303,7 +339,7 @@ exports.update = async ctx => {
ctx.body = data ctx.body = data
} }
exports.updateClient = async ctx => { export const updateClient = async (ctx: any) => {
// Get current app version // Get current app version
const db = getAppDB() const db = getAppDB()
const application = await db.get(DocumentTypes.APP_METADATA) const application = await db.get(DocumentTypes.APP_METADATA)
@ -325,7 +361,7 @@ exports.updateClient = async ctx => {
ctx.body = data ctx.body = data
} }
exports.revertClient = async ctx => { export const revertClient = async (ctx: any) => {
// Check app can be reverted // Check app can be reverted
const db = getAppDB() const db = getAppDB()
const application = await db.get(DocumentTypes.APP_METADATA) const application = await db.get(DocumentTypes.APP_METADATA)
@ -348,10 +384,15 @@ exports.revertClient = async ctx => {
ctx.body = data ctx.body = data
} }
exports.delete = async ctx => { const destroyApp = async (ctx: any) => {
const db = getAppDB() const db = getAppDB()
const result = await db.destroy() const result = await db.destroy()
if (ctx.query.unpublish) {
await quotas.removePublishedApp()
} else {
await quotas.removeApp()
}
/* istanbul ignore next */ /* istanbul ignore next */
if (!env.isTest() && !ctx.query.unpublish) { if (!env.isTest() && !ctx.query.unpublish) {
await deleteApp(ctx.params.appId) await deleteApp(ctx.params.appId)
@ -362,12 +403,30 @@ exports.delete = async ctx => {
// make sure the app/role doesn't stick around after the app has been deleted // make sure the app/role doesn't stick around after the app has been deleted
await removeAppFromUserRoles(ctx, ctx.params.appId) await removeAppFromUserRoles(ctx, ctx.params.appId)
await appCache.invalidateAppMetadata(ctx.params.appId) await appCache.invalidateAppMetadata(ctx.params.appId)
return result
}
const preDestroyApp = async (ctx: any) => {
const rows = await getUniqueRows([ctx.appId])
ctx.rowCount = rows.length
}
const postDestroyApp = async (ctx: any) => {
const rowCount = ctx.rowCount
if (rowCount) {
await quotas.removeRows(rowCount)
}
}
export const destroy = async (ctx: any) => {
await preDestroyApp(ctx)
const result = await destroyApp(ctx)
await postDestroyApp(ctx)
ctx.status = 200 ctx.status = 200
ctx.body = result ctx.body = result
} }
exports.sync = async (ctx, next) => { export const sync = async (ctx: any, next: any) => {
const appId = ctx.params.appId const appId = ctx.params.appId
if (!isDevAppID(appId)) { if (!isDevAppID(appId)) {
ctx.throw(400, "This action cannot be performed for production apps") ctx.throw(400, "This action cannot be performed for production apps")
@ -397,7 +456,7 @@ exports.sync = async (ctx, next) => {
let error let error
try { try {
await replication.replicate({ await replication.replicate({
filter: function (doc) { filter: function (doc: any) {
return doc._id !== DocumentTypes.APP_METADATA return doc._id !== DocumentTypes.APP_METADATA
}, },
}) })
@ -417,7 +476,7 @@ exports.sync = async (ctx, next) => {
} }
} }
const updateAppPackage = async (appPackage, appId) => { const updateAppPackage = async (appPackage: any, appId: any) => {
const db = getAppDB() const db = getAppDB()
const application = await db.get(DocumentTypes.APP_METADATA) const application = await db.get(DocumentTypes.APP_METADATA)
@ -436,7 +495,7 @@ const updateAppPackage = async (appPackage, appId) => {
return response return response
} }
const createEmptyAppPackage = async (ctx, app) => { const createEmptyAppPackage = async (ctx: any, app: any) => {
const db = getAppDB() const db = getAppDB()
let screensAndLayouts = [] let screensAndLayouts = []

View File

@ -1,20 +1,18 @@
const Deployment = require("./Deployment") import Deployment from "./Deployment"
const { import {
Replication, Replication,
getProdAppID, getProdAppID,
getDevelopmentAppID, getDevelopmentAppID,
} = require("@budibase/backend-core/db") } from "@budibase/backend-core/db"
const { DocumentTypes, getAutomationParams } = require("../../../db/utils") import { DocumentTypes, getAutomationParams } from "../../../db/utils"
const { import { disableAllCrons, enableCronTrigger } from "../../../automations/utils"
disableAllCrons, import { app as appCache } from "@budibase/backend-core/cache"
enableCronTrigger, import {
} = require("../../../automations/utils")
const { app: appCache } = require("@budibase/backend-core/cache")
const {
getAppId, getAppId,
getAppDB, getAppDB,
getProdAppDB, getProdAppDB,
} = require("@budibase/backend-core/context") } from "@budibase/backend-core/context"
import { quotas } from "@budibase/pro"
// the max time we can wait for an invalidation to complete before considering it failed // the max time we can wait for an invalidation to complete before considering it failed
const MAX_PENDING_TIME_MS = 30 * 60000 const MAX_PENDING_TIME_MS = 30 * 60000
@ -25,9 +23,10 @@ const DeploymentStatus = {
} }
// checks that deployments are in a good state, any pending will be updated // checks that deployments are in a good state, any pending will be updated
async function checkAllDeployments(deployments) { async function checkAllDeployments(deployments: any) {
let updated = false let updated = false
for (let deployment of Object.values(deployments.history)) { let deployment: any
for (deployment of Object.values(deployments.history)) {
// check that no deployments have crashed etc and are now stuck // check that no deployments have crashed etc and are now stuck
if ( if (
deployment.status === DeploymentStatus.PENDING && deployment.status === DeploymentStatus.PENDING &&
@ -41,7 +40,7 @@ async function checkAllDeployments(deployments) {
return { updated, deployments } return { updated, deployments }
} }
async function storeDeploymentHistory(deployment) { async function storeDeploymentHistory(deployment: any) {
const deploymentJSON = deployment.getJSON() const deploymentJSON = deployment.getJSON()
const db = getAppDB() const db = getAppDB()
@ -70,7 +69,7 @@ async function storeDeploymentHistory(deployment) {
return deployment return deployment
} }
async function initDeployedApp(prodAppId) { async function initDeployedApp(prodAppId: any) {
const db = getProdAppDB() const db = getProdAppDB()
console.log("Reading automation docs") console.log("Reading automation docs")
const automations = ( const automations = (
@ -79,7 +78,7 @@ async function initDeployedApp(prodAppId) {
include_docs: true, include_docs: true,
}) })
) )
).rows.map(row => row.doc) ).rows.map((row: any) => row.doc)
console.log("You have " + automations.length + " automations") console.log("You have " + automations.length + " automations")
const promises = [] const promises = []
console.log("Disabling prod crons..") console.log("Disabling prod crons..")
@ -93,16 +92,17 @@ async function initDeployedApp(prodAppId) {
console.log("Enabled cron triggers for deployed app..") console.log("Enabled cron triggers for deployed app..")
} }
async function deployApp(deployment) { async function deployApp(deployment: any) {
try { try {
const appId = getAppId() const appId = getAppId()
const devAppId = getDevelopmentAppID(appId) const devAppId = getDevelopmentAppID(appId)
const productionAppId = getProdAppID(appId) const productionAppId = getProdAppID(appId)
const replication = new Replication({ const config: any = {
source: devAppId, source: devAppId,
target: productionAppId, target: productionAppId,
}) }
const replication = new Replication(config)
console.log("Replication object created") console.log("Replication object created")
@ -119,7 +119,7 @@ async function deployApp(deployment) {
console.log("Deployed app initialised, setting deployment to successful") console.log("Deployed app initialised, setting deployment to successful")
deployment.setStatus(DeploymentStatus.SUCCESS) deployment.setStatus(DeploymentStatus.SUCCESS)
await storeDeploymentHistory(deployment) await storeDeploymentHistory(deployment)
} catch (err) { } catch (err: any) {
deployment.setStatus(DeploymentStatus.FAILURE, err.message) deployment.setStatus(DeploymentStatus.FAILURE, err.message)
await storeDeploymentHistory(deployment) await storeDeploymentHistory(deployment)
throw { throw {
@ -129,14 +129,11 @@ async function deployApp(deployment) {
} }
} }
exports.fetchDeployments = async function (ctx) { export async function fetchDeployments(ctx: any) {
try { try {
const db = getAppDB() const db = getAppDB()
const deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS) const deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS)
const { updated, deployments } = await checkAllDeployments( const { updated, deployments } = await checkAllDeployments(deploymentDoc)
deploymentDoc,
ctx.user
)
if (updated) { if (updated) {
await db.put(deployments) await db.put(deployments)
} }
@ -146,7 +143,7 @@ exports.fetchDeployments = async function (ctx) {
} }
} }
exports.deploymentProgress = async function (ctx) { export async function deploymentProgress(ctx: any) {
try { try {
const db = getAppDB() const db = getAppDB()
const deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS) const deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS)
@ -159,7 +156,20 @@ exports.deploymentProgress = async function (ctx) {
} }
} }
exports.deployApp = async function (ctx) { const isFirstDeploy = async () => {
try {
const db = getProdAppDB()
await db.get(DocumentTypes.APP_METADATA)
} catch (e: any) {
if (e.status === 404) {
return true
}
throw e
}
return false
}
const _deployApp = async function (ctx: any) {
let deployment = new Deployment() let deployment = new Deployment()
console.log("Deployment object created") console.log("Deployment object created")
deployment.setStatus(DeploymentStatus.PENDING) deployment.setStatus(DeploymentStatus.PENDING)
@ -168,7 +178,14 @@ exports.deployApp = async function (ctx) {
console.log("Stored deployment history") console.log("Stored deployment history")
console.log("Deploying app...") console.log("Deploying app...")
if (await isFirstDeploy()) {
await quotas.addPublishedApp(() => deployApp(deployment))
} else {
await deployApp(deployment) await deployApp(deployment)
}
ctx.body = deployment ctx.body = deployment
} }
export { _deployApp as deployApp }

View File

@ -1,22 +1,19 @@
const { import { generateQueryID, getQueryParams, isProdAppID } from "../../../db/utils"
generateQueryID, import { BaseQueryVerbs } from "../../../constants"
getQueryParams, import { Thread, ThreadType } from "../../../threads"
isProdAppID, import { save as saveDatasource } from "../datasource"
} = require("../../../db/utils") import { RestImporter } from "./import"
const { BaseQueryVerbs } = require("../../../constants") import { invalidateDynamicVariables } from "../../../threads/utils"
const { Thread, ThreadType } = require("../../../threads") import { QUERY_THREAD_TIMEOUT } from "../../../environment"
const { save: saveDatasource } = require("../datasource") import { getAppDB } from "@budibase/backend-core/context"
const { RestImporter } = require("./import") import { quotas } from "@budibase/pro"
const { invalidateDynamicVariables } = require("../../../threads/utils")
const environment = require("../../../environment")
const { getAppDB } = require("@budibase/backend-core/context")
const Runner = new Thread(ThreadType.QUERY, { const Runner = new Thread(ThreadType.QUERY, {
timeoutMs: environment.QUERY_THREAD_TIMEOUT || 10000, timeoutMs: QUERY_THREAD_TIMEOUT || 10000,
}) })
// simple function to append "readable" to all read queries // simple function to append "readable" to all read queries
function enrichQueries(input) { function enrichQueries(input: any) {
const wasArray = Array.isArray(input) const wasArray = Array.isArray(input)
const queries = wasArray ? input : [input] const queries = wasArray ? input : [input]
for (let query of queries) { for (let query of queries) {
@ -27,7 +24,7 @@ function enrichQueries(input) {
return wasArray ? queries : queries[0] return wasArray ? queries : queries[0]
} }
exports.fetch = async function (ctx) { export async function fetch(ctx: any) {
const db = getAppDB() const db = getAppDB()
const body = await db.allDocs( const body = await db.allDocs(
@ -36,10 +33,10 @@ exports.fetch = async function (ctx) {
}) })
) )
ctx.body = enrichQueries(body.rows.map(row => row.doc)) ctx.body = enrichQueries(body.rows.map((row: any) => row.doc))
} }
exports.import = async ctx => { const _import = async (ctx: any) => {
const body = ctx.request.body const body = ctx.request.body
const data = body.data const data = body.data
@ -49,7 +46,7 @@ exports.import = async ctx => {
let datasourceId let datasourceId
if (!body.datasourceId) { if (!body.datasourceId) {
// construct new datasource // construct new datasource
const info = await importer.getInfo() const info: any = await importer.getInfo()
let datasource = { let datasource = {
type: "datasource", type: "datasource",
source: "REST", source: "REST",
@ -77,8 +74,9 @@ exports.import = async ctx => {
} }
ctx.status = 200 ctx.status = 200
} }
export { _import as import }
exports.save = async function (ctx) { export async function save(ctx: any) {
const db = getAppDB() const db = getAppDB()
const query = ctx.request.body const query = ctx.request.body
@ -93,7 +91,7 @@ exports.save = async function (ctx) {
ctx.message = `Query ${query.name} saved successfully.` ctx.message = `Query ${query.name} saved successfully.`
} }
exports.find = async function (ctx) { export async function find(ctx: any) {
const db = getAppDB() const db = getAppDB()
const query = enrichQueries(await db.get(ctx.params.queryId)) const query = enrichQueries(await db.get(ctx.params.queryId))
// remove properties that could be dangerous in real app // remove properties that could be dangerous in real app
@ -104,7 +102,7 @@ exports.find = async function (ctx) {
ctx.body = query ctx.body = query
} }
exports.preview = async function (ctx) { export async function preview(ctx: any) {
const db = getAppDB() const db = getAppDB()
const datasource = await db.get(ctx.request.body.datasourceId) const datasource = await db.get(ctx.request.body.datasourceId)
@ -114,7 +112,8 @@ exports.preview = async function (ctx) {
ctx.request.body ctx.request.body
try { try {
const { rows, keys, info, extra } = await Runner.run({ const runFn = () =>
Runner.run({
appId: ctx.appId, appId: ctx.appId,
datasource, datasource,
queryVerb, queryVerb,
@ -124,6 +123,7 @@ exports.preview = async function (ctx) {
queryId, queryId,
}) })
const { rows, keys, info, extra } = await quotas.addQuery(runFn)
ctx.body = { ctx.body = {
rows, rows,
schemaFields: [...new Set(keys)], schemaFields: [...new Set(keys)],
@ -135,7 +135,7 @@ exports.preview = async function (ctx) {
} }
} }
async function execute(ctx, opts = { rowsOnly: false }) { async function execute(ctx: any, opts = { rowsOnly: false }) {
const db = getAppDB() const db = getAppDB()
const query = await db.get(ctx.params.queryId) const query = await db.get(ctx.params.queryId)
@ -153,7 +153,8 @@ async function execute(ctx, opts = { rowsOnly: false }) {
// call the relevant CRUD method on the integration class // call the relevant CRUD method on the integration class
try { try {
const { rows, pagination, extra } = await Runner.run({ const runFn = () =>
Runner.run({
appId: ctx.appId, appId: ctx.appId,
datasource, datasource,
queryVerb: query.queryVerb, queryVerb: query.queryVerb,
@ -163,6 +164,8 @@ async function execute(ctx, opts = { rowsOnly: false }) {
transformer: query.transformer, transformer: query.transformer,
queryId: ctx.params.queryId, queryId: ctx.params.queryId,
}) })
const { rows, pagination, extra } = await quotas.addQuery(runFn)
if (opts && opts.rowsOnly) { if (opts && opts.rowsOnly) {
ctx.body = rows ctx.body = rows
} else { } else {
@ -173,15 +176,15 @@ async function execute(ctx, opts = { rowsOnly: false }) {
} }
} }
exports.executeV1 = async function (ctx) { export async function executeV1(ctx: any) {
return execute(ctx, { rowsOnly: true }) return execute(ctx, { rowsOnly: true })
} }
exports.executeV2 = async function (ctx) { export async function executeV2(ctx: any) {
return execute(ctx, { rowsOnly: false }) return execute(ctx, { rowsOnly: false })
} }
const removeDynamicVariables = async queryId => { const removeDynamicVariables = async (queryId: any) => {
const db = getAppDB() const db = getAppDB()
const query = await db.get(queryId) const query = await db.get(queryId)
const datasource = await db.get(query.datasourceId) const datasource = await db.get(query.datasourceId)
@ -190,19 +193,19 @@ const removeDynamicVariables = async queryId => {
if (dynamicVariables) { if (dynamicVariables) {
// delete dynamic variables from the datasource // delete dynamic variables from the datasource
datasource.config.dynamicVariables = dynamicVariables.filter( datasource.config.dynamicVariables = dynamicVariables.filter(
dv => dv.queryId !== queryId (dv: any) => dv.queryId !== queryId
) )
await db.put(datasource) await db.put(datasource)
// invalidate the deleted variables // invalidate the deleted variables
const variablesToDelete = dynamicVariables.filter( const variablesToDelete = dynamicVariables.filter(
dv => dv.queryId === queryId (dv: any) => dv.queryId === queryId
) )
await invalidateDynamicVariables(variablesToDelete) await invalidateDynamicVariables(variablesToDelete)
} }
} }
exports.destroy = async function (ctx) { export async function destroy(ctx: any) {
const db = getAppDB() const db = getAppDB()
await removeDynamicVariables(ctx.params.queryId) await removeDynamicVariables(ctx.params.queryId)
await db.remove(ctx.params.queryId, ctx.params.revId) await db.remove(ctx.params.queryId, ctx.params.revId)

View File

@ -1,15 +1,16 @@
const internal = require("./internal") import { quotas } from "@budibase/pro"
const external = require("./external") import internal from "./internal"
const { isExternalTable } = require("../../../integrations/utils") import external from "./external"
import { isExternalTable } from "../../../integrations/utils"
function pickApi(tableId) { function pickApi(tableId: any) {
if (isExternalTable(tableId)) { if (isExternalTable(tableId)) {
return external return external
} }
return internal return internal
} }
function getTableId(ctx) { function getTableId(ctx: any) {
if (ctx.request.body && ctx.request.body.tableId) { if (ctx.request.body && ctx.request.body.tableId) {
return ctx.request.body.tableId return ctx.request.body.tableId
} }
@ -21,13 +22,13 @@ function getTableId(ctx) {
} }
} }
exports.patch = async ctx => { export async function patch(ctx: any): Promise<any> {
const appId = ctx.appId const appId = ctx.appId
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
const body = ctx.request.body const body = ctx.request.body
// if it doesn't have an _id then its save // if it doesn't have an _id then its save
if (body && !body._id) { if (body && !body._id) {
return exports.save(ctx) return save(ctx)
} }
try { try {
const { row, table } = await pickApi(tableId).patch(ctx) const { row, table } = await pickApi(tableId).patch(ctx)
@ -41,13 +42,13 @@ exports.patch = async ctx => {
} }
} }
exports.save = async function (ctx) { const saveRow = async (ctx: any) => {
const appId = ctx.appId const appId = ctx.appId
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
const body = ctx.request.body const body = ctx.request.body
// if it has an ID already then its a patch // if it has an ID already then its a patch
if (body && body._id) { if (body && body._id) {
return exports.patch(ctx) return patch(ctx)
} }
try { try {
const { row, table } = await pickApi(tableId).save(ctx) const { row, table } = await pickApi(tableId).save(ctx)
@ -60,7 +61,11 @@ exports.save = async function (ctx) {
} }
} }
exports.fetchView = async function (ctx) { export async function save(ctx: any) {
await quotas.addRow(() => saveRow(ctx))
}
export async function fetchView(ctx: any) {
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
try { try {
ctx.body = await pickApi(tableId).fetchView(ctx) ctx.body = await pickApi(tableId).fetchView(ctx)
@ -69,7 +74,7 @@ exports.fetchView = async function (ctx) {
} }
} }
exports.fetch = async function (ctx) { export async function fetch(ctx: any) {
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
try { try {
ctx.body = await pickApi(tableId).fetch(ctx) ctx.body = await pickApi(tableId).fetch(ctx)
@ -78,7 +83,7 @@ exports.fetch = async function (ctx) {
} }
} }
exports.find = async function (ctx) { export async function find(ctx: any) {
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
try { try {
ctx.body = await pickApi(tableId).find(ctx) ctx.body = await pickApi(tableId).find(ctx)
@ -87,19 +92,21 @@ exports.find = async function (ctx) {
} }
} }
exports.destroy = async function (ctx) { export async function destroy(ctx: any) {
const appId = ctx.appId const appId = ctx.appId
const inputs = ctx.request.body const inputs = ctx.request.body
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
let response, row let response, row
if (inputs.rows) { if (inputs.rows) {
let { rows } = await pickApi(tableId).bulkDestroy(ctx) let { rows } = await pickApi(tableId).bulkDestroy(ctx)
await quotas.removeRows(rows.length)
response = rows response = rows
for (let row of rows) { for (let row of rows) {
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
} }
} else { } else {
let resp = await pickApi(tableId).destroy(ctx) let resp = await pickApi(tableId).destroy(ctx)
await quotas.removeRow()
response = resp.response response = resp.response
row = resp.row row = resp.row
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
@ -110,7 +117,7 @@ exports.destroy = async function (ctx) {
ctx.body = response ctx.body = response
} }
exports.search = async ctx => { export async function search(ctx: any) {
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
try { try {
ctx.status = 200 ctx.status = 200
@ -120,7 +127,7 @@ exports.search = async ctx => {
} }
} }
exports.validate = async function (ctx) { export async function validate(ctx: any) {
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
try { try {
ctx.body = await pickApi(tableId).validate(ctx) ctx.body = await pickApi(tableId).validate(ctx)
@ -129,7 +136,7 @@ exports.validate = async function (ctx) {
} }
} }
exports.fetchEnrichedRow = async function (ctx) { export async function fetchEnrichedRow(ctx: any) {
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
try { try {
ctx.body = await pickApi(tableId).fetchEnrichedRow(ctx) ctx.body = await pickApi(tableId).fetchEnrichedRow(ctx)

View File

@ -120,11 +120,7 @@ export async function destroy(ctx: any) {
await db.bulkDocs( await db.bulkDocs(
rows.rows.map((row: any) => ({ ...row.doc, _deleted: true })) rows.rows.map((row: any) => ({ ...row.doc, _deleted: true }))
) )
await quotas.updateUsage( await quotas.removeRows(rows.rows.length)
-rows.rows.length,
StaticQuotaName.ROWS,
QuotaUsageType.STATIC
)
// update linked rows // update linked rows
await updateLinks({ await updateLinks({

View File

@ -147,12 +147,7 @@ export async function handleDataImport(user: any, table: any, dataImport: any) {
finalData.push(row) finalData.push(row)
} }
await quotas.tryUpdateUsage( await quotas.addRows(finalData.length, () => db.bulkDocs(finalData))
() => db.bulkDocs(finalData),
finalData.length,
StaticQuotaName.ROWS,
QuotaUsageType.STATIC
)
let response = await db.put(table) let response = await db.put(table)
table._rev = response._rev table._rev = response._rev
return table return table

View File

@ -1,14 +1,13 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/application") import * as controller from "../controllers/application"
const authorized = require("../../middleware/authorized") import authorized from "../../middleware/authorized"
const { BUILDER } = require("@budibase/backend-core/permissions") const { BUILDER } = require("@budibase/backend-core/permissions")
const usage = require("../../middleware/usageQuota")
const router = Router() const router = Router()
router router
.post("/api/applications/:appId/sync", authorized(BUILDER), controller.sync) .post("/api/applications/:appId/sync", authorized(BUILDER), controller.sync)
.post("/api/applications", authorized(BUILDER), usage, controller.create) .post("/api/applications", authorized(BUILDER), controller.create)
.get("/api/applications/:appId/definition", controller.fetchAppDefinition) .get("/api/applications/:appId/definition", controller.fetchAppDefinition)
.get("/api/applications", controller.fetch) .get("/api/applications", controller.fetch)
.get("/api/applications/:appId/appPackage", controller.fetchAppPackage) .get("/api/applications/:appId/appPackage", controller.fetchAppPackage)
@ -23,11 +22,6 @@ router
authorized(BUILDER), authorized(BUILDER),
controller.revertClient controller.revertClient
) )
.delete( .delete("/api/applications/:appId", authorized(BUILDER), controller.destroy)
"/api/applications/:appId",
authorized(BUILDER),
usage,
controller.delete
)
module.exports = router export default router

View File

@ -1,32 +1,33 @@
const authRoutes = require("./auth") import authRoutes from "./auth"
const layoutRoutes = require("./layout") import layoutRoutes from "./layout"
const screenRoutes = require("./screen") import screenRoutes from "./screen"
const userRoutes = require("./user") import userRoutes from "./user"
const applicationRoutes = require("./application") import applicationRoutes from "./application"
const tableRoutes = require("./table") import tableRoutes from "./table"
const rowRoutes = require("./row") import rowRoutes from "./row"
const viewRoutes = require("./view") import viewRoutes from "./view"
const staticRoutes = require("./static") import componentRoutes from "./component"
const componentRoutes = require("./component") import automationRoutes from "./automation"
const automationRoutes = require("./automation") import webhookRoutes from "./webhook"
const webhookRoutes = require("./webhook") import roleRoutes from "./role"
const roleRoutes = require("./role") import deployRoutes from "./deploy"
const deployRoutes = require("./deploy") import apiKeysRoutes from "./apikeys"
const apiKeysRoutes = require("./apikeys") import templatesRoutes from "./templates"
const templatesRoutes = require("./templates") import analyticsRoutes from "./analytics"
const analyticsRoutes = require("./analytics") import routingRoutes from "./routing"
const routingRoutes = require("./routing") import integrationRoutes from "./integration"
const integrationRoutes = require("./integration") import permissionRoutes from "./permission"
const permissionRoutes = require("./permission") import datasourceRoutes from "./datasource"
const datasourceRoutes = require("./datasource") import queryRoutes from "./query"
const queryRoutes = require("./query") import backupRoutes from "./backup"
const backupRoutes = require("./backup") import metadataRoutes from "./metadata"
const metadataRoutes = require("./metadata") import devRoutes from "./dev"
const devRoutes = require("./dev") import cloudRoutes from "./cloud"
const cloudRoutes = require("./cloud") import migrationRoutes from "./migrations"
const migrationRoutes = require("./migrations")
exports.mainRoutes = [ export { default as staticRoutes } from "./static"
export const mainRoutes = [
authRoutes, authRoutes,
deployRoutes, deployRoutes,
layoutRoutes, layoutRoutes,
@ -56,5 +57,3 @@ exports.mainRoutes = [
rowRoutes, rowRoutes,
migrationRoutes, migrationRoutes,
] ]
exports.staticRoutes = staticRoutes

View File

@ -1,11 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const rowController = require("../controllers/row") import * as rowController from "../controllers/row"
const authorized = require("../../middleware/authorized") import authorized from "../../middleware/authorized"
const usage = require("../../middleware/usageQuota") import { paramResource, paramSubResource } from "../../middleware/resourceId"
const {
paramResource,
paramSubResource,
} = require("../../middleware/resourceId")
const { const {
PermissionLevels, PermissionLevels,
PermissionTypes, PermissionTypes,
@ -180,7 +176,6 @@ router
"/api/:tableId/rows", "/api/:tableId/rows",
paramResource("tableId"), paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
usage,
rowController.save rowController.save
) )
/** /**
@ -247,8 +242,7 @@ router
"/api/:tableId/rows", "/api/:tableId/rows",
paramResource("tableId"), paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
usage,
rowController.destroy rowController.destroy
) )
module.exports = router export default router

View File

@ -1,14 +1,14 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/static") import * as controller from "../controllers/static"
const { budibaseTempDir } = require("../../utilities/budibaseDir") import { budibaseTempDir } from "../../utilities/budibaseDir"
const authorized = require("../../middleware/authorized") import authorized from "../../middleware/authorized"
const { import {
BUILDER, BUILDER,
PermissionTypes, PermissionTypes,
PermissionLevels, PermissionLevels,
} = require("@budibase/backend-core/permissions") } from "@budibase/backend-core/permissions"
const env = require("../../environment") import * as env from "../../environment"
const { paramResource } = require("../../middleware/resourceId") import { paramResource } from "../../middleware/resourceId"
const router = Router() const router = Router()
@ -52,4 +52,4 @@ router
controller.getSignedUploadURL controller.getSignedUploadURL
) )
module.exports = router export default router

View File

@ -1,4 +1,4 @@
import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { save } from "../../api/controllers/row" import { save } from "../../api/controllers/row"
import { cleanUpRow, getError } from "../automationUtils" import { cleanUpRow, getError } from "../automationUtils"
import { buildCtx } from "./utils" import { buildCtx } from "./utils"
@ -78,12 +78,7 @@ export async function run({ inputs, appId, emitter }: any) {
try { try {
inputs.row = await cleanUpRow(inputs.row.tableId, inputs.row) inputs.row = await cleanUpRow(inputs.row.tableId, inputs.row)
await quotas.tryUpdateUsage( await quotas.addRow(() => save(ctx))
() => save(ctx),
1,
StaticQuotaName.ROWS,
QuotaUsageType.STATIC
)
return { return {
row: inputs.row, row: inputs.row,
response: ctx.body, response: ctx.body,

View File

@ -1,7 +1,7 @@
import { destroy } from "../../api/controllers/row" import { destroy } from "../../api/controllers/row"
import { buildCtx } from "./utils" import { buildCtx } from "./utils"
import { getError } from "../automationUtils" import { getError } from "../automationUtils"
import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro" import { quotas } from "@budibase/pro"
export const definition = { export const definition = {
description: "Delete a row from your database", description: "Delete a row from your database",
@ -73,8 +73,8 @@ export async function run({ inputs, appId, emitter }: any) {
}) })
try { try {
await quotas.updateUsage(-1, StaticQuotaName.ROWS, QuotaUsageType.STATIC)
await destroy(ctx) await destroy(ctx)
await quotas.removeRow()
return { return {
response: ctx.body, response: ctx.body,
row: ctx.row, row: ctx.row,

View File

@ -1,23 +1,29 @@
const { Thread, ThreadType } = require("../threads") import { Thread, ThreadType } from "../threads"
const { definitions } = require("./triggerInfo") import { definitions } from "./triggerInfo"
const webhooks = require("../api/controllers/webhook") import { destroy, Webhook, WebhookType, save } from "../api/controllers/webhook"
const CouchDB = require("../db") import CouchDB from "../db"
const { queue } = require("./bullboard") import { queue } from "./bullboard"
const newid = require("../db/newid") import newid from "../db/newid"
const { updateEntityMetadata } = require("../utilities") import { updateEntityMetadata } from "../utilities"
const { MetadataTypes } = require("../constants") import { MetadataTypes } from "../constants"
const { getProdAppID } = require("@budibase/backend-core/db") import { getProdAppID } from "@budibase/backend-core/db"
const { cloneDeep } = require("lodash/fp") import { cloneDeep } from "lodash/fp"
const { getAppDB, getAppId } = require("@budibase/backend-core/context") import { getAppDB, getAppId } from "@budibase/backend-core/context"
import { tenancy } from "@budibase/backend-core"
import { quotas } from "@budibase/pro"
const WH_STEP_ID = definitions.WEBHOOK.stepId const WH_STEP_ID = definitions.WEBHOOK.stepId
const CRON_STEP_ID = definitions.CRON.stepId const CRON_STEP_ID = definitions.CRON.stepId
const Runner = new Thread(ThreadType.AUTOMATION) const Runner = new Thread(ThreadType.AUTOMATION)
exports.processEvent = async job => { export async function processEvent(job: any) {
try { try {
const tenantId = tenancy.getTenantIDFromAppID(job.data.event.appId)
return await tenancy.doInTenant(tenantId, async () => {
// need to actually await these so that an error can be captured properly // need to actually await these so that an error can be captured properly
return await Runner.run(job) const runFn = () => Runner.run(job)
return quotas.addAutomation(runFn)
})
} catch (err) { } catch (err) {
console.error( console.error(
`${job.data.automation.appId} automation ${job.data.automation._id} was unable to run - ${err}` `${job.data.automation.appId} automation ${job.data.automation._id} was unable to run - ${err}`
@ -26,11 +32,15 @@ exports.processEvent = async job => {
} }
} }
exports.updateTestHistory = async (appId, automation, history) => { export async function updateTestHistory(
appId: any,
automation: any,
history: any
) {
return updateEntityMetadata( return updateEntityMetadata(
MetadataTypes.AUTOMATION_TEST_HISTORY, MetadataTypes.AUTOMATION_TEST_HISTORY,
automation._id, automation._id,
metadata => { (metadata: any) => {
if (metadata && Array.isArray(metadata.history)) { if (metadata && Array.isArray(metadata.history)) {
metadata.history.push(history) metadata.history.push(history)
} else { } else {
@ -43,7 +53,7 @@ exports.updateTestHistory = async (appId, automation, history) => {
) )
} }
exports.removeDeprecated = definitions => { export function removeDeprecated(definitions: any) {
const base = cloneDeep(definitions) const base = cloneDeep(definitions)
for (let key of Object.keys(base)) { for (let key of Object.keys(base)) {
if (base[key].deprecated) { if (base[key].deprecated) {
@ -54,15 +64,17 @@ exports.removeDeprecated = definitions => {
} }
// end the repetition and the job itself // end the repetition and the job itself
exports.disableAllCrons = async appId => { export async function disableAllCrons(appId: any) {
const promises = [] const promises = []
const jobs = await queue.getRepeatableJobs() const jobs = await queue.getRepeatableJobs()
for (let job of jobs) { for (let job of jobs) {
if (job.key.includes(`${appId}_cron`)) { if (job.key.includes(`${appId}_cron`)) {
promises.push(queue.removeRepeatableByKey(job.key)) promises.push(queue.removeRepeatableByKey(job.key))
if (job.id) {
promises.push(queue.removeJobs(job.id)) promises.push(queue.removeJobs(job.id))
} }
} }
}
return Promise.all(promises) return Promise.all(promises)
} }
@ -71,9 +83,9 @@ exports.disableAllCrons = async appId => {
* @param {string} appId The ID of the app in which we are checking for webhooks * @param {string} appId The ID of the app in which we are checking for webhooks
* @param {object|undefined} automation The automation object to be updated. * @param {object|undefined} automation The automation object to be updated.
*/ */
exports.enableCronTrigger = async (appId, automation) => { export async function enableCronTrigger(appId: any, automation: any) {
const trigger = automation ? automation.definition.trigger : null const trigger = automation ? automation.definition.trigger : null
function isCronTrigger(auto) { function isCronTrigger(auto: any) {
return ( return (
auto && auto &&
auto.definition.trigger && auto.definition.trigger &&
@ -84,7 +96,7 @@ exports.enableCronTrigger = async (appId, automation) => {
if (isCronTrigger(automation)) { if (isCronTrigger(automation)) {
// make a job id rather than letting Bull decide, makes it easier to handle on way out // make a job id rather than letting Bull decide, makes it easier to handle on way out
const jobId = `${appId}_cron_${newid()}` const jobId = `${appId}_cron_${newid()}`
const job = await queue.add( const job: any = await queue.add(
{ {
automation, automation,
event: { appId, timestamp: Date.now() }, event: { appId, timestamp: Date.now() },
@ -112,13 +124,13 @@ exports.enableCronTrigger = async (appId, automation) => {
* @returns {Promise<object|undefined>} After this is complete the new automation object may have been updated and should be * @returns {Promise<object|undefined>} After this is complete the new automation object may have been updated and should be
* written to DB (this does not write to DB as it would be wasteful to repeat). * written to DB (this does not write to DB as it would be wasteful to repeat).
*/ */
exports.checkForWebhooks = async ({ oldAuto, newAuto }) => { export async function checkForWebhooks({ oldAuto, newAuto }: any) {
const appId = getAppId() const appId = getAppId()
const oldTrigger = oldAuto ? oldAuto.definition.trigger : null const oldTrigger = oldAuto ? oldAuto.definition.trigger : null
const newTrigger = newAuto ? newAuto.definition.trigger : null const newTrigger = newAuto ? newAuto.definition.trigger : null
const triggerChanged = const triggerChanged =
oldTrigger && newTrigger && oldTrigger.id !== newTrigger.id oldTrigger && newTrigger && oldTrigger.id !== newTrigger.id
function isWebhookTrigger(auto) { function isWebhookTrigger(auto: any) {
return ( return (
auto && auto &&
auto.definition.trigger && auto.definition.trigger &&
@ -144,7 +156,7 @@ exports.checkForWebhooks = async ({ oldAuto, newAuto }) => {
delete newTrigger.webhookId delete newTrigger.webhookId
newTrigger.inputs = {} newTrigger.inputs = {}
} }
await webhooks.destroy(ctx) await destroy(ctx)
} catch (err) { } catch (err) {
// don't worry about not being able to delete, if it doesn't exist all good // don't worry about not being able to delete, if it doesn't exist all good
} }
@ -154,17 +166,17 @@ exports.checkForWebhooks = async ({ oldAuto, newAuto }) => {
(!isWebhookTrigger(oldAuto) || triggerChanged) && (!isWebhookTrigger(oldAuto) || triggerChanged) &&
isWebhookTrigger(newAuto) isWebhookTrigger(newAuto)
) { ) {
const ctx = { const ctx: any = {
appId, appId,
request: { request: {
body: new webhooks.Webhook( body: new Webhook(
"Automation webhook", "Automation webhook",
webhooks.WebhookType.AUTOMATION, WebhookType.AUTOMATION,
newAuto._id newAuto._id
), ),
}, },
} }
await webhooks.save(ctx) await save(ctx)
const id = ctx.body.webhook._id const id = ctx.body.webhook._id
newTrigger.webhookId = id newTrigger.webhookId = id
// the app ID has to be development for this endpoint // the app ID has to be development for this endpoint
@ -184,6 +196,6 @@ exports.checkForWebhooks = async ({ oldAuto, newAuto }) => {
* @param appId {string} the app that is being removed. * @param appId {string} the app that is being removed.
* @return {Promise<void>} clean is complete if this succeeds. * @return {Promise<void>} clean is complete if this succeeds.
*/ */
exports.cleanupAutomations = async appId => { export async function cleanupAutomations(appId: any) {
await exports.disableAllCrons(appId) await disableAllCrons(appId)
} }

View File

@ -1,172 +0,0 @@
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.APPS].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,
}

View File

@ -1,8 +1,3 @@
import { quotas } from "@budibase/pro"
export const runQuotaMigration = async (migration: Function) => { export const runQuotaMigration = async (migration: Function) => {
if (!quotas.useQuotas()) {
return
}
await migration() await migration()
} }

View File

@ -1,3 +1,5 @@
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"

View File

@ -1,12 +1,12 @@
const workerFarm = require("worker-farm") import workerFarm from "worker-farm"
const env = require("../environment") import * as env from "../environment"
const ThreadType = { export const ThreadType = {
QUERY: "query", QUERY: "query",
AUTOMATION: "automation", AUTOMATION: "automation",
} }
function typeToFile(type) { function typeToFile(type: any) {
let filename = null let filename = null
switch (type) { switch (type) {
case ThreadType.QUERY: case ThreadType.QUERY:
@ -21,8 +21,13 @@ function typeToFile(type) {
return require.resolve(filename) return require.resolve(filename)
} }
class Thread { export class Thread {
constructor(type, opts = { timeoutMs: null, count: 1 }) { type: any
count: any
disableThreading: any
workers: any
constructor(type: any, opts: any = { timeoutMs: null, count: 1 }) {
this.type = type this.type = type
this.count = opts.count ? opts.count : 1 this.count = opts.count ? opts.count : 1
this.disableThreading = this.disableThreading =
@ -31,7 +36,7 @@ class Thread {
this.count === 0 || this.count === 0 ||
env.isInThread() env.isInThread()
if (!this.disableThreading) { if (!this.disableThreading) {
const workerOpts = { const workerOpts: any = {
autoStart: true, autoStart: true,
maxConcurrentWorkers: this.count, maxConcurrentWorkers: this.count,
} }
@ -42,7 +47,7 @@ class Thread {
} }
} }
run(data) { run(data: any) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let fncToCall let fncToCall
// if in test then don't use threading // if in test then don't use threading
@ -51,7 +56,7 @@ class Thread {
} else { } else {
fncToCall = this.workers fncToCall = this.workers
} }
fncToCall(data, (err, response) => { fncToCall(data, (err: any, response: any) => {
if (err) { if (err) {
reject(err) reject(err)
} else { } else {
@ -61,6 +66,3 @@ class Thread {
}) })
} }
} }
module.exports.Thread = Thread
module.exports.ThreadType = ThreadType

View File

@ -66,7 +66,8 @@ class InMemoryQueue {
* @param {object} msg A message to be transported over the queue, this should be * @param {object} msg A message to be transported over the queue, this should be
* a JSON message as this is required by Bull. * a JSON message as this is required by Bull.
*/ */
add(msg) { // eslint-disable-next-line no-unused-vars
add(msg, repeat) {
if (typeof msg !== "object") { if (typeof msg !== "object") {
throw "Queue only supports carrying JSON." throw "Queue only supports carrying JSON."
} }
@ -90,6 +91,11 @@ class InMemoryQueue {
return [] return []
} }
// eslint-disable-next-line no-unused-vars
removeJobs(pattern) {
// no-op
}
/** /**
* Implemented for tests * Implemented for tests
*/ */

View File

@ -224,7 +224,7 @@ async function oidcStrategyFactory(ctx: any, configId: any) {
const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0] const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
let callbackUrl = await exports.oidcCallbackUrl(chosenConfig) let callbackUrl = await exports.oidcCallbackUrl(chosenConfig)
return oidc.strategyFactory(chosenConfig, callbackUrl) return oidc.strategyFactory(chosenConfig, callbackUrl, users.save)
} }
/** /**

View File

@ -37,7 +37,7 @@ const allUsers = async () => {
export const save = async (ctx: any) => { export const save = async (ctx: any) => {
try { try {
const user = await users.save(ctx.request.body, getTenantId()) const user: any = await users.save(ctx.request.body, getTenantId())
// let server know to sync user // let server know to sync user
await syncUserInApps(user._id) await syncUserInApps(user._id)
ctx.body = user ctx.body = user
@ -129,6 +129,7 @@ export const destroy = async (ctx: any) => {
await removeUserFromInfoDB(dbUser) await removeUserFromInfoDB(dbUser)
await db.remove(dbUser._id, dbUser._rev) await db.remove(dbUser._id, dbUser._rev)
await quotas.removeUser(dbUser)
await userCache.invalidateUser(dbUser._id) await userCache.invalidateUser(dbUser._id)
await invalidateSessions(dbUser._id) await invalidateSessions(dbUser._id)
// let server know to sync user // let server know to sync user