Merge pull request #6074 from Budibase/feature/app-quotas
App/resource ID breakdown of quotas
This commit is contained in:
commit
a3cd3c8067
|
@ -6,6 +6,7 @@ const {
|
||||||
updateAppId,
|
updateAppId,
|
||||||
doInAppContext,
|
doInAppContext,
|
||||||
doInTenant,
|
doInTenant,
|
||||||
|
doInContext,
|
||||||
} = require("./src/context")
|
} = require("./src/context")
|
||||||
|
|
||||||
const identity = require("./src/context/identity")
|
const identity = require("./src/context/identity")
|
||||||
|
@ -19,4 +20,5 @@ module.exports = {
|
||||||
doInAppContext,
|
doInAppContext,
|
||||||
doInTenant,
|
doInTenant,
|
||||||
identity,
|
identity,
|
||||||
|
doInContext,
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,16 @@ export const getTenantIDFromAppID = (appId: string) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// used for automations, API endpoints should always be in context already
|
export const doInContext = async (appId: string, task: any) => {
|
||||||
|
// gets the tenant ID from the app ID
|
||||||
|
const tenantId = getTenantIDFromAppID(appId)
|
||||||
|
return doInTenant(tenantId, async () => {
|
||||||
|
return doInAppContext(appId, async () => {
|
||||||
|
return task()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const doInTenant = (tenantId: string | null, task: any) => {
|
export const doInTenant = (tenantId: string | null, task: any) => {
|
||||||
// make sure default always selected in single tenancy
|
// make sure default always selected in single tenancy
|
||||||
if (!env.MULTI_TENANCY) {
|
if (!env.MULTI_TENANCY) {
|
||||||
|
|
|
@ -46,6 +46,9 @@ export enum DocumentType {
|
||||||
AUTOMATION_LOG = "log_au",
|
AUTOMATION_LOG = "log_au",
|
||||||
ACCOUNT_METADATA = "acc_metadata",
|
ACCOUNT_METADATA = "acc_metadata",
|
||||||
PLUGIN = "plg",
|
PLUGIN = "plg",
|
||||||
|
TABLE = "ta",
|
||||||
|
DATASOURCE = "datasource",
|
||||||
|
DATASOURCE_PLUS = "datasource_plus",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StaticDatabases = {
|
export const StaticDatabases = {
|
||||||
|
|
|
@ -64,6 +64,28 @@ export function getQueryIndex(viewName: ViewName) {
|
||||||
return `database/${viewName}`
|
return `database/${viewName}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given ID is that of a table.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export const isTableId = (id: string) => {
|
||||||
|
// this includes datasource plus tables
|
||||||
|
return (
|
||||||
|
id &&
|
||||||
|
(id.startsWith(`${DocumentType.TABLE}${SEPARATOR}`) ||
|
||||||
|
id.startsWith(`${DocumentType.DATASOURCE_PLUS}${SEPARATOR}`))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given ID is that of a datasource or datasource plus.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export const isDatasourceId = (id: string) => {
|
||||||
|
// this covers both datasources and datasource plus
|
||||||
|
return id && id.startsWith(`${DocumentType.DATASOURCE}${SEPARATOR}`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new workspace ID.
|
* Generates a new workspace ID.
|
||||||
* @returns {string} The new workspace ID which the workspace doc can be stored under.
|
* @returns {string} The new workspace ID which the workspace doc can be stored under.
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const DEFINITIONS: MigrationDefinition[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: MigrationType.GLOBAL,
|
type: MigrationType.GLOBAL,
|
||||||
name: MigrationName.QUOTAS_1,
|
name: MigrationName.SYNC_QUOTAS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: MigrationType.APP,
|
type: MigrationType.APP,
|
||||||
|
@ -33,8 +33,4 @@ export const DEFINITIONS: MigrationDefinition[] = [
|
||||||
type: MigrationType.GLOBAL,
|
type: MigrationType.GLOBAL,
|
||||||
name: MigrationName.GLOBAL_INFO_SYNC_USERS,
|
name: MigrationName.GLOBAL_INFO_SYNC_USERS,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: MigrationType.GLOBAL,
|
|
||||||
name: MigrationName.PLUGIN_COUNT,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
updateAppId,
|
updateAppId,
|
||||||
doInAppContext,
|
doInAppContext,
|
||||||
doInTenant,
|
doInTenant,
|
||||||
|
doInContext,
|
||||||
} from "../context"
|
} from "../context"
|
||||||
|
|
||||||
import * as identity from "../context/identity"
|
import * as identity from "../context/identity"
|
||||||
|
@ -20,5 +21,6 @@ export = {
|
||||||
updateAppId,
|
updateAppId,
|
||||||
doInAppContext,
|
doInAppContext,
|
||||||
doInTenant,
|
doInTenant,
|
||||||
|
doInContext,
|
||||||
identity,
|
identity,
|
||||||
}
|
}
|
||||||
|
|
|
@ -356,7 +356,7 @@ const appPostCreate = async (ctx: any, app: App) => {
|
||||||
await creationEvents(ctx.request, app)
|
await creationEvents(ctx.request, app)
|
||||||
// app import & template creation
|
// app import & template creation
|
||||||
if (ctx.request.body.useTemplate === "true") {
|
if (ctx.request.body.useTemplate === "true") {
|
||||||
const rows = await getUniqueRows([app.appId])
|
const { rows } = await getUniqueRows([app.appId])
|
||||||
const rowCount = rows ? rows.length : 0
|
const rowCount = rows ? rows.length : 0
|
||||||
if (rowCount) {
|
if (rowCount) {
|
||||||
try {
|
try {
|
||||||
|
@ -490,7 +490,7 @@ const destroyApp = async (ctx: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const preDestroyApp = async (ctx: any) => {
|
const preDestroyApp = async (ctx: any) => {
|
||||||
const rows = await getUniqueRows([ctx.params.appId])
|
const { rows } = await getUniqueRows([ctx.params.appId])
|
||||||
ctx.rowCount = rows.length
|
ctx.rowCount = rows.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -153,7 +153,10 @@ export async function preview(ctx: any) {
|
||||||
auth: { ...authConfigCtx },
|
auth: { ...authConfigCtx },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const { rows, keys, info, extra } = await quotas.addQuery(runFn)
|
|
||||||
|
const { rows, keys, info, extra } = await quotas.addQuery(runFn, {
|
||||||
|
datasourceId: datasource._id,
|
||||||
|
})
|
||||||
const schemaFields: any = {}
|
const schemaFields: any = {}
|
||||||
if (rows?.length > 0) {
|
if (rows?.length > 0) {
|
||||||
for (let key of [...new Set(keys)] as string[]) {
|
for (let key of [...new Set(keys)] as string[]) {
|
||||||
|
@ -234,7 +237,9 @@ async function execute(
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { rows, pagination, extra } = await quotas.addQuery(runFn)
|
const { rows, pagination, extra } = await quotas.addQuery(runFn, {
|
||||||
|
datasourceId: datasource._id,
|
||||||
|
})
|
||||||
if (opts && opts.rowsOnly) {
|
if (opts && opts.rowsOnly) {
|
||||||
ctx.body = rows
|
ctx.body = rows
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -31,8 +31,11 @@ export async function patch(ctx: any): Promise<any> {
|
||||||
return save(ctx)
|
return save(ctx)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { row, table } = await quotas.addQuery(() =>
|
const { row, table } = await quotas.addQuery(
|
||||||
pickApi(tableId).patch(ctx)
|
() => pickApi(tableId).patch(ctx),
|
||||||
|
{
|
||||||
|
datasourceId: tableId,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.eventEmitter &&
|
ctx.eventEmitter &&
|
||||||
|
@ -54,7 +57,9 @@ export const save = async (ctx: any) => {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { row, table } = await quotas.addRow(() =>
|
const { row, table } = await quotas.addRow(() =>
|
||||||
quotas.addQuery(() => pickApi(tableId).save(ctx))
|
quotas.addQuery(() => pickApi(tableId).save(ctx), {
|
||||||
|
datasourceId: tableId,
|
||||||
|
})
|
||||||
)
|
)
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
|
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
|
||||||
|
@ -68,7 +73,9 @@ export const save = async (ctx: any) => {
|
||||||
export async function fetchView(ctx: any) {
|
export async function fetchView(ctx: any) {
|
||||||
const tableId = getTableId(ctx)
|
const tableId = getTableId(ctx)
|
||||||
try {
|
try {
|
||||||
ctx.body = await quotas.addQuery(() => pickApi(tableId).fetchView(ctx))
|
ctx.body = await quotas.addQuery(() => pickApi(tableId).fetchView(ctx), {
|
||||||
|
datasourceId: tableId,
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.throw(400, err)
|
ctx.throw(400, err)
|
||||||
}
|
}
|
||||||
|
@ -77,7 +84,9 @@ export async function fetchView(ctx: any) {
|
||||||
export async function fetch(ctx: any) {
|
export async function fetch(ctx: any) {
|
||||||
const tableId = getTableId(ctx)
|
const tableId = getTableId(ctx)
|
||||||
try {
|
try {
|
||||||
ctx.body = await quotas.addQuery(() => pickApi(tableId).fetch(ctx))
|
ctx.body = await quotas.addQuery(() => pickApi(tableId).fetch(ctx), {
|
||||||
|
datasourceId: tableId,
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.throw(400, err)
|
ctx.throw(400, err)
|
||||||
}
|
}
|
||||||
|
@ -86,7 +95,9 @@ export async function fetch(ctx: any) {
|
||||||
export async function find(ctx: any) {
|
export async function find(ctx: any) {
|
||||||
const tableId = getTableId(ctx)
|
const tableId = getTableId(ctx)
|
||||||
try {
|
try {
|
||||||
ctx.body = await quotas.addQuery(() => pickApi(tableId).find(ctx))
|
ctx.body = await quotas.addQuery(() => pickApi(tableId).find(ctx), {
|
||||||
|
datasourceId: tableId,
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.throw(400, err)
|
ctx.throw(400, err)
|
||||||
}
|
}
|
||||||
|
@ -98,8 +109,11 @@ export async function destroy(ctx: any) {
|
||||||
const tableId = getTableId(ctx)
|
const tableId = getTableId(ctx)
|
||||||
let response, row
|
let response, row
|
||||||
if (inputs.rows) {
|
if (inputs.rows) {
|
||||||
let { rows } = await quotas.addQuery(() =>
|
let { rows } = await quotas.addQuery(
|
||||||
pickApi(tableId).bulkDestroy(ctx)
|
() => pickApi(tableId).bulkDestroy(ctx),
|
||||||
|
{
|
||||||
|
datasourceId: tableId,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
await quotas.removeRows(rows.length)
|
await quotas.removeRows(rows.length)
|
||||||
response = rows
|
response = rows
|
||||||
|
@ -107,7 +121,9 @@ export async function destroy(ctx: any) {
|
||||||
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
|
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let resp = await quotas.addQuery(() => pickApi(tableId).destroy(ctx))
|
let resp = await quotas.addQuery(() => pickApi(tableId).destroy(ctx), {
|
||||||
|
datasourceId: tableId,
|
||||||
|
})
|
||||||
await quotas.removeRow()
|
await quotas.removeRow()
|
||||||
response = resp.response
|
response = resp.response
|
||||||
row = resp.row
|
row = resp.row
|
||||||
|
@ -123,7 +139,9 @@ export async function search(ctx: any) {
|
||||||
const tableId = getTableId(ctx)
|
const tableId = getTableId(ctx)
|
||||||
try {
|
try {
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = await quotas.addQuery(() => pickApi(tableId).search(ctx))
|
ctx.body = await quotas.addQuery(() => pickApi(tableId).search(ctx), {
|
||||||
|
datasourceId: tableId,
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.throw(400, err)
|
ctx.throw(400, err)
|
||||||
}
|
}
|
||||||
|
@ -141,8 +159,11 @@ export async function validate(ctx: any) {
|
||||||
export async function fetchEnrichedRow(ctx: any) {
|
export async function fetchEnrichedRow(ctx: any) {
|
||||||
const tableId = getTableId(ctx)
|
const tableId = getTableId(ctx)
|
||||||
try {
|
try {
|
||||||
ctx.body = await quotas.addQuery(() =>
|
ctx.body = await quotas.addQuery(
|
||||||
pickApi(tableId).fetchEnrichedRow(ctx)
|
() => pickApi(tableId).fetchEnrichedRow(ctx),
|
||||||
|
{
|
||||||
|
datasourceId: tableId,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.throw(400, err)
|
ctx.throw(400, err)
|
||||||
|
@ -152,7 +173,9 @@ export async function fetchEnrichedRow(ctx: any) {
|
||||||
export const exportRows = async (ctx: any) => {
|
export const exportRows = async (ctx: any) => {
|
||||||
const tableId = getTableId(ctx)
|
const tableId = getTableId(ctx)
|
||||||
try {
|
try {
|
||||||
ctx.body = await quotas.addQuery(() => pickApi(tableId).exportRows(ctx))
|
ctx.body = await quotas.addQuery(() => pickApi(tableId).exportRows(ctx), {
|
||||||
|
datasourceId: tableId,
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.throw(400, err)
|
ctx.throw(400, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,7 +145,9 @@ 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.removeRows(rows.rows.length)
|
await quotas.removeRows(rows.rows.length, {
|
||||||
|
tableId: ctx.params.tableId,
|
||||||
|
})
|
||||||
|
|
||||||
// update linked rows
|
// update linked rows
|
||||||
await updateLinks({
|
await updateLinks({
|
||||||
|
|
|
@ -148,7 +148,9 @@ export async function handleDataImport(user: any, table: any, dataImport: any) {
|
||||||
finalData.push(row)
|
finalData.push(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
await quotas.addRows(finalData.length, () => db.bulkDocs(finalData))
|
await quotas.addRows(finalData.length, () => db.bulkDocs(finalData), {
|
||||||
|
tableId: table._id,
|
||||||
|
})
|
||||||
await events.rows.imported(table, "csv", finalData.length)
|
await events.rows.imported(table, "csv", finalData.length)
|
||||||
return table
|
return table
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,18 +34,13 @@ describe("/rows", () => {
|
||||||
.expect(status)
|
.expect(status)
|
||||||
|
|
||||||
const getRowUsage = async () => {
|
const getRowUsage = async () => {
|
||||||
return config.doInContext(null, () =>
|
const { total } = await config.doInContext(null, () => quotas.getCurrentUsageValues(QuotaUsageType.STATIC, StaticQuotaName.ROWS))
|
||||||
quotas.getCurrentUsageValue(QuotaUsageType.STATIC, StaticQuotaName.ROWS)
|
return total
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getQueryUsage = async () => {
|
const getQueryUsage = async () => {
|
||||||
return config.doInContext(null, () =>
|
const { total } = await config.doInContext(null, () => quotas.getCurrentUsageValues(QuotaUsageType.MONTHLY, MonthlyQuotaName.QUERIES))
|
||||||
quotas.getCurrentUsageValue(
|
return total
|
||||||
QuotaUsageType.MONTHLY,
|
|
||||||
MonthlyQuotaName.QUERIES
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const assertRowUsage = async expected => {
|
const assertRowUsage = async expected => {
|
||||||
|
@ -60,26 +55,26 @@ describe("/rows", () => {
|
||||||
|
|
||||||
describe("save, load, update", () => {
|
describe("save, load, update", () => {
|
||||||
it("returns a success message when the row is created", async () => {
|
it("returns a success message when the row is created", async () => {
|
||||||
// const rowUsage = await getRowUsage()
|
const rowUsage = await getRowUsage()
|
||||||
// const queryUsage = await getQueryUsage()
|
const queryUsage = await getQueryUsage()
|
||||||
//
|
|
||||||
// const res = await request
|
const res = await request
|
||||||
// .post(`/api/${row.tableId}/rows`)
|
.post(`/api/${row.tableId}/rows`)
|
||||||
// .send(row)
|
.send(row)
|
||||||
// .set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
// .expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
// .expect(200)
|
.expect(200)
|
||||||
// expect(res.res.statusMessage).toEqual(`${table.name} saved successfully`)
|
expect(res.res.statusMessage).toEqual(`${table.name} saved successfully`)
|
||||||
// expect(res.body.name).toEqual("Test Contact")
|
expect(res.body.name).toEqual("Test Contact")
|
||||||
// expect(res.body._rev).toBeDefined()
|
expect(res.body._rev).toBeDefined()
|
||||||
// await assertRowUsage(rowUsage + 1)
|
await assertRowUsage(rowUsage + 1)
|
||||||
// await assertQueryUsage(queryUsage + 1)
|
await assertQueryUsage(queryUsage + 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("updates a row successfully", async () => {
|
it("updates a row successfully", async () => {
|
||||||
const existing = await config.createRow()
|
const existing = await config.createRow()
|
||||||
// const rowUsage = await getRowUsage()
|
const rowUsage = await getRowUsage()
|
||||||
// const queryUsage = await getQueryUsage()
|
const queryUsage = await getQueryUsage()
|
||||||
|
|
||||||
const res = await request
|
const res = await request
|
||||||
.post(`/api/${table._id}/rows`)
|
.post(`/api/${table._id}/rows`)
|
||||||
|
@ -97,8 +92,8 @@ describe("/rows", () => {
|
||||||
`${table.name} updated successfully.`
|
`${table.name} updated successfully.`
|
||||||
)
|
)
|
||||||
expect(res.body.name).toEqual("Updated Name")
|
expect(res.body.name).toEqual("Updated Name")
|
||||||
// await assertRowUsage(rowUsage)
|
await assertRowUsage(rowUsage)
|
||||||
// await assertQueryUsage(queryUsage + 1)
|
await assertQueryUsage(queryUsage + 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should load a row", async () => {
|
it("should load a row", async () => {
|
||||||
|
|
|
@ -29,16 +29,11 @@ describe("Run through some parts of the automations system", () => {
|
||||||
afterAll(setup.afterAll)
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
it("should be able to init in builder", async () => {
|
it("should be able to init in builder", async () => {
|
||||||
await triggers.externalTrigger(basicAutomation(), { a: 1 })
|
await triggers.externalTrigger(basicAutomation(), { a: 1, appId: "app_123" })
|
||||||
await wait(100)
|
await wait(100)
|
||||||
expect(thread.execute).toHaveBeenCalled()
|
expect(thread.execute).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to init in prod", async () => {
|
|
||||||
await triggers.externalTrigger(basicAutomation(), { a: 1 })
|
|
||||||
await wait(100)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should check coercion", async () => {
|
it("should check coercion", async () => {
|
||||||
const table = await config.createTable()
|
const table = await config.createTable()
|
||||||
const automation = basicAutomation()
|
const automation = basicAutomation()
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
getAppId,
|
getAppId,
|
||||||
getProdAppDB,
|
getProdAppDB,
|
||||||
} from "@budibase/backend-core/context"
|
} from "@budibase/backend-core/context"
|
||||||
import { tenancy } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import { Automation } from "@budibase/types"
|
import { Automation } from "@budibase/types"
|
||||||
|
|
||||||
|
@ -28,12 +28,14 @@ const jobMessage = (job: any, message: string) => {
|
||||||
|
|
||||||
export async function processEvent(job: any) {
|
export async function processEvent(job: any) {
|
||||||
try {
|
try {
|
||||||
|
const automationId = job.data.automation._id
|
||||||
console.log(jobMessage(job, "running"))
|
console.log(jobMessage(job, "running"))
|
||||||
// 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
|
||||||
const tenantId = tenancy.getTenantIDFromAppID(job.data.event.appId)
|
return await context.doInContext(job.data.event.appId, async () => {
|
||||||
return await tenancy.doInTenant(tenantId, async () => {
|
|
||||||
const runFn = () => Runner.run(job)
|
const runFn = () => Runner.run(job)
|
||||||
return quotas.addAutomation(runFn)
|
return quotas.addAutomation(runFn, {
|
||||||
|
automationId,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errJson = JSON.stringify(err)
|
const errJson = JSON.stringify(err)
|
||||||
|
|
|
@ -34,8 +34,6 @@ const DocumentType = {
|
||||||
INSTANCE: "inst",
|
INSTANCE: "inst",
|
||||||
LAYOUT: "layout",
|
LAYOUT: "layout",
|
||||||
SCREEN: "screen",
|
SCREEN: "screen",
|
||||||
DATASOURCE: "datasource",
|
|
||||||
DATASOURCE_PLUS: "datasource_plus",
|
|
||||||
QUERY: "query",
|
QUERY: "query",
|
||||||
DEPLOYMENTS: "deployments",
|
DEPLOYMENTS: "deployments",
|
||||||
METADATA: "metadata",
|
METADATA: "metadata",
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
accounts,
|
accounts,
|
||||||
db as dbUtils,
|
db as dbUtils,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { QuotaUsage } from "@budibase/pro"
|
import { QuotaUsage } from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
CloudAccount,
|
CloudAccount,
|
||||||
App,
|
App,
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { tenancy, logging } from "@budibase/backend-core"
|
|
||||||
import { plugins } from "@budibase/pro"
|
|
||||||
|
|
||||||
export const run = async () => {
|
|
||||||
try {
|
|
||||||
await tenancy.doInTenant(tenancy.DEFAULT_TENANT_ID, async () => {
|
|
||||||
await plugins.checkPluginQuotas()
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
logging.logAlert("Failed to update plugin quotas", err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +1,15 @@
|
||||||
import { runQuotaMigration } from "./usageQuotas"
|
import { runQuotaMigration } from "./usageQuotas"
|
||||||
import * as syncApps from "./usageQuotas/syncApps"
|
import * as syncApps from "./usageQuotas/syncApps"
|
||||||
import * as syncRows from "./usageQuotas/syncRows"
|
import * as syncRows from "./usageQuotas/syncRows"
|
||||||
|
import * as syncPlugins from "./usageQuotas/syncPlugins"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Date:
|
* Synchronise quotas to the state of the db.
|
||||||
* January 2022
|
|
||||||
*
|
|
||||||
* Description:
|
|
||||||
* Synchronise the app and row quotas to the state of the db after it was
|
|
||||||
* discovered that the quota resets were still in place and the row quotas
|
|
||||||
* weren't being decremented correctly.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const run = async () => {
|
export const run = async () => {
|
||||||
await runQuotaMigration(async () => {
|
await runQuotaMigration(async () => {
|
||||||
await syncApps.run()
|
await syncApps.run()
|
||||||
await syncRows.run()
|
await syncRows.run()
|
||||||
|
await syncPlugins.run()
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -2,11 +2,13 @@ const TestConfig = require("../../../tests/utilities/TestConfiguration")
|
||||||
|
|
||||||
const syncApps = jest.fn()
|
const syncApps = jest.fn()
|
||||||
const syncRows = jest.fn()
|
const syncRows = jest.fn()
|
||||||
|
const syncPlugins = jest.fn()
|
||||||
|
|
||||||
jest.mock("../usageQuotas/syncApps", () => ({ run: syncApps }) )
|
jest.mock("../usageQuotas/syncApps", () => ({ run: syncApps }) )
|
||||||
jest.mock("../usageQuotas/syncRows", () => ({ run: syncRows }) )
|
jest.mock("../usageQuotas/syncRows", () => ({ run: syncRows }) )
|
||||||
|
jest.mock("../usageQuotas/syncPlugins", () => ({ run: syncPlugins }) )
|
||||||
|
|
||||||
const migration = require("../quotas1")
|
const migration = require("../syncQuotas")
|
||||||
|
|
||||||
describe("run", () => {
|
describe("run", () => {
|
||||||
let config = new TestConfig(false)
|
let config = new TestConfig(false)
|
||||||
|
@ -17,9 +19,10 @@ describe("run", () => {
|
||||||
|
|
||||||
afterAll(config.end)
|
afterAll(config.end)
|
||||||
|
|
||||||
it("runs ", async () => {
|
it("run", async () => {
|
||||||
await migration.run()
|
await migration.run()
|
||||||
expect(syncApps).toHaveBeenCalledTimes(1)
|
expect(syncApps).toHaveBeenCalledTimes(1)
|
||||||
expect(syncRows).toHaveBeenCalledTimes(1)
|
expect(syncRows).toHaveBeenCalledTimes(1)
|
||||||
|
expect(syncPlugins).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -5,7 +5,6 @@ import { QuotaUsageType, StaticQuotaName } from "@budibase/types"
|
||||||
|
|
||||||
export const run = async () => {
|
export const run = async () => {
|
||||||
// get app count
|
// get app count
|
||||||
// @ts-ignore
|
|
||||||
const devApps = await getAllApps({ dev: true })
|
const devApps = await getAllApps({ dev: true })
|
||||||
const appCount = devApps ? devApps.length : 0
|
const appCount = devApps ? devApps.length : 0
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { logging } from "@budibase/backend-core"
|
||||||
|
import { plugins } from "@budibase/pro"
|
||||||
|
|
||||||
|
export const run = async () => {
|
||||||
|
try {
|
||||||
|
await plugins.checkPluginQuotas()
|
||||||
|
} catch (err) {
|
||||||
|
logging.logAlert("Failed to update plugin quotas", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,19 +2,28 @@ import { getTenantId } from "@budibase/backend-core/tenancy"
|
||||||
import { getAllApps } from "@budibase/backend-core/db"
|
import { getAllApps } from "@budibase/backend-core/db"
|
||||||
import { getUniqueRows } from "../../../utilities/usageQuota/rows"
|
import { getUniqueRows } from "../../../utilities/usageQuota/rows"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import { QuotaUsageType, StaticQuotaName } from "@budibase/types"
|
import { StaticQuotaName, QuotaUsageType } from "@budibase/types"
|
||||||
|
|
||||||
export const run = async () => {
|
export const run = async () => {
|
||||||
// get all rows in all apps
|
// get all rows in all apps
|
||||||
// @ts-ignore
|
|
||||||
const allApps = await getAllApps({ all: true })
|
const allApps = await getAllApps({ all: true })
|
||||||
// @ts-ignore
|
|
||||||
const appIds = allApps ? allApps.map((app: { appId: any }) => app.appId) : []
|
const appIds = allApps ? allApps.map((app: { appId: any }) => app.appId) : []
|
||||||
const rows = await getUniqueRows(appIds)
|
const { appRows } = await getUniqueRows(appIds)
|
||||||
const rowCount = rows ? rows.length : 0
|
|
||||||
|
// get the counts per app
|
||||||
|
const counts: { [key: string]: number } = {}
|
||||||
|
let rowCount = 0
|
||||||
|
Object.entries(appRows).forEach(([appId, rows]) => {
|
||||||
|
counts[appId] = rows.length
|
||||||
|
rowCount += rows.length
|
||||||
|
})
|
||||||
|
|
||||||
// sync row count
|
// sync row count
|
||||||
const tenantId = getTenantId()
|
const tenantId = getTenantId()
|
||||||
console.log(`[Tenant: ${tenantId}] Syncing row count: ${rowCount}`)
|
console.log(`[Tenant: ${tenantId}] Syncing row count: ${rowCount}`)
|
||||||
await quotas.setUsage(rowCount, StaticQuotaName.ROWS, QuotaUsageType.STATIC)
|
await quotas.setUsagePerApp(
|
||||||
|
counts,
|
||||||
|
StaticQuotaName.ROWS,
|
||||||
|
QuotaUsageType.STATIC
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import TestConfig from "../../../../tests/utilities/TestConfiguration"
|
||||||
import * as syncRows from "../syncRows"
|
import * as syncRows from "../syncRows"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import { QuotaUsageType, StaticQuotaName } from "@budibase/types"
|
import { QuotaUsageType, StaticQuotaName } from "@budibase/types"
|
||||||
|
const { getProdAppID } = require("@budibase/backend-core/db")
|
||||||
|
|
||||||
describe("syncRows", () => {
|
describe("syncRows", () => {
|
||||||
let config = new TestConfig(false)
|
let config = new TestConfig(false)
|
||||||
|
@ -22,10 +23,11 @@ describe("syncRows", () => {
|
||||||
expect(usageDoc.usageQuota.rows).toEqual(300)
|
expect(usageDoc.usageQuota.rows).toEqual(300)
|
||||||
|
|
||||||
// app 1
|
// app 1
|
||||||
|
const app1 = config.app
|
||||||
await config.createTable()
|
await config.createTable()
|
||||||
await config.createRow()
|
await config.createRow()
|
||||||
// app 2
|
// app 2
|
||||||
await config.createApp("second-app")
|
const app2 = await config.createApp("second-app")
|
||||||
await config.createTable()
|
await config.createTable()
|
||||||
await config.createRow()
|
await config.createRow()
|
||||||
await config.createRow()
|
await config.createRow()
|
||||||
|
@ -36,6 +38,12 @@ describe("syncRows", () => {
|
||||||
// assert the migration worked
|
// assert the migration worked
|
||||||
usageDoc = await quotas.getQuotaUsage()
|
usageDoc = await quotas.getQuotaUsage()
|
||||||
expect(usageDoc.usageQuota.rows).toEqual(3)
|
expect(usageDoc.usageQuota.rows).toEqual(3)
|
||||||
|
expect(usageDoc.apps?.[getProdAppID(app1.appId)].usageQuota.rows).toEqual(
|
||||||
|
1
|
||||||
|
)
|
||||||
|
expect(usageDoc.apps?.[getProdAppID(app2.appId)].usageQuota.rows).toEqual(
|
||||||
|
2
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,11 +4,9 @@ import env from "../environment"
|
||||||
|
|
||||||
// migration functions
|
// migration functions
|
||||||
import * as userEmailViewCasing from "./functions/userEmailViewCasing"
|
import * as userEmailViewCasing from "./functions/userEmailViewCasing"
|
||||||
import * as quota1 from "./functions/quotas1"
|
import * as syncQuotas from "./functions/syncQuotas"
|
||||||
import * as appUrls from "./functions/appUrls"
|
import * as appUrls from "./functions/appUrls"
|
||||||
import * as backfill from "./functions/backfill"
|
import * as backfill from "./functions/backfill"
|
||||||
import * as pluginCount from "./functions/pluginCount"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populate the migration function and additional configuration from
|
* Populate the migration function and additional configuration from
|
||||||
* the static migration definitions.
|
* the static migration definitions.
|
||||||
|
@ -26,10 +24,10 @@ export const buildMigrations = () => {
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case MigrationName.QUOTAS_1: {
|
case MigrationName.SYNC_QUOTAS: {
|
||||||
serverMigrations.push({
|
serverMigrations.push({
|
||||||
...definition,
|
...definition,
|
||||||
fn: quota1.run,
|
fn: syncQuotas.run,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -69,16 +67,6 @@ export const buildMigrations = () => {
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case MigrationName.PLUGIN_COUNT: {
|
|
||||||
if (env.SELF_HOSTED) {
|
|
||||||
serverMigrations.push({
|
|
||||||
...definition,
|
|
||||||
fn: pluginCount.run,
|
|
||||||
silent: !!env.SELF_HOSTED,
|
|
||||||
preventRetry: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
tenancy,
|
tenancy,
|
||||||
DocumentType,
|
DocumentType,
|
||||||
context,
|
context,
|
||||||
db,
|
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import TestConfig from "../../tests/utilities/TestConfiguration"
|
import TestConfig from "../../tests/utilities/TestConfiguration"
|
||||||
import structures from "../../tests/utilities/structures"
|
import structures from "../../tests/utilities/structures"
|
||||||
|
|
|
@ -2,6 +2,7 @@ const { getRowParams, USER_METDATA_PREFIX } = require("../../db/utils")
|
||||||
const {
|
const {
|
||||||
isDevAppID,
|
isDevAppID,
|
||||||
getDevelopmentAppID,
|
getDevelopmentAppID,
|
||||||
|
getProdAppID,
|
||||||
doWithDB,
|
doWithDB,
|
||||||
} = require("@budibase/backend-core/db")
|
} = require("@budibase/backend-core/db")
|
||||||
|
|
||||||
|
@ -52,7 +53,8 @@ const getAppRows = async appId => {
|
||||||
* Rows duplicates may exist across apps due to data import so they are not filtered out.
|
* Rows duplicates may exist across apps due to data import so they are not filtered out.
|
||||||
*/
|
*/
|
||||||
exports.getUniqueRows = async appIds => {
|
exports.getUniqueRows = async appIds => {
|
||||||
let uniqueRows = []
|
let uniqueRows = [],
|
||||||
|
rowsByApp = {}
|
||||||
const pairs = getAppPairs(appIds)
|
const pairs = getAppPairs(appIds)
|
||||||
|
|
||||||
for (let pair of Object.values(pairs)) {
|
for (let pair of Object.values(pairs)) {
|
||||||
|
@ -73,8 +75,10 @@ exports.getUniqueRows = async appIds => {
|
||||||
// this can't be done on all rows because app import results in
|
// this can't be done on all rows because app import results in
|
||||||
// duplicate row ids across apps
|
// duplicate row ids across apps
|
||||||
// the array pre-concat is important to avoid stack overflow
|
// the array pre-concat is important to avoid stack overflow
|
||||||
uniqueRows = uniqueRows.concat([...new Set(appRows)])
|
const prodId = getProdAppID(pair.devId || pair.prodId)
|
||||||
|
rowsByApp[prodId] = [...new Set(appRows)]
|
||||||
|
uniqueRows = uniqueRows.concat(rowsByApp[prodId])
|
||||||
}
|
}
|
||||||
|
|
||||||
return uniqueRows
|
return { rows: uniqueRows, appRows: rowsByApp }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,58 @@
|
||||||
import { MonthlyQuotaName, StaticQuotaName } from "../../sdk"
|
import { MonthlyQuotaName, StaticQuotaName } from "../../sdk"
|
||||||
|
|
||||||
export interface QuotaUsage {
|
export enum BreakdownQuotaName {
|
||||||
_id: string
|
ROW_QUERIES = "rowQueries",
|
||||||
_rev?: string
|
DATASOURCE_QUERIES = "datasourceQueries",
|
||||||
quotaReset: string
|
AUTOMATIONS = "automations",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const APP_QUOTA_NAMES = [
|
||||||
|
StaticQuotaName.ROWS,
|
||||||
|
MonthlyQuotaName.QUERIES,
|
||||||
|
MonthlyQuotaName.AUTOMATIONS,
|
||||||
|
]
|
||||||
|
|
||||||
|
export const BREAKDOWN_QUOTA_NAMES = [
|
||||||
|
MonthlyQuotaName.QUERIES,
|
||||||
|
MonthlyQuotaName.AUTOMATIONS,
|
||||||
|
]
|
||||||
|
|
||||||
|
export interface UsageBreakdown {
|
||||||
|
parent: MonthlyQuotaName
|
||||||
|
values: {
|
||||||
|
[key: string]: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MonthlyUsage = {
|
||||||
|
[MonthlyQuotaName.QUERIES]: number
|
||||||
|
[MonthlyQuotaName.AUTOMATIONS]: number
|
||||||
|
[MonthlyQuotaName.DAY_PASSES]: number
|
||||||
|
breakdown?: {
|
||||||
|
[key in BreakdownQuotaName]?: UsageBreakdown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseQuotaUsage {
|
||||||
usageQuota: {
|
usageQuota: {
|
||||||
[key in StaticQuotaName]: number
|
[key in StaticQuotaName]: number
|
||||||
}
|
}
|
||||||
monthly: {
|
monthly: {
|
||||||
[key: string]: {
|
[key: string]: MonthlyUsage
|
||||||
[key in MonthlyQuotaName]: number
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface QuotaUsage extends BaseQuotaUsage {
|
||||||
|
_id: string
|
||||||
|
_rev?: string
|
||||||
|
quotaReset: string
|
||||||
|
apps?: {
|
||||||
|
[key: string]: BaseQuotaUsage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UsageValues = {
|
||||||
|
total: number
|
||||||
|
app?: number
|
||||||
|
breakdown?: number
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ export enum ConstantQuotaName {
|
||||||
AUTOMATION_LOG_RETENTION_DAYS = "automationLogRetentionDays",
|
AUTOMATION_LOG_RETENTION_DAYS = "automationLogRetentionDays",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MeteredQuotaName = StaticQuotaName | MonthlyQuotaName
|
||||||
export type QuotaName = StaticQuotaName | MonthlyQuotaName | ConstantQuotaName
|
export type QuotaName = StaticQuotaName | MonthlyQuotaName | ConstantQuotaName
|
||||||
|
|
||||||
export const isStaticQuota = (
|
export const isStaticQuota = (
|
||||||
|
|
|
@ -39,14 +39,13 @@ export interface MigrationOptions {
|
||||||
|
|
||||||
export enum MigrationName {
|
export enum MigrationName {
|
||||||
USER_EMAIL_VIEW_CASING = "user_email_view_casing",
|
USER_EMAIL_VIEW_CASING = "user_email_view_casing",
|
||||||
QUOTAS_1 = "quotas_1",
|
|
||||||
APP_URLS = "app_urls",
|
APP_URLS = "app_urls",
|
||||||
EVENT_APP_BACKFILL = "event_app_backfill",
|
EVENT_APP_BACKFILL = "event_app_backfill",
|
||||||
EVENT_GLOBAL_BACKFILL = "event_global_backfill",
|
EVENT_GLOBAL_BACKFILL = "event_global_backfill",
|
||||||
EVENT_INSTALLATION_BACKFILL = "event_installation_backfill",
|
EVENT_INSTALLATION_BACKFILL = "event_installation_backfill",
|
||||||
GLOBAL_INFO_SYNC_USERS = "global_info_sync_users",
|
GLOBAL_INFO_SYNC_USERS = "global_info_sync_users",
|
||||||
PLATFORM_USERS_EMAIL_CASING = "platform_users_email_casing",
|
// increment this number to re-activate this migration
|
||||||
PLUGIN_COUNT = "plugin_count",
|
SYNC_QUOTAS = "sync_quotas_1",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MigrationDefinition {
|
export interface MigrationDefinition {
|
||||||
|
|
Loading…
Reference in New Issue