Merge branch 'master' into feature/form-block-description
This commit is contained in:
commit
b88be9b1df
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.11.42",
|
||||
"version": "2.11.43",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -1,37 +1,50 @@
|
|||
import env from "../../environment"
|
||||
import * as objectStore from "../objectStore"
|
||||
import * as cloudfront from "../cloudfront"
|
||||
import qs from "querystring"
|
||||
import { DEFAULT_TENANT_ID, getTenantId } from "../../context"
|
||||
|
||||
export function clientLibraryPath(appId: string) {
|
||||
return `${objectStore.sanitizeKey(appId)}/budibase-client.js`
|
||||
}
|
||||
|
||||
/**
|
||||
* In production the client library is stored in the object store, however in development
|
||||
* we use the symlinked version produced by lerna, located in node modules. We link to this
|
||||
* via a specific endpoint (under /api/assets/client).
|
||||
* @param appId In production we need the appId to look up the correct bucket, as the
|
||||
* version of the client lib may differ between apps.
|
||||
* @param version The version to retrieve.
|
||||
* @return The URL to be inserted into appPackage response or server rendered
|
||||
* app index file.
|
||||
* Previously we used to serve the client library directly from Cloudfront, however
|
||||
* due to issues with the domain we were unable to continue doing this - keeping
|
||||
* incase we are able to switch back to CDN path again in future.
|
||||
*/
|
||||
export const clientLibraryUrl = (appId: string, version: string) => {
|
||||
if (env.isProd()) {
|
||||
let file = `${objectStore.sanitizeKey(appId)}/budibase-client.js`
|
||||
if (env.CLOUDFRONT_CDN) {
|
||||
// append app version to bust the cache
|
||||
if (version) {
|
||||
file += `?v=${version}`
|
||||
}
|
||||
// don't need to use presigned for client with cloudfront
|
||||
// file is public
|
||||
return cloudfront.getUrl(file)
|
||||
} else {
|
||||
return objectStore.getPresignedUrl(env.APPS_BUCKET_NAME, file)
|
||||
export function clientLibraryCDNUrl(appId: string, version: string) {
|
||||
let file = clientLibraryPath(appId)
|
||||
if (env.CLOUDFRONT_CDN) {
|
||||
// append app version to bust the cache
|
||||
if (version) {
|
||||
file += `?v=${version}`
|
||||
}
|
||||
// don't need to use presigned for client with cloudfront
|
||||
// file is public
|
||||
return cloudfront.getUrl(file)
|
||||
} else {
|
||||
return `/api/assets/client`
|
||||
return objectStore.getPresignedUrl(env.APPS_BUCKET_NAME, file)
|
||||
}
|
||||
}
|
||||
|
||||
export const getAppFileUrl = (s3Key: string) => {
|
||||
export function clientLibraryUrl(appId: string, version: string) {
|
||||
let tenantId, qsParams: { appId: string; version: string; tenantId?: string }
|
||||
try {
|
||||
tenantId = getTenantId()
|
||||
} finally {
|
||||
qsParams = {
|
||||
appId,
|
||||
version,
|
||||
}
|
||||
}
|
||||
if (tenantId && tenantId !== DEFAULT_TENANT_ID) {
|
||||
qsParams.tenantId = tenantId
|
||||
}
|
||||
return `/api/assets/client?${qs.encode(qsParams)}`
|
||||
}
|
||||
|
||||
export function getAppFileUrl(s3Key: string) {
|
||||
if (env.CLOUDFRONT_CDN) {
|
||||
return cloudfront.getPresignedUrl(s3Key)
|
||||
} else {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Plugin } from "@budibase/types"
|
|||
|
||||
// URLS
|
||||
|
||||
export const enrichPluginURLs = (plugins: Plugin[]) => {
|
||||
export function enrichPluginURLs(plugins: Plugin[]) {
|
||||
if (!plugins || !plugins.length) {
|
||||
return []
|
||||
}
|
||||
|
@ -17,12 +17,12 @@ export const enrichPluginURLs = (plugins: Plugin[]) => {
|
|||
})
|
||||
}
|
||||
|
||||
const getPluginJSUrl = (plugin: Plugin) => {
|
||||
function getPluginJSUrl(plugin: Plugin) {
|
||||
const s3Key = getPluginJSKey(plugin)
|
||||
return getPluginUrl(s3Key)
|
||||
}
|
||||
|
||||
const getPluginIconUrl = (plugin: Plugin): string | undefined => {
|
||||
function getPluginIconUrl(plugin: Plugin): string | undefined {
|
||||
const s3Key = getPluginIconKey(plugin)
|
||||
if (!s3Key) {
|
||||
return
|
||||
|
@ -30,7 +30,7 @@ const getPluginIconUrl = (plugin: Plugin): string | undefined => {
|
|||
return getPluginUrl(s3Key)
|
||||
}
|
||||
|
||||
const getPluginUrl = (s3Key: string) => {
|
||||
function getPluginUrl(s3Key: string) {
|
||||
if (env.CLOUDFRONT_CDN) {
|
||||
return cloudfront.getPresignedUrl(s3Key)
|
||||
} else {
|
||||
|
@ -40,11 +40,11 @@ const getPluginUrl = (s3Key: string) => {
|
|||
|
||||
// S3 KEYS
|
||||
|
||||
export const getPluginJSKey = (plugin: Plugin) => {
|
||||
export function getPluginJSKey(plugin: Plugin) {
|
||||
return getPluginS3Key(plugin, "plugin.min.js")
|
||||
}
|
||||
|
||||
export const getPluginIconKey = (plugin: Plugin) => {
|
||||
export function getPluginIconKey(plugin: Plugin) {
|
||||
// stored iconUrl is deprecated - hardcode to icon.svg in this case
|
||||
const iconFileName = plugin.iconUrl ? "icon.svg" : plugin.iconFileName
|
||||
if (!iconFileName) {
|
||||
|
@ -53,12 +53,12 @@ export const getPluginIconKey = (plugin: Plugin) => {
|
|||
return getPluginS3Key(plugin, iconFileName)
|
||||
}
|
||||
|
||||
const getPluginS3Key = (plugin: Plugin, fileName: string) => {
|
||||
function getPluginS3Key(plugin: Plugin, fileName: string) {
|
||||
const s3Key = getPluginS3Dir(plugin.name)
|
||||
return `${s3Key}/${fileName}`
|
||||
}
|
||||
|
||||
export const getPluginS3Dir = (pluginName: string) => {
|
||||
export function getPluginS3Dir(pluginName: string) {
|
||||
let s3Key = `${pluginName}`
|
||||
if (env.MULTI_TENANCY) {
|
||||
const tenantId = context.getTenantId()
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as app from "../app"
|
||||
import { getAppFileUrl } from "../app"
|
||||
import { testEnv } from "../../../../tests/extra"
|
||||
|
||||
describe("app", () => {
|
||||
|
@ -7,6 +6,15 @@ describe("app", () => {
|
|||
testEnv.nodeJest()
|
||||
})
|
||||
|
||||
function baseCheck(url: string, tenantId?: string) {
|
||||
expect(url).toContain("/api/assets/client")
|
||||
if (tenantId) {
|
||||
expect(url).toContain(`tenantId=${tenantId}`)
|
||||
}
|
||||
expect(url).toContain("appId=app_123")
|
||||
expect(url).toContain("version=2.0.0")
|
||||
}
|
||||
|
||||
describe("clientLibraryUrl", () => {
|
||||
function getClientUrl() {
|
||||
return app.clientLibraryUrl("app_123/budibase-client.js", "2.0.0")
|
||||
|
@ -20,31 +28,19 @@ describe("app", () => {
|
|||
it("gets url in dev", () => {
|
||||
testEnv.nodeDev()
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe("/api/assets/client")
|
||||
})
|
||||
|
||||
it("gets url with embedded minio", () => {
|
||||
testEnv.withMinio()
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe(
|
||||
"/files/signed/prod-budi-app-assets/app_123/budibase-client.js/budibase-client.js"
|
||||
)
|
||||
baseCheck(url)
|
||||
})
|
||||
|
||||
it("gets url with custom S3", () => {
|
||||
testEnv.withS3()
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe(
|
||||
"http://s3.example.com/prod-budi-app-assets/app_123/budibase-client.js/budibase-client.js"
|
||||
)
|
||||
baseCheck(url)
|
||||
})
|
||||
|
||||
it("gets url with cloudfront + s3", () => {
|
||||
testEnv.withCloudfront()
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe(
|
||||
"http://cf.example.com/app_123/budibase-client.js/budibase-client.js?v=2.0.0"
|
||||
)
|
||||
baseCheck(url)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -57,7 +53,7 @@ describe("app", () => {
|
|||
testEnv.nodeDev()
|
||||
await testEnv.withTenant(tenantId => {
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe("/api/assets/client")
|
||||
baseCheck(url, tenantId)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -65,9 +61,7 @@ describe("app", () => {
|
|||
await testEnv.withTenant(tenantId => {
|
||||
testEnv.withMinio()
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe(
|
||||
"/files/signed/prod-budi-app-assets/app_123/budibase-client.js/budibase-client.js"
|
||||
)
|
||||
baseCheck(url, tenantId)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -75,9 +69,7 @@ describe("app", () => {
|
|||
await testEnv.withTenant(tenantId => {
|
||||
testEnv.withS3()
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe(
|
||||
"http://s3.example.com/prod-budi-app-assets/app_123/budibase-client.js/budibase-client.js"
|
||||
)
|
||||
baseCheck(url, tenantId)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -85,9 +77,7 @@ describe("app", () => {
|
|||
await testEnv.withTenant(tenantId => {
|
||||
testEnv.withCloudfront()
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe(
|
||||
"http://cf.example.com/app_123/budibase-client.js/budibase-client.js?v=2.0.0"
|
||||
)
|
||||
baseCheck(url, tenantId)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const sanitize = require("sanitize-s3-objectkey")
|
||||
import AWS from "aws-sdk"
|
||||
import stream from "stream"
|
||||
import stream, { Readable } from "stream"
|
||||
import fetch from "node-fetch"
|
||||
import tar from "tar-fs"
|
||||
import zlib from "zlib"
|
||||
|
@ -66,10 +66,10 @@ export function sanitizeBucket(input: string) {
|
|||
* @return an S3 object store object, check S3 Nodejs SDK for usage.
|
||||
* @constructor
|
||||
*/
|
||||
export const ObjectStore = (
|
||||
export function ObjectStore(
|
||||
bucket: string,
|
||||
opts: { presigning: boolean } = { presigning: false }
|
||||
) => {
|
||||
) {
|
||||
const config: any = {
|
||||
s3ForcePathStyle: true,
|
||||
signatureVersion: "v4",
|
||||
|
@ -104,7 +104,7 @@ export const ObjectStore = (
|
|||
* Given an object store and a bucket name this will make sure the bucket exists,
|
||||
* if it does not exist then it will create it.
|
||||
*/
|
||||
export const makeSureBucketExists = async (client: any, bucketName: string) => {
|
||||
export async function makeSureBucketExists(client: any, bucketName: string) {
|
||||
bucketName = sanitizeBucket(bucketName)
|
||||
try {
|
||||
await client
|
||||
|
@ -139,13 +139,13 @@ export const makeSureBucketExists = async (client: any, bucketName: string) => {
|
|||
* Uploads the contents of a file given the required parameters, useful when
|
||||
* temp files in use (for example file uploaded as an attachment).
|
||||
*/
|
||||
export const upload = async ({
|
||||
export async function upload({
|
||||
bucket: bucketName,
|
||||
filename,
|
||||
path,
|
||||
type,
|
||||
metadata,
|
||||
}: UploadParams) => {
|
||||
}: UploadParams) {
|
||||
const extension = filename.split(".").pop()
|
||||
const fileBytes = fs.readFileSync(path)
|
||||
|
||||
|
@ -180,12 +180,12 @@ export const upload = async ({
|
|||
* Similar to the upload function but can be used to send a file stream
|
||||
* through to the object store.
|
||||
*/
|
||||
export const streamUpload = async (
|
||||
export async function streamUpload(
|
||||
bucketName: string,
|
||||
filename: string,
|
||||
stream: any,
|
||||
extra = {}
|
||||
) => {
|
||||
) {
|
||||
const objectStore = ObjectStore(bucketName)
|
||||
await makeSureBucketExists(objectStore, bucketName)
|
||||
|
||||
|
@ -215,7 +215,7 @@ export const streamUpload = async (
|
|||
* retrieves the contents of a file from the object store, if it is a known content type it
|
||||
* will be converted, otherwise it will be returned as a buffer stream.
|
||||
*/
|
||||
export const retrieve = async (bucketName: string, filepath: string) => {
|
||||
export async function retrieve(bucketName: string, filepath: string) {
|
||||
const objectStore = ObjectStore(bucketName)
|
||||
const params = {
|
||||
Bucket: sanitizeBucket(bucketName),
|
||||
|
@ -230,7 +230,7 @@ export const retrieve = async (bucketName: string, filepath: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const listAllObjects = async (bucketName: string, path: string) => {
|
||||
export async function listAllObjects(bucketName: string, path: string) {
|
||||
const objectStore = ObjectStore(bucketName)
|
||||
const list = (params: ListParams = {}) => {
|
||||
return objectStore
|
||||
|
@ -261,11 +261,11 @@ export const listAllObjects = async (bucketName: string, path: string) => {
|
|||
/**
|
||||
* Generate a presigned url with a default TTL of 1 hour
|
||||
*/
|
||||
export const getPresignedUrl = (
|
||||
export function getPresignedUrl(
|
||||
bucketName: string,
|
||||
key: string,
|
||||
durationSeconds: number = 3600
|
||||
) => {
|
||||
) {
|
||||
const objectStore = ObjectStore(bucketName, { presigning: true })
|
||||
const params = {
|
||||
Bucket: sanitizeBucket(bucketName),
|
||||
|
@ -291,7 +291,7 @@ export const getPresignedUrl = (
|
|||
/**
|
||||
* Same as retrieval function but puts to a temporary file.
|
||||
*/
|
||||
export const retrieveToTmp = async (bucketName: string, filepath: string) => {
|
||||
export async function retrieveToTmp(bucketName: string, filepath: string) {
|
||||
bucketName = sanitizeBucket(bucketName)
|
||||
filepath = sanitizeKey(filepath)
|
||||
const data = await retrieve(bucketName, filepath)
|
||||
|
@ -300,7 +300,7 @@ export const retrieveToTmp = async (bucketName: string, filepath: string) => {
|
|||
return outputPath
|
||||
}
|
||||
|
||||
export const retrieveDirectory = async (bucketName: string, path: string) => {
|
||||
export async function retrieveDirectory(bucketName: string, path: string) {
|
||||
let writePath = join(budibaseTempDir(), v4())
|
||||
fs.mkdirSync(writePath)
|
||||
const objects = await listAllObjects(bucketName, path)
|
||||
|
@ -324,7 +324,7 @@ export const retrieveDirectory = async (bucketName: string, path: string) => {
|
|||
/**
|
||||
* Delete a single file.
|
||||
*/
|
||||
export const deleteFile = async (bucketName: string, filepath: string) => {
|
||||
export async function deleteFile(bucketName: string, filepath: string) {
|
||||
const objectStore = ObjectStore(bucketName)
|
||||
await makeSureBucketExists(objectStore, bucketName)
|
||||
const params = {
|
||||
|
@ -334,7 +334,7 @@ export const deleteFile = async (bucketName: string, filepath: string) => {
|
|||
return objectStore.deleteObject(params).promise()
|
||||
}
|
||||
|
||||
export const deleteFiles = async (bucketName: string, filepaths: string[]) => {
|
||||
export async function deleteFiles(bucketName: string, filepaths: string[]) {
|
||||
const objectStore = ObjectStore(bucketName)
|
||||
await makeSureBucketExists(objectStore, bucketName)
|
||||
const params = {
|
||||
|
@ -349,10 +349,10 @@ export const deleteFiles = async (bucketName: string, filepaths: string[]) => {
|
|||
/**
|
||||
* Delete a path, including everything within.
|
||||
*/
|
||||
export const deleteFolder = async (
|
||||
export async function deleteFolder(
|
||||
bucketName: string,
|
||||
folder: string
|
||||
): Promise<any> => {
|
||||
): Promise<any> {
|
||||
bucketName = sanitizeBucket(bucketName)
|
||||
folder = sanitizeKey(folder)
|
||||
const client = ObjectStore(bucketName)
|
||||
|
@ -383,11 +383,11 @@ export const deleteFolder = async (
|
|||
}
|
||||
}
|
||||
|
||||
export const uploadDirectory = async (
|
||||
export async function uploadDirectory(
|
||||
bucketName: string,
|
||||
localPath: string,
|
||||
bucketPath: string
|
||||
) => {
|
||||
) {
|
||||
bucketName = sanitizeBucket(bucketName)
|
||||
let uploads = []
|
||||
const files = fs.readdirSync(localPath, { withFileTypes: true })
|
||||
|
@ -404,11 +404,11 @@ export const uploadDirectory = async (
|
|||
return files
|
||||
}
|
||||
|
||||
export const downloadTarballDirect = async (
|
||||
export async function downloadTarballDirect(
|
||||
url: string,
|
||||
path: string,
|
||||
headers = {}
|
||||
) => {
|
||||
) {
|
||||
path = sanitizeKey(path)
|
||||
const response = await fetch(url, { headers })
|
||||
if (!response.ok) {
|
||||
|
@ -418,11 +418,11 @@ export const downloadTarballDirect = async (
|
|||
await streamPipeline(response.body, zlib.createUnzip(), tar.extract(path))
|
||||
}
|
||||
|
||||
export const downloadTarball = async (
|
||||
export async function downloadTarball(
|
||||
url: string,
|
||||
bucketName: string,
|
||||
path: string
|
||||
) => {
|
||||
) {
|
||||
bucketName = sanitizeBucket(bucketName)
|
||||
path = sanitizeKey(path)
|
||||
const response = await fetch(url)
|
||||
|
@ -438,3 +438,17 @@ export const downloadTarball = async (
|
|||
// return the temporary path incase there is a use for it
|
||||
return tmpPath
|
||||
}
|
||||
|
||||
export async function getReadStream(
|
||||
bucketName: string,
|
||||
path: string
|
||||
): Promise<Readable> {
|
||||
bucketName = sanitizeBucket(bucketName)
|
||||
path = sanitizeKey(path)
|
||||
const client = ObjectStore(bucketName)
|
||||
const params = {
|
||||
Bucket: bucketName,
|
||||
Key: path,
|
||||
}
|
||||
return client.getObject(params).createReadStream()
|
||||
}
|
||||
|
|
|
@ -21,8 +21,9 @@ import {
|
|||
User,
|
||||
DatabaseQueryOpts,
|
||||
} from "@budibase/types"
|
||||
import * as context from "../context"
|
||||
import { getGlobalDB } from "../context"
|
||||
import * as context from "../context"
|
||||
import { isCreator } from "./utils"
|
||||
|
||||
type GetOpts = { cleanup?: boolean }
|
||||
|
||||
|
@ -286,6 +287,19 @@ export async function getUserCount() {
|
|||
return response.total_rows
|
||||
}
|
||||
|
||||
export async function getCreatorCount() {
|
||||
let creators = 0
|
||||
async function iterate(startPage?: string) {
|
||||
const page = await paginatedUsers({ bookmark: startPage })
|
||||
creators += page.data.filter(isCreator).length
|
||||
if (page.hasNextPage) {
|
||||
await iterate(page.nextPage)
|
||||
}
|
||||
}
|
||||
await iterate()
|
||||
return creators
|
||||
}
|
||||
|
||||
// used to remove the builder/admin permissions, for processing the
|
||||
// user as an app user (they may have some specific role/group
|
||||
export function removePortalUserPermissions(user: User | ContextUser) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { getAccountByTenantId } from "../accounts"
|
|||
// extract from shared-core to make easily accessible from backend-core
|
||||
export const isBuilder = sdk.users.isBuilder
|
||||
export const isAdmin = sdk.users.isAdmin
|
||||
export const isCreator = sdk.users.isCreator
|
||||
export const isGlobalBuilder = sdk.users.isGlobalBuilder
|
||||
export const isAdminOrBuilder = sdk.users.isAdminOrBuilder
|
||||
export const hasAdminPermissions = sdk.users.hasAdminPermissions
|
||||
|
|
|
@ -72,6 +72,11 @@ export function quotas(): Quotas {
|
|||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
creators: {
|
||||
name: "Creators",
|
||||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
userGroups: {
|
||||
name: "User Groups",
|
||||
value: 1,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { MonthlyQuotaName, QuotaUsage } from "@budibase/types"
|
||||
|
||||
export const usage = (): QuotaUsage => {
|
||||
export const usage = (users: number = 0, creators: number = 0): QuotaUsage => {
|
||||
return {
|
||||
_id: "usage_quota",
|
||||
quotaReset: new Date().toISOString(),
|
||||
|
@ -58,7 +58,8 @@ export const usage = (): QuotaUsage => {
|
|||
usageQuota: {
|
||||
apps: 0,
|
||||
plugins: 0,
|
||||
users: 0,
|
||||
users,
|
||||
creators,
|
||||
userGroups: 0,
|
||||
rows: 0,
|
||||
triggers: {},
|
||||
|
|
|
@ -106,6 +106,13 @@
|
|||
name: fieldName,
|
||||
}
|
||||
}
|
||||
|
||||
// Delete numeric only widths as these are grid widths and should be
|
||||
// ignored
|
||||
const width = fixedSchema[fieldName].width
|
||||
if (width != null && `${width}`.trim().match(/^[0-9]+$/)) {
|
||||
delete fixedSchema[fieldName].width
|
||||
}
|
||||
})
|
||||
return fixedSchema
|
||||
}
|
||||
|
|
|
@ -16,7 +16,11 @@
|
|||
<DrawerContent>
|
||||
<div class="container">
|
||||
<Layout noPadding gap="S">
|
||||
<Input bind:value={column.width} label="Width" placeholder="Auto" />
|
||||
<Input
|
||||
bind:value={column.width}
|
||||
label="Width (must include a unit like px or %)"
|
||||
placeholder="Auto"
|
||||
/>
|
||||
<Select
|
||||
label="Alignment"
|
||||
bind:value={column.align}
|
||||
|
|
|
@ -81,6 +81,7 @@
|
|||
sortOrder: $fetch.sortOrder,
|
||||
},
|
||||
limit,
|
||||
primaryDisplay: $fetch.definition?.primaryDisplay,
|
||||
}
|
||||
|
||||
const createFetch = datasource => {
|
||||
|
|
|
@ -32,7 +32,8 @@
|
|||
$: loading = dataProvider?.loading ?? false
|
||||
$: data = dataProvider?.rows || []
|
||||
$: fullSchema = dataProvider?.schema ?? {}
|
||||
$: fields = getFields(fullSchema, columns, false)
|
||||
$: primaryDisplay = dataProvider?.primaryDisplay
|
||||
$: fields = getFields(fullSchema, columns, false, primaryDisplay)
|
||||
$: schema = getFilteredSchema(fullSchema, fields, hasChildren)
|
||||
$: setSorting = getAction(
|
||||
dataProvider?.id,
|
||||
|
@ -55,18 +56,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
const getFields = (schema, customColumns, showAutoColumns) => {
|
||||
// Check for an invalid column selection
|
||||
let invalid = false
|
||||
customColumns?.forEach(column => {
|
||||
const columnName = typeof column === "string" ? column : column.name
|
||||
if (schema[columnName] == null) {
|
||||
invalid = true
|
||||
}
|
||||
})
|
||||
|
||||
// Use column selection if it exists
|
||||
if (!invalid && customColumns?.length) {
|
||||
const getFields = (
|
||||
schema,
|
||||
customColumns,
|
||||
showAutoColumns,
|
||||
primaryDisplay
|
||||
) => {
|
||||
if (customColumns?.length) {
|
||||
return customColumns
|
||||
}
|
||||
|
||||
|
@ -74,13 +70,38 @@
|
|||
let columns = []
|
||||
let autoColumns = []
|
||||
Object.entries(schema).forEach(([field, fieldSchema]) => {
|
||||
if (fieldSchema.visible === false) {
|
||||
return
|
||||
}
|
||||
if (!fieldSchema?.autocolumn) {
|
||||
columns.push(field)
|
||||
} else if (showAutoColumns) {
|
||||
autoColumns.push(field)
|
||||
}
|
||||
})
|
||||
return columns.concat(autoColumns)
|
||||
|
||||
// Sort columns to respect grid metadata
|
||||
const allCols = columns.concat(autoColumns)
|
||||
return allCols.sort((a, b) => {
|
||||
if (a === primaryDisplay) {
|
||||
return -1
|
||||
}
|
||||
if (b === primaryDisplay) {
|
||||
return 1
|
||||
}
|
||||
const aOrder = schema[a].order
|
||||
const bOrder = schema[b].order
|
||||
if (aOrder === bOrder) {
|
||||
return 0
|
||||
}
|
||||
if (aOrder == null) {
|
||||
return 1
|
||||
}
|
||||
if (bOrder == null) {
|
||||
return -1
|
||||
}
|
||||
return aOrder < bOrder ? -1 : 1
|
||||
})
|
||||
}
|
||||
|
||||
const getFilteredSchema = (schema, fields, hasChildren) => {
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit f7e7cffe422086d9449c2075a74a378c16caff9d
|
||||
Subproject commit d24c0dc3a30014cbe61860252aa48104cad36376
|
|
@ -16,7 +16,7 @@ import AWS from "aws-sdk"
|
|||
import fs from "fs"
|
||||
import sdk from "../../../sdk"
|
||||
import * as pro from "@budibase/pro"
|
||||
import { App } from "@budibase/types"
|
||||
import { App, Ctx } from "@budibase/types"
|
||||
|
||||
const send = require("koa-send")
|
||||
|
||||
|
@ -39,7 +39,7 @@ async function prepareUpload({ s3Key, bucket, metadata, file }: any) {
|
|||
}
|
||||
}
|
||||
|
||||
export const toggleBetaUiFeature = async function (ctx: any) {
|
||||
export const toggleBetaUiFeature = async function (ctx: Ctx) {
|
||||
const cookieName = `beta:${ctx.params.feature}`
|
||||
|
||||
if (ctx.cookies.get(cookieName)) {
|
||||
|
@ -67,16 +67,14 @@ export const toggleBetaUiFeature = async function (ctx: any) {
|
|||
}
|
||||
}
|
||||
|
||||
export const serveBuilder = async function (ctx: any) {
|
||||
export const serveBuilder = async function (ctx: Ctx) {
|
||||
const builderPath = join(TOP_LEVEL_PATH, "builder")
|
||||
await send(ctx, ctx.file, { root: builderPath })
|
||||
}
|
||||
|
||||
export const uploadFile = async function (ctx: any) {
|
||||
let files =
|
||||
ctx.request.files.file.length > 1
|
||||
? Array.from(ctx.request.files.file)
|
||||
: [ctx.request.files.file]
|
||||
export const uploadFile = async function (ctx: Ctx) {
|
||||
const file = ctx.request?.files?.file
|
||||
let files = file && Array.isArray(file) ? Array.from(file) : [file]
|
||||
|
||||
const uploads = files.map(async (file: any) => {
|
||||
const fileExtension = [...file.name.split(".")].pop()
|
||||
|
@ -93,14 +91,14 @@ export const uploadFile = async function (ctx: any) {
|
|||
ctx.body = await Promise.all(uploads)
|
||||
}
|
||||
|
||||
export const deleteObjects = async function (ctx: any) {
|
||||
export const deleteObjects = async function (ctx: Ctx) {
|
||||
ctx.body = await objectStore.deleteFiles(
|
||||
ObjectStoreBuckets.APPS,
|
||||
ctx.request.body.keys
|
||||
)
|
||||
}
|
||||
|
||||
export const serveApp = async function (ctx: any) {
|
||||
export const serveApp = async function (ctx: Ctx) {
|
||||
const bbHeaderEmbed =
|
||||
ctx.request.get("x-budibase-embed")?.toLowerCase() === "true"
|
||||
|
||||
|
@ -181,7 +179,7 @@ export const serveApp = async function (ctx: any) {
|
|||
}
|
||||
}
|
||||
|
||||
export const serveBuilderPreview = async function (ctx: any) {
|
||||
export const serveBuilderPreview = async function (ctx: Ctx) {
|
||||
const db = context.getAppDB({ skip_setup: true })
|
||||
const appInfo = await db.get<App>(DocumentType.APP_METADATA)
|
||||
|
||||
|
@ -197,18 +195,30 @@ export const serveBuilderPreview = async function (ctx: any) {
|
|||
}
|
||||
}
|
||||
|
||||
export const serveClientLibrary = async function (ctx: any) {
|
||||
export const serveClientLibrary = async function (ctx: Ctx) {
|
||||
const appId = context.getAppId() || (ctx.request.query.appId as string)
|
||||
let rootPath = join(NODE_MODULES_PATH, "@budibase", "client", "dist")
|
||||
// incase running from TS directly
|
||||
if (env.isDev() && !fs.existsSync(rootPath)) {
|
||||
rootPath = join(require.resolve("@budibase/client"), "..")
|
||||
if (!appId) {
|
||||
ctx.throw(400, "No app ID provided - cannot fetch client library.")
|
||||
}
|
||||
if (env.isProd()) {
|
||||
ctx.body = await objectStore.getReadStream(
|
||||
ObjectStoreBuckets.APPS,
|
||||
objectStore.clientLibraryPath(appId!)
|
||||
)
|
||||
ctx.set("Content-Type", "application/javascript")
|
||||
} else if (env.isDev()) {
|
||||
// incase running from TS directly
|
||||
const tsPath = join(require.resolve("@budibase/client"), "..")
|
||||
return send(ctx, "budibase-client.js", {
|
||||
root: !fs.existsSync(rootPath) ? tsPath : rootPath,
|
||||
})
|
||||
} else {
|
||||
ctx.throw(500, "Unable to retrieve client library.")
|
||||
}
|
||||
return send(ctx, "budibase-client.js", {
|
||||
root: rootPath,
|
||||
})
|
||||
}
|
||||
|
||||
export const getSignedUploadURL = async function (ctx: any) {
|
||||
export const getSignedUploadURL = async function (ctx: Ctx) {
|
||||
// Ensure datasource is valid
|
||||
let datasource
|
||||
try {
|
||||
|
@ -247,7 +257,7 @@ export const getSignedUploadURL = async function (ctx: any) {
|
|||
const params = { Bucket: bucket, Key: key }
|
||||
signedUrl = s3.getSignedUrl("putObject", params)
|
||||
publicUrl = `https://${bucket}.s3.${awsRegion}.amazonaws.com/${key}`
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
ctx.throw(400, error)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,15 +27,9 @@ router.param("file", async (file: any, ctx: any, next: any) => {
|
|||
return next()
|
||||
})
|
||||
|
||||
// only used in development for retrieving the client library,
|
||||
// in production the client lib is always stored in the object store.
|
||||
if (env.isDev()) {
|
||||
router.get("/api/assets/client", controller.serveClientLibrary)
|
||||
}
|
||||
|
||||
router
|
||||
// TODO: for now this builder endpoint is not authorized/secured, will need to be
|
||||
.get("/builder/:file*", controller.serveBuilder)
|
||||
.get("/api/assets/client", controller.serveClientLibrary)
|
||||
.post("/api/attachments/process", authorized(BUILDER), controller.uploadFile)
|
||||
.post(
|
||||
"/api/attachments/delete",
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as syncApps from "./usageQuotas/syncApps"
|
|||
import * as syncRows from "./usageQuotas/syncRows"
|
||||
import * as syncPlugins from "./usageQuotas/syncPlugins"
|
||||
import * as syncUsers from "./usageQuotas/syncUsers"
|
||||
import * as syncCreators from "./usageQuotas/syncCreators"
|
||||
|
||||
/**
|
||||
* Synchronise quotas to the state of the db.
|
||||
|
@ -13,5 +14,6 @@ export const run = async () => {
|
|||
await syncRows.run()
|
||||
await syncPlugins.run()
|
||||
await syncUsers.run()
|
||||
await syncCreators.run()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { users } from "@budibase/backend-core"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import { QuotaUsageType, StaticQuotaName } from "@budibase/types"
|
||||
|
||||
export const run = async () => {
|
||||
const creatorCount = await users.getCreatorCount()
|
||||
console.log(`Syncing creator count: ${creatorCount}`)
|
||||
await quotas.setUsage(
|
||||
creatorCount,
|
||||
StaticQuotaName.CREATORS,
|
||||
QuotaUsageType.STATIC
|
||||
)
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import TestConfig from "../../../../tests/utilities/TestConfiguration"
|
||||
import * as syncCreators from "../syncCreators"
|
||||
import { quotas } from "@budibase/pro"
|
||||
|
||||
describe("syncCreators", () => {
|
||||
let config = new TestConfig(false)
|
||||
|
||||
beforeEach(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(config.end)
|
||||
|
||||
it("syncs creators", async () => {
|
||||
return config.doInContext(null, async () => {
|
||||
await config.createUser({ admin: true })
|
||||
|
||||
await syncCreators.run()
|
||||
|
||||
const usageDoc = await quotas.getQuotaUsage()
|
||||
// default + additional creator
|
||||
const creatorsCount = 2
|
||||
expect(usageDoc.usageQuota.creators).toBe(creatorsCount)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -6,6 +6,7 @@ import {
|
|||
InternalTable,
|
||||
} from "@budibase/types"
|
||||
import { getProdAppID } from "./applications"
|
||||
import * as _ from "lodash/fp"
|
||||
|
||||
// checks if a user is specifically a builder, given an app ID
|
||||
export function isBuilder(user: User | ContextUser, appId?: string): boolean {
|
||||
|
@ -58,6 +59,18 @@ export function hasAppBuilderPermissions(user?: User | ContextUser): boolean {
|
|||
return !isGlobalBuilder && appLength != null && appLength > 0
|
||||
}
|
||||
|
||||
export function hasAppCreatorPermissions(user?: User | ContextUser): boolean {
|
||||
if (!user) {
|
||||
return false
|
||||
}
|
||||
return _.flow(
|
||||
_.get("roles"),
|
||||
_.values,
|
||||
_.find(x => ["CREATOR", "ADMIN"].includes(x)),
|
||||
x => !!x
|
||||
)(user)
|
||||
}
|
||||
|
||||
// checks if a user is capable of building any app
|
||||
export function hasBuilderPermissions(user?: User | ContextUser): boolean {
|
||||
if (!user) {
|
||||
|
@ -74,6 +87,18 @@ export function hasAdminPermissions(user?: User | ContextUser): boolean {
|
|||
return !!user.admin?.global
|
||||
}
|
||||
|
||||
export function isCreator(user?: User | ContextUser): boolean {
|
||||
if (!user) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
isGlobalBuilder(user) ||
|
||||
hasAdminPermissions(user) ||
|
||||
hasAppBuilderPermissions(user) ||
|
||||
hasAppCreatorPermissions(user)
|
||||
)
|
||||
}
|
||||
|
||||
export function getGlobalUserID(userId?: string): string | undefined {
|
||||
if (typeof userId !== "string") {
|
||||
return userId
|
||||
|
|
|
@ -32,6 +32,7 @@ export interface StaticUsage {
|
|||
[StaticQuotaName.APPS]: number
|
||||
[StaticQuotaName.PLUGINS]: number
|
||||
[StaticQuotaName.USERS]: number
|
||||
[StaticQuotaName.CREATORS]: number
|
||||
[StaticQuotaName.USER_GROUPS]: number
|
||||
[StaticQuotaName.ROWS]: number
|
||||
triggers: {
|
||||
|
|
|
@ -14,6 +14,7 @@ export enum StaticQuotaName {
|
|||
ROWS = "rows",
|
||||
APPS = "apps",
|
||||
USERS = "users",
|
||||
CREATORS = "creators",
|
||||
USER_GROUPS = "userGroups",
|
||||
PLUGINS = "plugins",
|
||||
}
|
||||
|
@ -67,6 +68,7 @@ export type StaticQuotas = {
|
|||
[StaticQuotaName.ROWS]: Quota
|
||||
[StaticQuotaName.APPS]: Quota
|
||||
[StaticQuotaName.USERS]: Quota
|
||||
[StaticQuotaName.CREATORS]: Quota
|
||||
[StaticQuotaName.USER_GROUPS]: Quota
|
||||
[StaticQuotaName.PLUGINS]: Quota
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
"test:notify": "node scripts/testResultsWebhook",
|
||||
"test:cloud:prod": "yarn run test --testPathIgnorePatterns=\\.integration\\.",
|
||||
"test:cloud:qa": "yarn run test",
|
||||
"test:self:ci": "yarn run test --testPathIgnorePatterns=\\.integration\\. \\.cloud\\.",
|
||||
"test:self:ci": "yarn run test --testPathIgnorePatterns=\\.integration\\. \\.cloud\\. \\.license\\.",
|
||||
"serve:test:self:ci": "start-server-and-test dev:built http://localhost:4001/health test:self:ci",
|
||||
"serve": "start-server-and-test dev:built http://localhost:4001/health",
|
||||
"dev:built": "cd ../ && yarn dev:built"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import AccountInternalAPIClient from "./AccountInternalAPIClient"
|
||||
import { AccountAPI, LicenseAPI, AuthAPI } from "./apis"
|
||||
import { AccountAPI, LicenseAPI, AuthAPI, StripeAPI } from "./apis"
|
||||
import { State } from "../../types"
|
||||
|
||||
export default class AccountInternalAPI {
|
||||
|
@ -8,11 +8,13 @@ export default class AccountInternalAPI {
|
|||
auth: AuthAPI
|
||||
accounts: AccountAPI
|
||||
licenses: LicenseAPI
|
||||
stripe: StripeAPI
|
||||
|
||||
constructor(state: State) {
|
||||
this.client = new AccountInternalAPIClient(state)
|
||||
this.auth = new AuthAPI(this.client)
|
||||
this.accounts = new AccountAPI(this.client)
|
||||
this.licenses = new LicenseAPI(this.client)
|
||||
this.stripe = new StripeAPI(this.client)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,21 +2,19 @@ import AccountInternalAPIClient from "../AccountInternalAPIClient"
|
|||
import {
|
||||
Account,
|
||||
CreateOfflineLicenseRequest,
|
||||
GetLicenseKeyResponse,
|
||||
GetOfflineLicenseResponse,
|
||||
UpdateLicenseRequest,
|
||||
} from "@budibase/types"
|
||||
import { Response } from "node-fetch"
|
||||
import BaseAPI from "./BaseAPI"
|
||||
import { APIRequestOpts } from "../../../types"
|
||||
|
||||
export default class LicenseAPI extends BaseAPI {
|
||||
client: AccountInternalAPIClient
|
||||
|
||||
constructor(client: AccountInternalAPIClient) {
|
||||
super()
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async updateLicense(
|
||||
accountId: string,
|
||||
body: UpdateLicenseRequest,
|
||||
|
@ -29,9 +27,7 @@ export default class LicenseAPI extends BaseAPI {
|
|||
})
|
||||
}, opts)
|
||||
}
|
||||
|
||||
// TODO: Better approach for setting tenant id header
|
||||
|
||||
async createOfflineLicense(
|
||||
accountId: string,
|
||||
tenantId: string,
|
||||
|
@ -51,7 +47,6 @@ export default class LicenseAPI extends BaseAPI {
|
|||
expect(response.status).toBe(opts.status ? opts.status : 201)
|
||||
return response
|
||||
}
|
||||
|
||||
async getOfflineLicense(
|
||||
accountId: string,
|
||||
tenantId: string,
|
||||
|
@ -69,4 +64,74 @@ export default class LicenseAPI extends BaseAPI {
|
|||
expect(response.status).toBe(opts.status ? opts.status : 200)
|
||||
return [response, json]
|
||||
}
|
||||
async getLicenseKey(
|
||||
opts: { status?: number } = {}
|
||||
): Promise<[Response, GetLicenseKeyResponse]> {
|
||||
const [response, json] = await this.client.get(`/api/license/key`)
|
||||
expect(response.status).toBe(opts.status || 200)
|
||||
return [response, json]
|
||||
}
|
||||
async activateLicense(
|
||||
apiKey: string,
|
||||
tenantId: string,
|
||||
licenseKey: string,
|
||||
opts: APIRequestOpts = { status: 200 }
|
||||
) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.post(`/api/license/activate`, {
|
||||
body: {
|
||||
apiKey: apiKey,
|
||||
tenantId: tenantId,
|
||||
licenseKey: licenseKey,
|
||||
},
|
||||
})
|
||||
}, opts)
|
||||
}
|
||||
async regenerateLicenseKey(opts: APIRequestOpts = { status: 200 }) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.post(`/api/license/key/regenerate`, {})
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async getPlans(opts: APIRequestOpts = { status: 200 }) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.get(`/api/plans`)
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async updatePlan(opts: APIRequestOpts = { status: 200 }) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.put(`/api/license/plan`)
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async refreshAccountLicense(
|
||||
accountId: string,
|
||||
opts: { status?: number } = {}
|
||||
): Promise<Response> {
|
||||
const [response, json] = await this.client.post(
|
||||
`/api/accounts/${accountId}/license/refresh`,
|
||||
{
|
||||
internal: true,
|
||||
}
|
||||
)
|
||||
expect(response.status).toBe(opts.status ? opts.status : 201)
|
||||
return response
|
||||
}
|
||||
|
||||
async getLicenseUsage(opts: APIRequestOpts = { status: 200 }) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.get(`/api/license/usage`)
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async licenseUsageTriggered(
|
||||
opts: { status?: number } = {}
|
||||
): Promise<Response> {
|
||||
const [response, json] = await this.client.post(
|
||||
`/api/license/usage/triggered`
|
||||
)
|
||||
expect(response.status).toBe(opts.status ? opts.status : 201)
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import AccountInternalAPIClient from "../AccountInternalAPIClient"
|
||||
import BaseAPI from "./BaseAPI"
|
||||
import { APIRequestOpts } from "../../../types"
|
||||
|
||||
export default class StripeAPI extends BaseAPI {
|
||||
client: AccountInternalAPIClient
|
||||
|
||||
constructor(client: AccountInternalAPIClient) {
|
||||
super()
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async createCheckoutSession(
|
||||
priceId: string,
|
||||
opts: APIRequestOpts = { status: 200 }
|
||||
) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.post(`/api/stripe/checkout-session`, {
|
||||
body: { priceId },
|
||||
})
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async checkoutSuccess(opts: APIRequestOpts = { status: 200 }) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.post(`/api/stripe/checkout-success`)
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async createPortalSession(
|
||||
stripeCustomerId: string,
|
||||
opts: APIRequestOpts = { status: 200 }
|
||||
) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.post(`/api/stripe/portal-session`, {
|
||||
body: { stripeCustomerId },
|
||||
})
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async linkStripeCustomer(opts: APIRequestOpts = { status: 200 }) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.post(`/api/stripe/link`)
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async getInvoices(opts: APIRequestOpts = { status: 200 }) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.get(`/api/stripe/invoices`)
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async getUpcomingInvoice(opts: APIRequestOpts = { status: 200 }) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.get(`/api/stripe/upcoming-invoice`)
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async getStripeCustomers(opts: APIRequestOpts = { status: 200 }) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.get(`/api/stripe/customers`)
|
||||
}, opts)
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export { default as AuthAPI } from "./AuthAPI"
|
||||
export { default as AccountAPI } from "./AccountAPI"
|
||||
export { default as LicenseAPI } from "./LicenseAPI"
|
||||
export { default as StripeAPI } from "./StripeAPI"
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import TestConfiguration from "../../config/TestConfiguration"
|
||||
import * as fixures from "../../fixtures"
|
||||
import { Feature, Hosting } from "@budibase/types"
|
||||
|
||||
describe("license activation", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.beforeAll()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await config.afterAll()
|
||||
})
|
||||
|
||||
it("creates, activates and deletes online license - self host", async () => {
|
||||
// Remove existing license key
|
||||
await config.internalApi.license.deleteLicenseKey()
|
||||
|
||||
// Verify license key not found
|
||||
await config.internalApi.license.getLicenseKey({ status: 404 })
|
||||
|
||||
// Create self host account
|
||||
const createAccountRequest = fixures.accounts.generateAccount({
|
||||
hosting: Hosting.SELF,
|
||||
})
|
||||
const [createAccountRes, account] =
|
||||
await config.accountsApi.accounts.create(createAccountRequest, {
|
||||
autoVerify: true,
|
||||
})
|
||||
|
||||
let licenseKey: string = " "
|
||||
await config.doInNewState(async () => {
|
||||
await config.loginAsAccount(createAccountRequest)
|
||||
// Retrieve license key
|
||||
const [res, body] = await config.accountsApi.licenses.getLicenseKey()
|
||||
licenseKey = body.licenseKey
|
||||
})
|
||||
|
||||
const accountId = account.accountId!
|
||||
|
||||
// Update license to have paid feature
|
||||
const [res, acc] = await config.accountsApi.licenses.updateLicense(
|
||||
accountId,
|
||||
{
|
||||
overrides: {
|
||||
features: [Feature.APP_BACKUPS],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Activate license key
|
||||
await config.internalApi.license.activateLicenseKey({ licenseKey })
|
||||
|
||||
// Verify license updated with new feature
|
||||
await config.doInNewState(async () => {
|
||||
await config.loginAsAccount(createAccountRequest)
|
||||
const [selfRes, body] = await config.api.accounts.self()
|
||||
expect(body.license.features[0]).toBe("appBackups")
|
||||
})
|
||||
|
||||
// Remove license key
|
||||
await config.internalApi.license.deleteLicenseKey()
|
||||
|
||||
// Verify license key not found
|
||||
await config.internalApi.license.getLicenseKey({ status: 404 })
|
||||
})
|
||||
})
|
|
@ -1,17 +1,19 @@
|
|||
import { Response } from "node-fetch"
|
||||
import {
|
||||
ActivateLicenseKeyRequest,
|
||||
ActivateOfflineLicenseTokenRequest,
|
||||
GetLicenseKeyResponse,
|
||||
GetOfflineIdentifierResponse,
|
||||
GetOfflineLicenseTokenResponse,
|
||||
} from "@budibase/types"
|
||||
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
|
||||
import BaseAPI from "./BaseAPI"
|
||||
import { APIRequestOpts } from "../../../types"
|
||||
|
||||
export default class LicenseAPI extends BaseAPI {
|
||||
constructor(client: BudibaseInternalAPIClient) {
|
||||
super(client)
|
||||
}
|
||||
|
||||
async getOfflineLicenseToken(
|
||||
opts: { status?: number } = {}
|
||||
): Promise<[Response, GetOfflineLicenseTokenResponse]> {
|
||||
|
@ -21,19 +23,16 @@ export default class LicenseAPI extends BaseAPI {
|
|||
)
|
||||
return [response, body]
|
||||
}
|
||||
|
||||
async deleteOfflineLicenseToken(): Promise<[Response]> {
|
||||
const [response] = await this.del(`/global/license/offline`, 204)
|
||||
return [response]
|
||||
}
|
||||
|
||||
async activateOfflineLicenseToken(
|
||||
body: ActivateOfflineLicenseTokenRequest
|
||||
): Promise<[Response]> {
|
||||
const [response] = await this.post(`/global/license/offline`, body)
|
||||
return [response]
|
||||
}
|
||||
|
||||
async getOfflineIdentifier(): Promise<
|
||||
[Response, GetOfflineIdentifierResponse]
|
||||
> {
|
||||
|
@ -42,4 +41,23 @@ export default class LicenseAPI extends BaseAPI {
|
|||
)
|
||||
return [response, body]
|
||||
}
|
||||
|
||||
async getLicenseKey(
|
||||
opts: { status?: number } = {}
|
||||
): Promise<[Response, GetLicenseKeyResponse]> {
|
||||
const [response, body] = await this.get(`/global/license/key`, opts.status)
|
||||
return [response, body]
|
||||
}
|
||||
|
||||
async activateLicenseKey(
|
||||
body: ActivateLicenseKeyRequest
|
||||
): Promise<[Response]> {
|
||||
const [response] = await this.post(`/global/license/key`, body)
|
||||
return [response]
|
||||
}
|
||||
|
||||
async deleteLicenseKey(): Promise<[Response]> {
|
||||
const [response] = await this.del(`/global/license/key`, 204)
|
||||
return [response]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue