Self Host <-> Licensing integration

This commit is contained in:
Rory Powell 2022-03-09 21:16:22 +00:00
parent eefe4ea2ad
commit 7698e9293c
26 changed files with 1605 additions and 191 deletions

View File

@ -22,25 +22,3 @@ exports.getAccount = async email => {
return json[0] return json[0]
} }
// TODO: Replace with licensing key
exports.getLicense = async tenantId => {
const response = await api.get(`/api/license/${tenantId}`, {
headers: {
[Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY,
},
})
if (response.status === 404) {
// no license for the tenant
return
}
if (response.status !== 200) {
const text = await response.text()
console.error("Error getting license: ", text)
throw new Error(`Error getting license for tenant ${tenantId}`)
}
return response.json()
}

View File

@ -13,6 +13,7 @@ exports.Cookies = {
exports.Headers = { exports.Headers = {
API_KEY: "x-budibase-api-key", API_KEY: "x-budibase-api-key",
LICENSE_KEY: "x-budibase-license-key",
API_VER: "x-budibase-api-version", API_VER: "x-budibase-api-version",
APP_ID: "x-budibase-app-id", APP_ID: "x-budibase-app-id",
TYPE: "x-budibase-type", TYPE: "x-budibase-type",

View File

@ -22,6 +22,7 @@ exports.StaticDatabases = {
docs: { docs: {
apiKeys: "apikeys", apiKeys: "apikeys",
usageQuota: "usage_quota", usageQuota: "usage_quota",
licenseInfo: "license_info",
}, },
}, },
// contains information about tenancy and so on // contains information about tenancy and so on

View File

@ -55,6 +55,10 @@
title: "Updates", title: "Updates",
href: "/builder/portal/settings/update", href: "/builder/portal/settings/update",
}, },
{
title: "Upgrade",
href: "/builder/portal/settings/upgrade",
},
]) ])
} }
} else { } else {

View File

@ -0,0 +1,151 @@
<script>
import {
Layout,
Heading,
Body,
Divider,
Link,
Button,
Input,
Label,
notifications,
} from "@budibase/bbui"
import { auth, admin } from "stores/portal"
import { redirect } from "@roxi/routify"
import { processStringSync } from "@budibase/string-templates"
import { API } from "api"
import { onMount } from "svelte"
$: license = $auth.user.license
$: upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
$: activateDisabled = !licenseKey || licenseKeyDisabled
let licenseInfo
let licenseKeyDisabled = false
let licenseKeyType = "text"
let licenseKey = ""
// Make sure page can't be visited directly in cloud
$: {
if ($admin.cloud) {
$redirect("../../portal")
}
}
const activate = async () => {
await API.activateLicenseKey({ licenseKey })
await auth.getSelf()
await setLicenseInfo()
notifications.success("Successfully activated")
}
const refresh = async () => {
try {
await API.refreshLicense()
await auth.getSelf()
notifications.success("Refreshed license")
} catch (err) {
console.error(err)
notifications.error("Error refreshing license")
}
}
// deactivate the license key field if there is a license key set
$: {
if (licenseInfo?.licenseKey) {
licenseKey = "**********************************************"
licenseKeyType = "password"
licenseKeyDisabled = true
activateDisabled = true
}
}
const setLicenseInfo = async () => {
licenseInfo = await API.getLicenseInfo()
}
onMount(async () => {
await setLicenseInfo()
})
</script>
{#if $auth.isAdmin}
<Layout noPadding>
<Layout gap="XS" noPadding>
<Heading size="M">Upgrade</Heading>
<Body size="M">
{#if license.plan.type === "free"}
Upgrade your budibase installation to unlock additional features. To
subscribe to a plan visit your <Link size="L" href={upgradeUrl}
>Account</Link
>.
{:else}
To manage your plan visit your <Link size="L" href={upgradeUrl}
>Account</Link
>.
{/if}
</Body>
</Layout>
<Divider size="S" />
<Layout gap="XS" noPadding>
<Heading size="S">Activate</Heading>
<Body size="S">Enter your license key below to activate your plan</Body>
</Layout>
<Layout noPadding>
<div class="fields">
<div class="field">
<Label size="L">License Key</Label>
<Input
thin
bind:value={licenseKey}
type={licenseKeyType}
disabled={licenseKeyDisabled}
/>
</div>
</div>
<div>
<Button cta on:click={activate} disabled={activateDisabled}
>Activate</Button
>
</div>
</Layout>
<Divider size="S" />
<Layout gap="L" noPadding>
<Layout gap="S" noPadding>
<Heading size="S">Plan</Heading>
<Layout noPadding gap="XXS">
<Body size="S">You are currently on the {license.plan.type} plan</Body
>
<Body size="XS">
{processStringSync(
"Updated {{ duration time 'millisecond' }} ago",
{
time:
new Date().getTime() -
new Date(license.refreshedAt).getTime(),
}
)}
</Body>
</Layout>
</Layout>
<div>
<Button secondary on:click={refresh}>Refresh</Button>
</div>
</Layout>
</Layout>
{/if}
<style>
.fields {
display: grid;
grid-gap: var(--spacing-m);
}
.field {
display: grid;
grid-template-columns: 100px 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
</style>

View File

@ -21,6 +21,7 @@ import { buildTableEndpoints } from "./tables"
import { buildTemplateEndpoints } from "./templates" import { buildTemplateEndpoints } from "./templates"
import { buildUserEndpoints } from "./user" import { buildUserEndpoints } from "./user"
import { buildViewEndpoints } from "./views" import { buildViewEndpoints } from "./views"
import { buildLicensingEndpoints } from "./licensing"
const defaultAPIClientConfig = { const defaultAPIClientConfig = {
/** /**
@ -231,5 +232,6 @@ export const createAPIClient = config => {
...buildTemplateEndpoints(API), ...buildTemplateEndpoints(API),
...buildUserEndpoints(API), ...buildUserEndpoints(API),
...buildViewEndpoints(API), ...buildViewEndpoints(API),
...buildLicensingEndpoints(API),
} }
} }

View File

@ -0,0 +1,30 @@
export const buildLicensingEndpoints = API => ({
/**
* Activates a self hosted license key
*/
activateLicenseKey: async data => {
return API.post({
url: `/api/global/license/activate`,
body: data,
})
},
/**
* Get the license info - metadata about the license including the
* obfuscated license key.
*/
getLicenseInfo: async () => {
return API.get({
url: "/api/global/license/info",
})
},
/**
* Refreshes the license cache
*/
refreshLicense: async () => {
return API.post({
url: "/api/global/license/refresh",
})
},
})

View File

@ -148,6 +148,7 @@
"@types/jest": "^26.0.23", "@types/jest": "^26.0.23",
"@types/koa": "^2.13.3", "@types/koa": "^2.13.3",
"@types/koa-router": "^7.4.2", "@types/koa-router": "^7.4.2",
"@types/lodash": "^4.14.179",
"@types/node": "^15.12.4", "@types/node": "^15.12.4",
"@types/oracledb": "^5.2.1", "@types/oracledb": "^5.2.1",
"@typescript-eslint/parser": "5.12.0", "@typescript-eslint/parser": "5.12.0",

View File

@ -52,7 +52,7 @@ interface RunConfig {
module External { module External {
function buildFilters( function buildFilters(
id: string | undefined, id: string | undefined | string[],
filters: SearchFilters, filters: SearchFilters,
table: Table table: Table
) { ) {

View File

@ -1,19 +1,19 @@
const linkRows = require("../../../db/linkedRows") import { updateLinks, EventType } from "../../../db/linkedRows"
const { getRowParams, generateTableID } = require("../../../db/utils") import { getRowParams, generateTableID } from "../../../db/utils"
const { FieldTypes } = require("../../../constants") import { FieldTypes } from "../../../constants"
const { import {
TableSaveFunctions, TableSaveFunctions,
hasTypeChanged, hasTypeChanged,
getTable, getTable,
handleDataImport, handleDataImport,
} = require("./utils") } from "./utils"
const { quotas, StaticQuotaName, QuotaUsageType } = require("@budibase/pro")
const { getAppDB } = require("@budibase/backend-core/context") const { getAppDB } = require("@budibase/backend-core/context")
const env = require("../../../environment") import { isTest } from "../../../environment"
const { cleanupAttachments } = require("../../../utilities/rowProcessor") import { cleanupAttachments } from "../../../utilities/rowProcessor"
const { runStaticFormulaChecks } = require("./bulkFormula") import { runStaticFormulaChecks } from "./bulkFormula"
import * as Pro from "@budibase/pro"
exports.save = async function (ctx) { export async function save(ctx: any) {
const db = getAppDB() const db = getAppDB()
const { dataImport, ...rest } = ctx.request.body const { dataImport, ...rest } = ctx.request.body
let tableToSave = { let tableToSave = {
@ -80,10 +80,8 @@ exports.save = async function (ctx) {
// update linked rows // update linked rows
try { try {
const linkResp = await linkRows.updateLinks({ const linkResp: any = await updateLinks({
eventType: oldTable eventType: oldTable ? EventType.TABLE_UPDATED : EventType.TABLE_SAVE,
? linkRows.EventType.TABLE_UPDATED
: linkRows.EventType.TABLE_SAVE,
table: tableToSave, table: tableToSave,
oldTable: oldTable, oldTable: oldTable,
}) })
@ -105,11 +103,11 @@ exports.save = async function (ctx) {
tableToSave = await tableSaveFunctions.after(tableToSave) tableToSave = await tableSaveFunctions.after(tableToSave)
// has to run after, make sure it has _id // has to run after, make sure it has _id
await runStaticFormulaChecks(tableToSave, { oldTable }) await runStaticFormulaChecks(tableToSave, { oldTable, deletion: null })
return tableToSave return tableToSave
} }
exports.destroy = async function (ctx) { export async function destroy(ctx: any) {
const db = getAppDB() const db = getAppDB()
const tableToDelete = await db.get(ctx.params.tableId) const tableToDelete = await db.get(ctx.params.tableId)
@ -119,16 +117,18 @@ exports.destroy = async function (ctx) {
include_docs: true, include_docs: true,
}) })
) )
await db.bulkDocs(rows.rows.map(row => ({ ...row.doc, _deleted: true }))) await db.bulkDocs(
await quotas.updateUsage( rows.rows.map((row: any) => ({ ...row.doc, _deleted: true }))
)
await Pro.Licensing.Quotas.updateUsage(
-rows.rows.length, -rows.rows.length,
StaticQuotaName.ROWS, Pro.StaticQuotaName.ROWS,
QuotaUsageType.STATIC Pro.QuotaUsageType.STATIC
) )
// update linked rows // update linked rows
await linkRows.updateLinks({ await updateLinks({
eventType: linkRows.EventType.TABLE_DELETE, eventType: EventType.TABLE_DELETE,
table: tableToDelete, table: tableToDelete,
}) })
@ -136,10 +136,10 @@ exports.destroy = async function (ctx) {
await db.remove(tableToDelete) await db.remove(tableToDelete)
// remove table search index // remove table search index
if (!env.isTest()) { if (!isTest()) {
const currentIndexes = await db.getIndexes() const currentIndexes = await db.getIndexes()
const existingIndex = currentIndexes.indexes.find( const existingIndex = currentIndexes.indexes.find(
existing => existing.name === `search:${ctx.params.tableId}` (existing: any) => existing.name === `search:${ctx.params.tableId}`
) )
if (existingIndex) { if (existingIndex) {
await db.deleteIndex(existingIndex) await db.deleteIndex(existingIndex)
@ -147,12 +147,15 @@ exports.destroy = async function (ctx) {
} }
// has to run after, make sure it has _id // has to run after, make sure it has _id
await runStaticFormulaChecks(tableToDelete, { deletion: true }) await runStaticFormulaChecks(tableToDelete, {
oldTable: null,
deletion: true,
})
await cleanupAttachments(tableToDelete, { rows }) await cleanupAttachments(tableToDelete, { rows })
return tableToDelete return tableToDelete
} }
exports.bulkImport = async function (ctx) { export async function bulkImport(ctx: any) {
const table = await getTable(ctx.params.tableId) const table = await getTable(ctx.params.tableId)
const { dataImport } = ctx.request.body const { dataImport } = ctx.request.body
await handleDataImport(ctx.user, table, dataImport) await handleDataImport(ctx.user, table, dataImport)

View File

@ -1,34 +1,34 @@
const csvParser = require("../../../utilities/csvParser") import { transform } from "../../../utilities/csvParser"
const { import {
getRowParams, getRowParams,
generateRowID, generateRowID,
InternalTables, InternalTables,
getTableParams, getTableParams,
BudibaseInternalDB, BudibaseInternalDB,
} = require("../../../db/utils") } from "../../../db/utils"
const { isEqual } = require("lodash") import { isEqual } from "lodash"
const { AutoFieldSubTypes, FieldTypes } = require("../../../constants") import { AutoFieldSubTypes, FieldTypes } from "../../../constants"
const { import {
inputProcessing, inputProcessing,
cleanupAttachments, cleanupAttachments,
} = require("../../../utilities/rowProcessor") } from "../../../utilities/rowProcessor"
const { import {
USERS_TABLE_SCHEMA, USERS_TABLE_SCHEMA,
SwitchableTypes, SwitchableTypes,
CanSwitchTypes, CanSwitchTypes,
} = require("../../../constants") } from "../../../constants"
const { import {
isExternalTable, isExternalTable,
breakExternalTableId, breakExternalTableId,
isSQL, isSQL,
} = require("../../../integrations/utils") } from "../../../integrations/utils"
const { getViews, saveView } = require("../view/utils") import { getViews, saveView } from "../view/utils"
const viewTemplate = require("../view/viewBuilder") import viewTemplate from "../view/viewBuilder"
const { quotas, StaticQuotaName, QuotaUsageType } = require("@budibase/pro")
const { getAppDB } = require("@budibase/backend-core/context") const { getAppDB } = require("@budibase/backend-core/context")
const { cloneDeep } = require("lodash/fp") import { cloneDeep } from "lodash/fp"
import * as Pro from "@budibase/pro"
exports.clearColumns = async (table, columnNames) => { export async function clearColumns(table: any, columnNames: any) {
const db = getAppDB() const db = getAppDB()
const rows = await db.allDocs( const rows = await db.allDocs(
getRowParams(table._id, null, { getRowParams(table._id, null, {
@ -36,18 +36,18 @@ exports.clearColumns = async (table, columnNames) => {
}) })
) )
return db.bulkDocs( return db.bulkDocs(
rows.rows.map(({ doc }) => { rows.rows.map(({ doc }: any) => {
columnNames.forEach(colName => delete doc[colName]) columnNames.forEach((colName: any) => delete doc[colName])
return doc return doc
}) })
) )
} }
exports.checkForColumnUpdates = async (oldTable, updatedTable) => { export async function checkForColumnUpdates(oldTable: any, updatedTable: any) {
const db = getAppDB() const db = getAppDB()
let updatedRows = [] let updatedRows = []
const rename = updatedTable._rename const rename = updatedTable._rename
let deletedColumns = [] let deletedColumns: any = []
if (oldTable && oldTable.schema && updatedTable.schema) { if (oldTable && oldTable.schema && updatedTable.schema) {
deletedColumns = Object.keys(oldTable.schema).filter( deletedColumns = Object.keys(oldTable.schema).filter(
colName => updatedTable.schema[colName] == null colName => updatedTable.schema[colName] == null
@ -61,14 +61,14 @@ exports.checkForColumnUpdates = async (oldTable, updatedTable) => {
include_docs: true, include_docs: true,
}) })
) )
const rawRows = rows.rows.map(({ doc }) => doc) const rawRows = rows.rows.map(({ doc }: any) => doc)
updatedRows = rawRows.map(row => { updatedRows = rawRows.map((row: any) => {
row = cloneDeep(row) row = cloneDeep(row)
if (rename) { if (rename) {
row[rename.updated] = row[rename.old] row[rename.updated] = row[rename.old]
delete row[rename.old] delete row[rename.old]
} else if (deletedColumns.length !== 0) { } else if (deletedColumns.length !== 0) {
deletedColumns.forEach(colName => delete row[colName]) deletedColumns.forEach((colName: any) => delete row[colName])
} }
return row return row
}) })
@ -76,14 +76,14 @@ exports.checkForColumnUpdates = async (oldTable, updatedTable) => {
// cleanup any attachments from object storage for deleted attachment columns // cleanup any attachments from object storage for deleted attachment columns
await cleanupAttachments(updatedTable, { oldTable, rows: rawRows }) await cleanupAttachments(updatedTable, { oldTable, rows: rawRows })
// Update views // Update views
await exports.checkForViewUpdates(updatedTable, rename, deletedColumns) await checkForViewUpdates(updatedTable, rename, deletedColumns)
delete updatedTable._rename delete updatedTable._rename
} }
return { rows: updatedRows, table: updatedTable } return { rows: updatedRows, table: updatedTable }
} }
// makes sure the passed in table isn't going to reset the auto ID // makes sure the passed in table isn't going to reset the auto ID
exports.makeSureTableUpToDate = (table, tableToSave) => { export function makeSureTableUpToDate(table: any, tableToSave: any) {
if (!table) { if (!table) {
return tableToSave return tableToSave
} }
@ -91,7 +91,9 @@ exports.makeSureTableUpToDate = (table, tableToSave) => {
tableToSave._rev = table._rev tableToSave._rev = table._rev
// make sure auto IDs are always updated - these are internal // make sure auto IDs are always updated - these are internal
// so the client may not know they have changed // so the client may not know they have changed
for (let [field, column] of Object.entries(table.schema)) { let field: any
let column: any
for ([field, column] of Object.entries(table.schema)) {
if ( if (
column.autocolumn && column.autocolumn &&
column.subtype === AutoFieldSubTypes.AUTO_ID && column.subtype === AutoFieldSubTypes.AUTO_ID &&
@ -103,14 +105,14 @@ exports.makeSureTableUpToDate = (table, tableToSave) => {
return tableToSave return tableToSave
} }
exports.handleDataImport = async (user, table, dataImport) => { export async function handleDataImport(user: any, table: any, dataImport: any) {
if (!dataImport || !dataImport.csvString) { if (!dataImport || !dataImport.csvString) {
return table return table
} }
const db = getAppDB() const db = getAppDB()
// Populate the table with rows imported from CSV in a bulk update // Populate the table with rows imported from CSV in a bulk update
const data = await csvParser.transform({ const data = await transform({
...dataImport, ...dataImport,
existingTable: table, existingTable: table,
}) })
@ -120,13 +122,15 @@ exports.handleDataImport = async (user, table, dataImport) => {
let row = data[i] let row = data[i]
row._id = generateRowID(table._id) row._id = generateRowID(table._id)
row.tableId = table._id row.tableId = table._id
const processed = inputProcessing(user, table, row, { const processed: any = inputProcessing(user, table, row, {
noAutoRelationships: true, noAutoRelationships: true,
}) })
table = processed.table table = processed.table
row = processed.row row = processed.row
for (let [fieldName, schema] of Object.entries(table.schema)) { let fieldName: any
let schema: any
for ([fieldName, schema] of Object.entries(table.schema)) {
// check whether the options need to be updated for inclusion as part of the data import // check whether the options need to be updated for inclusion as part of the data import
if ( if (
schema.type === FieldTypes.OPTIONS && schema.type === FieldTypes.OPTIONS &&
@ -143,26 +147,26 @@ exports.handleDataImport = async (user, table, dataImport) => {
finalData.push(row) finalData.push(row)
} }
await quotas.updateUsage( await Pro.Licensing.Quotas.updateUsage(
finalData.length, finalData.length,
StaticQuotaName.ROWS, Pro.StaticQuotaName.ROWS,
QuotaUsageType.STATIC, Pro.QuotaUsageType.STATIC,
{ {
dryRun: true, dryRun: true,
} }
) )
await db.bulkDocs(finalData) await db.bulkDocs(finalData)
await quotas.updateUsage( await Pro.Licensing.Quotas.updateUsage(
finalData.length, finalData.length,
StaticQuotaName.ROWS, Pro.StaticQuotaName.ROWS,
QuotaUsageType.STATIC Pro.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
} }
exports.handleSearchIndexes = async table => { export async function handleSearchIndexes(table: any) {
const db = getAppDB() const db = getAppDB()
// create relevant search indexes // create relevant search indexes
if (table.indexes && table.indexes.length > 0) { if (table.indexes && table.indexes.length > 0) {
@ -170,12 +174,12 @@ exports.handleSearchIndexes = async table => {
const indexName = `search:${table._id}` const indexName = `search:${table._id}`
const existingIndex = currentIndexes.indexes.find( const existingIndex = currentIndexes.indexes.find(
existing => existing.name === indexName (existing: any) => existing.name === indexName
) )
if (existingIndex) { if (existingIndex) {
const currentFields = existingIndex.def.fields.map( const currentFields = existingIndex.def.fields.map(
field => Object.keys(field)[0] (field: any) => Object.keys(field)[0]
) )
// if index fields have changed, delete the original index // if index fields have changed, delete the original index
@ -206,7 +210,7 @@ exports.handleSearchIndexes = async table => {
return table return table
} }
exports.checkStaticTables = table => { export function checkStaticTables(table: any) {
// check user schema has all required elements // check user schema has all required elements
if (table._id === InternalTables.USER_METADATA) { if (table._id === InternalTables.USER_METADATA) {
for (let [key, schema] of Object.entries(USERS_TABLE_SCHEMA.schema)) { for (let [key, schema] of Object.entries(USERS_TABLE_SCHEMA.schema)) {
@ -220,7 +224,13 @@ exports.checkStaticTables = table => {
} }
class TableSaveFunctions { class TableSaveFunctions {
constructor({ user, oldTable, dataImport }) { db: any
user: any
oldTable: any
dataImport: any
rows: any
constructor({ user, oldTable, dataImport }: any) {
this.db = getAppDB() this.db = getAppDB()
this.user = user this.user = user
this.oldTable = oldTable this.oldTable = oldTable
@ -230,25 +240,25 @@ class TableSaveFunctions {
} }
// before anything is done // before anything is done
async before(table) { async before(table: any) {
if (this.oldTable) { if (this.oldTable) {
table = exports.makeSureTableUpToDate(this.oldTable, table) table = makeSureTableUpToDate(this.oldTable, table)
} }
table = exports.checkStaticTables(table) table = checkStaticTables(table)
return table return table
} }
// when confirmed valid // when confirmed valid
async mid(table) { async mid(table: any) {
let response = await exports.checkForColumnUpdates(this.oldTable, table) let response = await checkForColumnUpdates(this.oldTable, table)
this.rows = this.rows.concat(response.rows) this.rows = this.rows.concat(response.rows)
return table return table
} }
// after saving // after saving
async after(table) { async after(table: any) {
table = await exports.handleSearchIndexes(table) table = await handleSearchIndexes(table)
table = await exports.handleDataImport(this.user, table, this.dataImport) table = await handleDataImport(this.user, table, this.dataImport)
return table return table
} }
@ -257,21 +267,21 @@ class TableSaveFunctions {
} }
} }
exports.getAllInternalTables = async () => { export async function getAllInternalTables() {
const db = getAppDB() const db = getAppDB()
const internalTables = await db.allDocs( const internalTables = await db.allDocs(
getTableParams(null, { getTableParams(null, {
include_docs: true, include_docs: true,
}) })
) )
return internalTables.rows.map(tableDoc => ({ return internalTables.rows.map((tableDoc: any) => ({
...tableDoc.doc, ...tableDoc.doc,
type: "internal", type: "internal",
sourceId: BudibaseInternalDB._id, sourceId: BudibaseInternalDB._id,
})) }))
} }
exports.getAllExternalTables = async datasourceId => { export async function getAllExternalTables(datasourceId: any) {
const db = getAppDB() const db = getAppDB()
const datasource = await db.get(datasourceId) const datasource = await db.get(datasourceId)
if (!datasource || !datasource.entities) { if (!datasource || !datasource.entities) {
@ -280,24 +290,28 @@ exports.getAllExternalTables = async datasourceId => {
return datasource.entities return datasource.entities
} }
exports.getExternalTable = async (datasourceId, tableName) => { export async function getExternalTable(datasourceId: any, tableName: any) {
const entities = await exports.getAllExternalTables(datasourceId) const entities = await getAllExternalTables(datasourceId)
return entities[tableName] return entities[tableName]
} }
exports.getTable = async tableId => { export async function getTable(tableId: any) {
const db = getAppDB() const db = getAppDB()
if (isExternalTable(tableId)) { if (isExternalTable(tableId)) {
let { datasourceId, tableName } = breakExternalTableId(tableId) let { datasourceId, tableName } = breakExternalTableId(tableId)
const datasource = await db.get(datasourceId) const datasource = await db.get(datasourceId)
const table = await exports.getExternalTable(datasourceId, tableName) const table = await getExternalTable(datasourceId, tableName)
return { ...table, sql: isSQL(datasource) } return { ...table, sql: isSQL(datasource) }
} else { } else {
return db.get(tableId) return db.get(tableId)
} }
} }
exports.checkForViewUpdates = async (table, rename, deletedColumns) => { export async function checkForViewUpdates(
table: any,
rename: any,
deletedColumns: any
) {
const views = await getViews() const views = await getViews()
const tableViews = views.filter(view => view.meta.tableId === table._id) const tableViews = views.filter(view => view.meta.tableId === table._id)
@ -321,7 +335,7 @@ exports.checkForViewUpdates = async (table, rename, deletedColumns) => {
// Update filters if required // Update filters if required
if (view.meta.filters) { if (view.meta.filters) {
view.meta.filters.forEach(filter => { view.meta.filters.forEach((filter: any) => {
if (filter.key === rename.old) { if (filter.key === rename.old) {
filter.key = rename.updated filter.key = rename.updated
needsUpdated = true needsUpdated = true
@ -329,7 +343,7 @@ exports.checkForViewUpdates = async (table, rename, deletedColumns) => {
}) })
} }
} else if (deletedColumns) { } else if (deletedColumns) {
deletedColumns.forEach(column => { deletedColumns.forEach((column: any) => {
// Remove calculation statement if required // Remove calculation statement if required
if (view.meta.field === column) { if (view.meta.field === column) {
delete view.meta.field delete view.meta.field
@ -347,7 +361,7 @@ exports.checkForViewUpdates = async (table, rename, deletedColumns) => {
// Remove filters referencing deleted field if required // Remove filters referencing deleted field if required
if (view.meta.filters && view.meta.filters.length) { if (view.meta.filters && view.meta.filters.length) {
const initialLength = view.meta.filters.length const initialLength = view.meta.filters.length
view.meta.filters = view.meta.filters.filter(filter => { view.meta.filters = view.meta.filters.filter((filter: any) => {
return filter.key !== column return filter.key !== column
}) })
if (initialLength !== view.meta.filters.length) { if (initialLength !== view.meta.filters.length) {
@ -369,16 +383,20 @@ exports.checkForViewUpdates = async (table, rename, deletedColumns) => {
} }
} }
exports.generateForeignKey = (column, relatedTable) => { export function generateForeignKey(column: any, relatedTable: any) {
return `fk_${relatedTable.name}_${column.fieldName}` return `fk_${relatedTable.name}_${column.fieldName}`
} }
exports.generateJunctionTableName = (column, table, relatedTable) => { export function generateJunctionTableName(
column: any,
table: any,
relatedTable: any
) {
return `jt_${table.name}_${relatedTable.name}_${column.name}_${column.fieldName}` return `jt_${table.name}_${relatedTable.name}_${column.name}_${column.fieldName}`
} }
exports.foreignKeyStructure = (keyName, meta = null) => { export function foreignKeyStructure(keyName: any, meta = null) {
const structure = { const structure: any = {
type: FieldTypes.NUMBER, type: FieldTypes.NUMBER,
constraints: {}, constraints: {},
name: keyName, name: keyName,
@ -389,7 +407,7 @@ exports.foreignKeyStructure = (keyName, meta = null) => {
return structure return structure
} }
exports.areSwitchableTypes = (type1, type2) => { export function areSwitchableTypes(type1: any, type2: any) {
if ( if (
SwitchableTypes.indexOf(type1) === -1 && SwitchableTypes.indexOf(type1) === -1 &&
SwitchableTypes.indexOf(type2) === -1 SwitchableTypes.indexOf(type2) === -1
@ -406,21 +424,24 @@ exports.areSwitchableTypes = (type1, type2) => {
return false return false
} }
exports.hasTypeChanged = (table, oldTable) => { export function hasTypeChanged(table: any, oldTable: any) {
if (!oldTable) { if (!oldTable) {
return false return false
} }
for (let [key, field] of Object.entries(oldTable.schema)) { let key: any
let field: any
for ([key, field] of Object.entries(oldTable.schema)) {
const oldType = field.type const oldType = field.type
if (!table.schema[key]) { if (!table.schema[key]) {
continue continue
} }
const newType = table.schema[key].type const newType = table.schema[key].type
if (oldType !== newType && !exports.areSwitchableTypes(oldType, newType)) { if (oldType !== newType && !areSwitchableTypes(oldType, newType)) {
return true return true
} }
} }
return false return false
} }
exports.TableSaveFunctions = TableSaveFunctions const _TableSaveFunctions = TableSaveFunctions
export { _TableSaveFunctions as TableSaveFunctions }

View File

@ -11,7 +11,7 @@ const zlib = require("zlib")
const { mainRoutes, staticRoutes } = require("./routes") const { mainRoutes, staticRoutes } = require("./routes")
const pkg = require("../../package.json") const pkg = require("../../package.json")
const env = require("../environment") const env = require("../environment")
const { middleware: licensing } = require("@budibase/pro") const Pro = require("@budibase/pro")
const router = new Router() const router = new Router()
@ -55,7 +55,7 @@ router
.use(currentApp) .use(currentApp)
// this middleware will try to use the app ID to determine the tenancy // this middleware will try to use the app ID to determine the tenancy
.use(buildAppTenancyMiddleware()) .use(buildAppTenancyMiddleware())
.use(licensing()) .use(Pro.Middleware.Licensing())
.use(auditLog) .use(auditLog)
// error handling middleware // error handling middleware

View File

@ -1,9 +1,9 @@
const rowController = require("../../api/controllers/row") import { save } from "../../api/controllers/row"
const automationUtils = require("../automationUtils") import { cleanUpRow, getError } from "../automationUtils"
const { quotas, StaticQuotaName, QuotaUsageType } = require("@budibase/pro") import * as Pro from "@budibase/pro"
const { buildCtx } = require("./utils") import { buildCtx } from "./utils"
exports.definition = { export const definition = {
name: "Create Row", name: "Create Row",
tagline: "Create a {{inputs.enriched.table.name}} row", tagline: "Create a {{inputs.enriched.table.name}} row",
icon: "TableRowAddBottom", icon: "TableRowAddBottom",
@ -59,7 +59,7 @@ exports.definition = {
}, },
} }
exports.run = async function ({ inputs, appId, emitter }) { export async function run({ inputs, appId, emitter }: any) {
if (inputs.row == null || inputs.row.tableId == null) { if (inputs.row == null || inputs.row.tableId == null) {
return { return {
success: false, success: false,
@ -69,7 +69,7 @@ exports.run = async function ({ inputs, appId, emitter }) {
} }
} }
// have to clean up the row, remove the table from it // have to clean up the row, remove the table from it
const ctx = buildCtx(appId, emitter, { const ctx: any = buildCtx(appId, emitter, {
body: inputs.row, body: inputs.row,
params: { params: {
tableId: inputs.row.tableId, tableId: inputs.row.tableId,
@ -77,15 +77,21 @@ exports.run = async function ({ inputs, appId, emitter }) {
}) })
try { try {
inputs.row = await automationUtils.cleanUpRow( inputs.row = await cleanUpRow(inputs.row.tableId, inputs.row)
inputs.row.tableId, await Pro.Licensing.Quotas.updateUsage(
inputs.row 1,
) Pro.StaticQuotaName.ROWS,
await quotas.updateUsage(1, StaticQuotaName.ROWS, QuotaUsageType.STATIC, { Pro.QuotaUsageType.STATIC,
{
dryRun: true, dryRun: true,
}) }
await rowController.save(ctx) )
await quotas.updateUsage(1, StaticQuotaName.ROWS, QuotaUsageType.STATIC) await save(ctx)
await Pro.Licensing.Quotas.updateUsage(
1,
Pro.StaticQuotaName.ROWS,
Pro.QuotaUsageType.STATIC
)
return { return {
row: inputs.row, row: inputs.row,
response: ctx.body, response: ctx.body,
@ -96,7 +102,7 @@ exports.run = async function ({ inputs, appId, emitter }) {
} catch (err) { } catch (err) {
return { return {
success: false, success: false,
response: automationUtils.getError(err), response: getError(err),
} }
} }
} }

View File

@ -1,9 +1,9 @@
const rowController = require("../../api/controllers/row") import { destroy } from "../../api/controllers/row"
const { quotas, StaticQuotaName, QuotaUsageType } = require("@budibase/pro") import * as Pro from "@budibase/pro"
const { buildCtx } = require("./utils") import { buildCtx } from "./utils"
const automationUtils = require("../automationUtils") import { getError } from "../automationUtils"
exports.definition = { export const definition = {
description: "Delete a row from your database", description: "Delete a row from your database",
icon: "TableRowRemoveCenter", icon: "TableRowRemoveCenter",
name: "Delete Row", name: "Delete Row",
@ -52,7 +52,7 @@ exports.definition = {
}, },
} }
exports.run = async function ({ inputs, appId, emitter }) { export async function run({ inputs, appId, emitter }: any) {
if (inputs.id == null || inputs.revision == null) { if (inputs.id == null || inputs.revision == null) {
return { return {
success: false, success: false,
@ -62,7 +62,7 @@ exports.run = async function ({ inputs, appId, emitter }) {
} }
} }
let ctx = buildCtx(appId, emitter, { let ctx: any = buildCtx(appId, emitter, {
body: { body: {
_id: inputs.id, _id: inputs.id,
_rev: inputs.revision, _rev: inputs.revision,
@ -73,8 +73,12 @@ exports.run = async function ({ inputs, appId, emitter }) {
}) })
try { try {
await quotas.updateUsage(-1, StaticQuotaName.ROWS, QuotaUsageType.STATIC) await Pro.Licensing.Quotas.updateUsage(
await rowController.destroy(ctx) -1,
Pro.StaticQuotaName.ROWS,
Pro.QuotaUsageType.STATIC
)
await destroy(ctx)
return { return {
response: ctx.body, response: ctx.body,
row: ctx.row, row: ctx.row,
@ -83,7 +87,7 @@ exports.run = async function ({ inputs, appId, emitter }) {
} catch (err) { } catch (err) {
return { return {
success: false, success: false,
response: automationUtils.getError(err), response: getError(err),
} }
} }
} }

View File

@ -1,4 +1,4 @@
import { quotas, StaticQuotaName, QuotaUsageType } from "@budibase/pro" import * as Pro from "@budibase/pro"
const { getUniqueRows } = require("../utilities/usageQuota/rows") const { getUniqueRows } = require("../utilities/usageQuota/rows")
const { const {
isExternalTable, isExternalTable,
@ -14,12 +14,12 @@ const METHOD_MAP: any = {
const DOMAIN_MAP: any = { const DOMAIN_MAP: any = {
rows: { rows: {
name: StaticQuotaName.ROWS, name: Pro.StaticQuotaName.ROWS,
type: QuotaUsageType.STATIC, type: Pro.QuotaUsageType.STATIC,
}, },
applications: { applications: {
name: StaticQuotaName.APPS, name: Pro.StaticQuotaName.APPS,
type: QuotaUsageType.STATIC, type: Pro.QuotaUsageType.STATIC,
}, },
} }
@ -32,7 +32,7 @@ function getQuotaInfo(url: string) {
} }
module.exports = async (ctx: any, next: any) => { module.exports = async (ctx: any, next: any) => {
if (!quotas.useQuotas()) { if (!Pro.Licensing.Quotas.useQuotas()) {
return next() return next()
} }
@ -79,7 +79,7 @@ const performRequest = async (
const usageContext = { const usageContext = {
skipNext: false, skipNext: false,
skipUsage: false, skipUsage: false,
[StaticQuotaName.APPS]: {}, [Pro.StaticQuotaName.APPS]: {},
} }
const quotaName = quotaInfo.name const quotaName = quotaInfo.name
@ -96,7 +96,9 @@ const performRequest = async (
// run the request // run the request
if (!usageContext.skipNext) { if (!usageContext.skipNext) {
await quotas.updateUsage(usage, quotaName, quotaInfo.type, { dryRun: true }) await Pro.Licensing.Quotas.updateUsage(usage, quotaName, quotaInfo.type, {
dryRun: true,
})
await next() await next()
} }
@ -112,7 +114,7 @@ const performRequest = async (
// update the usage // update the usage
if (!usageContext.skipUsage) { if (!usageContext.skipUsage) {
await quotas.updateUsage(usage, quotaName, quotaInfo.type) await Pro.Licensing.Quotas.updateUsage(usage, quotaName, quotaInfo.type)
} }
} }
@ -126,18 +128,18 @@ const appPreDelete = async (ctx: any, usageContext: any) => {
// store the row count to delete // store the row count to delete
const rows = await getUniqueRows([ctx.appId]) const rows = await getUniqueRows([ctx.appId])
if (rows.length) { if (rows.length) {
usageContext[StaticQuotaName.APPS] = { rowCount: rows.length } usageContext[Pro.StaticQuotaName.APPS] = { rowCount: rows.length }
} }
} }
const appPostDelete = async (ctx: any, usageContext: any) => { const appPostDelete = async (ctx: any, usageContext: any) => {
// delete the app rows from usage // delete the app rows from usage
const rowCount = usageContext[StaticQuotaName.ROWS].rowCount const rowCount = usageContext[Pro.StaticQuotaName.ROWS].rowCount
if (rowCount) { if (rowCount) {
await quotas.updateUsage( await Pro.Licensing.Quotas.updateUsage(
-rowCount, -rowCount,
StaticQuotaName.ROWS, Pro.StaticQuotaName.ROWS,
QuotaUsageType.STATIC Pro.QuotaUsageType.STATIC
) )
} }
} }
@ -147,24 +149,24 @@ const appPostCreate = async (ctx: any) => {
if (ctx.request.body.useTemplate === "true") { if (ctx.request.body.useTemplate === "true") {
const rows = await getUniqueRows([ctx.response.body.appId]) const rows = await getUniqueRows([ctx.response.body.appId])
const rowCount = rows ? rows.length : 0 const rowCount = rows ? rows.length : 0
await quotas.updateUsage( await Pro.Licensing.Quotas.updateUsage(
rowCount, rowCount,
StaticQuotaName.ROWS, Pro.StaticQuotaName.ROWS,
QuotaUsageType.STATIC Pro.QuotaUsageType.STATIC
) )
} }
} }
const PRE_DELETE: any = { const PRE_DELETE: any = {
[StaticQuotaName.APPS]: appPreDelete, [Pro.StaticQuotaName.APPS]: appPreDelete,
} }
const POST_DELETE: any = { const POST_DELETE: any = {
[StaticQuotaName.APPS]: appPostDelete, [Pro.StaticQuotaName.APPS]: appPostDelete,
} }
const PRE_CREATE: any = {} const PRE_CREATE: any = {}
const POST_CREATE: any = { const POST_CREATE: any = {
[StaticQuotaName.APPS]: appPostCreate, [Pro.StaticQuotaName.APPS]: appPostCreate,
} }

View File

@ -1,7 +1,7 @@
import { quotas } from "@budibase/pro" import * as Pro from "@budibase/pro"
export const runQuotaMigration = async (migration: Function) => { export const runQuotaMigration = async (migration: Function) => {
if (!quotas.useQuotas()) { if (!Pro.Licensing.Quotas.useQuotas()) {
return return
} }
await migration() await migration()

View File

@ -1,6 +1,6 @@
import { getTenantId } from "@budibase/backend-core/tenancy" import { getTenantId } from "@budibase/backend-core/tenancy"
import { getAllApps } from "@budibase/backend-core/db" import { getAllApps } from "@budibase/backend-core/db"
import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro" import * as Pro from "@budibase/pro"
export const run = async () => { export const run = async () => {
// get app count // get app count
@ -11,5 +11,9 @@ export const run = async () => {
// sync app count // sync app count
const tenantId = getTenantId() const tenantId = getTenantId()
console.log(`[Tenant: ${tenantId}] Syncing app count: ${appCount}`) console.log(`[Tenant: ${tenantId}] Syncing app count: ${appCount}`)
await quotas.setUsage(appCount, StaticQuotaName.APPS, QuotaUsageType.STATIC) await Pro.Licensing.Quotas.setUsage(
appCount,
Pro.StaticQuotaName.APPS,
Pro.QuotaUsageType.STATIC
)
} }

View File

@ -1,6 +1,6 @@
import { getTenantId } from "@budibase/backend-core/tenancy" import { getTenantId } from "@budibase/backend-core/tenancy"
import { getAllApps } from "@budibase/backend-core/db" import { getAllApps } from "@budibase/backend-core/db"
import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro" import * as Pro from "@budibase/pro"
import { getUniqueRows } from "../../../utilities/usageQuota/rows" import { getUniqueRows } from "../../../utilities/usageQuota/rows"
export const run = async () => { export const run = async () => {
@ -15,5 +15,9 @@ export const run = async () => {
// 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 Pro.Licensing.Quotas.setUsage(
rowCount,
Pro.StaticQuotaName.ROWS,
Pro.QuotaUsageType.STATIC
)
} }

View File

@ -2438,6 +2438,11 @@
"@types/koa-compose" "*" "@types/koa-compose" "*"
"@types/node" "*" "@types/node" "*"
"@types/lodash@^4.14.179":
version "4.14.179"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.179.tgz#490ec3288088c91295780237d2497a3aa9dfb5c5"
integrity sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w==
"@types/mime@^1": "@types/mime@^1":
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"

View File

@ -1,5 +1,5 @@
{ {
"watch": ["src", "../backend-core"], "watch": ["src", "../backend-core", "../../../budibase-pro/packages/pro"],
"ext": "js,ts,json", "ext": "js,ts,json",
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"], "ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"],
"exec": "ts-node src/index.ts" "exec": "ts-node src/index.ts"

View File

@ -66,6 +66,7 @@
"@types/jest": "^26.0.23", "@types/jest": "^26.0.23",
"@types/koa": "^2.13.3", "@types/koa": "^2.13.3",
"@types/koa-router": "^7.4.2", "@types/koa-router": "^7.4.2",
"@types/koa__router": "^8.0.11",
"@types/node": "^15.12.4", "@types/node": "^15.12.4",
"@typescript-eslint/parser": "5.12.0", "@typescript-eslint/parser": "5.12.0",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",

View File

@ -0,0 +1,25 @@
import * as Pro from "@budibase/pro"
export const activate = async (ctx: any) => {
const { licenseKey } = ctx.request.body
if (!licenseKey) {
ctx.throw(400, "licenseKey is required")
}
await Pro.Licensing.activateLicenseKey(licenseKey)
ctx.status = 200
}
export const refresh = async (ctx: any) => {
await Pro.Licensing.Cache.refresh()
ctx.status = 200
}
export const getInfo = async (ctx: any) => {
const licenseInfo = await Pro.Licensing.getLicenseInfo()
if (licenseInfo) {
licenseInfo.licenseKey = "*"
ctx.body = licenseInfo
}
ctx.status = 200
}

View File

@ -8,7 +8,7 @@ const {
buildTenancyMiddleware, buildTenancyMiddleware,
buildCsrfMiddleware, buildCsrfMiddleware,
} = require("@budibase/backend-core/auth") } = require("@budibase/backend-core/auth")
const { middleware: licensing } = require("@budibase/pro") const Pro = require("@budibase/pro")
const { errors } = require("@budibase/backend-core") const { errors } = require("@budibase/backend-core")
const PUBLIC_ENDPOINTS = [ const PUBLIC_ENDPOINTS = [
@ -93,7 +93,7 @@ router
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS)) .use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
.use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS)) .use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
.use(buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS })) .use(buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS }))
.use(licensing()) .use(Pro.Middleware.Licensing())
// for now no public access is allowed to worker (bar health check) // for now no public access is allowed to worker (bar health check)
.use((ctx, next) => { .use((ctx, next) => {
if (ctx.publicEndpoint) { if (ctx.publicEndpoint) {

View File

@ -0,0 +1,11 @@
import Router from "@koa/router"
import * as controller from "../../controllers/global/license"
const router = new Router()
router
.post("/api/global/license/activate", controller.activate)
.post("/api/global/license/refresh", controller.refresh)
.get("/api/global/license/info", controller.getInfo)
export = router

View File

@ -8,6 +8,7 @@ const roleRoutes = require("./global/roles")
const sessionRoutes = require("./global/sessions") const sessionRoutes = require("./global/sessions")
const environmentRoutes = require("./system/environment") const environmentRoutes = require("./system/environment")
const tenantsRoutes = require("./system/tenants") const tenantsRoutes = require("./system/tenants")
const licenseRoutes = require("./global/license")
exports.routes = [ exports.routes = [
configRoutes, configRoutes,
@ -20,4 +21,5 @@ exports.routes = [
sessionRoutes, sessionRoutes,
roleRoutes, roleRoutes,
environmentRoutes, environmentRoutes,
licenseRoutes,
] ]

File diff suppressed because it is too large Load Diff