Merge branch 'develop' into api-screens
This commit is contained in:
commit
3c25763bd9
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.0.24-alpha.3",
|
"version": "2.0.30-alpha.1",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "2.0.24-alpha.3",
|
"version": "2.0.30-alpha.1",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
"test:watch": "jest --watchAll"
|
"test:watch": "jest --watchAll"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/types": "2.0.24-alpha.3",
|
"@budibase/types": "2.0.30-alpha.1",
|
||||||
"@shopify/jest-koa-mocks": "5.0.1",
|
"@shopify/jest-koa-mocks": "5.0.1",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-sdk": "2.1030.0",
|
"aws-sdk": "2.1030.0",
|
||||||
|
@ -62,6 +62,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/chance": "1.1.3",
|
||||||
"@types/jest": "27.5.1",
|
"@types/jest": "27.5.1",
|
||||||
"@types/koa": "2.0.52",
|
"@types/koa": "2.0.52",
|
||||||
"@types/lodash": "4.14.180",
|
"@types/lodash": "4.14.180",
|
||||||
|
@ -72,6 +73,7 @@
|
||||||
"@types/semver": "7.3.7",
|
"@types/semver": "7.3.7",
|
||||||
"@types/tar-fs": "2.0.1",
|
"@types/tar-fs": "2.0.1",
|
||||||
"@types/uuid": "8.3.4",
|
"@types/uuid": "8.3.4",
|
||||||
|
"chance": "1.1.3",
|
||||||
"ioredis-mock": "5.8.0",
|
"ioredis-mock": "5.8.0",
|
||||||
"jest": "27.5.1",
|
"jest": "27.5.1",
|
||||||
"koa": "2.7.0",
|
"koa": "2.7.0",
|
||||||
|
|
|
@ -37,6 +37,7 @@ const core = {
|
||||||
db,
|
db,
|
||||||
...dbConstants,
|
...dbConstants,
|
||||||
redis,
|
redis,
|
||||||
|
locks: redis.redlock,
|
||||||
objectStore,
|
objectStore,
|
||||||
utils,
|
utils,
|
||||||
users,
|
users,
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
import Client from "../redis"
|
import Client from "../redis"
|
||||||
import utils from "../redis/utils"
|
import utils from "../redis/utils"
|
||||||
import clients from "../redis/init"
|
import clients from "../redis/init"
|
||||||
|
import * as redlock from "../redis/redlock"
|
||||||
|
|
||||||
export = {
|
export = {
|
||||||
Client,
|
Client,
|
||||||
utils,
|
utils,
|
||||||
clients,
|
clients,
|
||||||
|
redlock,
|
||||||
}
|
}
|
||||||
|
|
|
@ -216,6 +216,9 @@ export = class RedisWrapper {
|
||||||
|
|
||||||
async bulkGet(keys: string[]) {
|
async bulkGet(keys: string[]) {
|
||||||
const db = this._db
|
const db = this._db
|
||||||
|
if (keys.length === 0) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
const prefixedKeys = keys.map(key => addDbPrefix(db, key))
|
const prefixedKeys = keys.map(key => addDbPrefix(db, key))
|
||||||
let response = await this.getClient().mget(prefixedKeys)
|
let response = await this.getClient().mget(prefixedKeys)
|
||||||
if (Array.isArray(response)) {
|
if (Array.isArray(response)) {
|
||||||
|
|
|
@ -1,27 +1,23 @@
|
||||||
const Client = require("./index")
|
const Client = require("./index")
|
||||||
const utils = require("./utils")
|
const utils = require("./utils")
|
||||||
const { getRedlock } = require("./redlock")
|
|
||||||
|
|
||||||
let userClient, sessionClient, appClient, cacheClient, writethroughClient
|
let userClient,
|
||||||
let migrationsRedlock
|
sessionClient,
|
||||||
|
appClient,
|
||||||
// turn retry off so that only one instance can ever hold the lock
|
cacheClient,
|
||||||
const migrationsRedlockConfig = { retryCount: 0 }
|
writethroughClient,
|
||||||
|
lockClient
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
userClient = await new Client(utils.Databases.USER_CACHE).init()
|
userClient = await new Client(utils.Databases.USER_CACHE).init()
|
||||||
sessionClient = await new Client(utils.Databases.SESSIONS).init()
|
sessionClient = await new Client(utils.Databases.SESSIONS).init()
|
||||||
appClient = await new Client(utils.Databases.APP_METADATA).init()
|
appClient = await new Client(utils.Databases.APP_METADATA).init()
|
||||||
cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init()
|
cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init()
|
||||||
|
lockClient = await new Client(utils.Databases.LOCKS).init()
|
||||||
writethroughClient = await new Client(
|
writethroughClient = await new Client(
|
||||||
utils.Databases.WRITE_THROUGH,
|
utils.Databases.WRITE_THROUGH,
|
||||||
utils.SelectableDatabases.WRITE_THROUGH
|
utils.SelectableDatabases.WRITE_THROUGH
|
||||||
).init()
|
).init()
|
||||||
// pass the underlying ioredis client to redlock
|
|
||||||
migrationsRedlock = getRedlock(
|
|
||||||
cacheClient.getClient(),
|
|
||||||
migrationsRedlockConfig
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on("exit", async () => {
|
process.on("exit", async () => {
|
||||||
|
@ -30,6 +26,7 @@ process.on("exit", async () => {
|
||||||
if (appClient) await appClient.finish()
|
if (appClient) await appClient.finish()
|
||||||
if (cacheClient) await cacheClient.finish()
|
if (cacheClient) await cacheClient.finish()
|
||||||
if (writethroughClient) await writethroughClient.finish()
|
if (writethroughClient) await writethroughClient.finish()
|
||||||
|
if (lockClient) await lockClient.finish()
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -63,10 +60,10 @@ module.exports = {
|
||||||
}
|
}
|
||||||
return writethroughClient
|
return writethroughClient
|
||||||
},
|
},
|
||||||
getMigrationsRedlock: async () => {
|
getLockClient: async () => {
|
||||||
if (!migrationsRedlock) {
|
if (!lockClient) {
|
||||||
await init()
|
await init()
|
||||||
}
|
}
|
||||||
return migrationsRedlock
|
return lockClient
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,37 @@
|
||||||
import Redlock from "redlock"
|
import Redlock, { Options } from "redlock"
|
||||||
|
import { getLockClient } from "./init"
|
||||||
|
import { LockOptions, LockType } from "@budibase/types"
|
||||||
|
import * as tenancy from "../tenancy"
|
||||||
|
|
||||||
export const getRedlock = (redisClient: any, opts = { retryCount: 10 }) => {
|
let noRetryRedlock: Redlock | undefined
|
||||||
return new Redlock([redisClient], {
|
|
||||||
|
const getClient = async (type: LockType): Promise<Redlock> => {
|
||||||
|
switch (type) {
|
||||||
|
case LockType.TRY_ONCE: {
|
||||||
|
if (!noRetryRedlock) {
|
||||||
|
noRetryRedlock = await newRedlock(OPTIONS.TRY_ONCE)
|
||||||
|
}
|
||||||
|
return noRetryRedlock
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Could not get redlock client: ${type}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OPTIONS = {
|
||||||
|
TRY_ONCE: {
|
||||||
|
// immediately throws an error if the lock is already held
|
||||||
|
retryCount: 0,
|
||||||
|
},
|
||||||
|
DEFAULT: {
|
||||||
// the expected clock drift; for more details
|
// the expected clock drift; for more details
|
||||||
// see http://redis.io/topics/distlock
|
// see http://redis.io/topics/distlock
|
||||||
driftFactor: 0.01, // multiplied by lock ttl to determine drift time
|
driftFactor: 0.01, // multiplied by lock ttl to determine drift time
|
||||||
|
|
||||||
// the max number of times Redlock will attempt
|
// the max number of times Redlock will attempt
|
||||||
// to lock a resource before erroring
|
// to lock a resource before erroring
|
||||||
retryCount: opts.retryCount,
|
retryCount: 10,
|
||||||
|
|
||||||
// the time in ms between attempts
|
// the time in ms between attempts
|
||||||
retryDelay: 200, // time in ms
|
retryDelay: 200, // time in ms
|
||||||
|
@ -16,6 +39,45 @@ export const getRedlock = (redisClient: any, opts = { retryCount: 10 }) => {
|
||||||
// the max time in ms randomly added to retries
|
// the max time in ms randomly added to retries
|
||||||
// to improve performance under high contention
|
// to improve performance under high contention
|
||||||
// see https://www.awsarchitectureblog.com/2015/03/backoff.html
|
// see https://www.awsarchitectureblog.com/2015/03/backoff.html
|
||||||
retryJitter: 200, // time in ms
|
retryJitter: 100, // time in ms
|
||||||
})
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const newRedlock = async (opts: Options = {}) => {
|
||||||
|
let options = { ...OPTIONS.DEFAULT, ...opts }
|
||||||
|
const redisWrapper = await getLockClient()
|
||||||
|
const client = redisWrapper.getClient()
|
||||||
|
return new Redlock([client], options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const doWithLock = async (opts: LockOptions, task: any) => {
|
||||||
|
const redlock = await getClient(opts.type)
|
||||||
|
let lock
|
||||||
|
try {
|
||||||
|
// aquire lock
|
||||||
|
let name: string = `${tenancy.getTenantId()}_${opts.name}`
|
||||||
|
if (opts.nameSuffix) {
|
||||||
|
name = name + `_${opts.nameSuffix}`
|
||||||
|
}
|
||||||
|
lock = await redlock.lock(name, opts.ttl)
|
||||||
|
// perform locked task
|
||||||
|
return task()
|
||||||
|
} catch (e: any) {
|
||||||
|
// lock limit exceeded
|
||||||
|
if (e.name === "LockError") {
|
||||||
|
if (opts.type === LockType.TRY_ONCE) {
|
||||||
|
// don't throw for try-once locks, they will always error
|
||||||
|
// due to retry count (0) exceeded
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (lock) {
|
||||||
|
await lock.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ exports.Databases = {
|
||||||
LICENSES: "license",
|
LICENSES: "license",
|
||||||
GENERIC_CACHE: "data_cache",
|
GENERIC_CACHE: "data_cache",
|
||||||
WRITE_THROUGH: "writeThrough",
|
WRITE_THROUGH: "writeThrough",
|
||||||
|
LOCKS: "locks",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { generator, uuid } from "."
|
||||||
|
import { AuthType, CloudAccount, Hosting } from "@budibase/types"
|
||||||
|
import * as db from "../../../src/db/utils"
|
||||||
|
|
||||||
|
export const cloudAccount = (): CloudAccount => {
|
||||||
|
return {
|
||||||
|
accountId: uuid(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
verified: true,
|
||||||
|
verificationSent: true,
|
||||||
|
tier: "",
|
||||||
|
email: generator.email(),
|
||||||
|
tenantId: generator.word(),
|
||||||
|
hosting: Hosting.CLOUD,
|
||||||
|
authType: AuthType.PASSWORD,
|
||||||
|
password: generator.word(),
|
||||||
|
tenantName: generator.word(),
|
||||||
|
name: generator.name(),
|
||||||
|
size: "10+",
|
||||||
|
profession: "Software Engineer",
|
||||||
|
budibaseUserId: db.generateGlobalUserID(),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { v4 as uuid } from "uuid"
|
|
@ -1 +1,8 @@
|
||||||
|
export * from "./common"
|
||||||
|
|
||||||
|
import Chance from "chance"
|
||||||
|
export const generator = new Chance()
|
||||||
|
|
||||||
export * as koa from "./koa"
|
export * as koa from "./koa"
|
||||||
|
export * as accounts from "./accounts"
|
||||||
|
export * as licenses from "./licenses"
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { AccountPlan, License, PlanType, Quotas } from "@budibase/types"
|
||||||
|
|
||||||
|
const newPlan = (type: PlanType = PlanType.FREE): AccountPlan => {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const newLicense = (opts: {
|
||||||
|
quotas: Quotas
|
||||||
|
planType?: PlanType
|
||||||
|
}): License => {
|
||||||
|
return {
|
||||||
|
features: [],
|
||||||
|
quotas: opts.quotas,
|
||||||
|
plan: newPlan(opts.planType),
|
||||||
|
}
|
||||||
|
}
|
|
@ -663,6 +663,11 @@
|
||||||
"@types/connect" "*"
|
"@types/connect" "*"
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/chance@1.1.3":
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.1.3.tgz#d19fe9391288d60fdccd87632bfc9ab2b4523fea"
|
||||||
|
integrity sha512-X6c6ghhe4/sQh4XzcZWSFaTAUOda38GQHmq9BUanYkOE/EO7ZrkazwKmtsj3xzTjkLWmwULE++23g3d3CCWaWw==
|
||||||
|
|
||||||
"@types/connect@*":
|
"@types/connect@*":
|
||||||
version "3.4.35"
|
version "3.4.35"
|
||||||
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
|
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
|
||||||
|
@ -1555,6 +1560,11 @@ chalk@^4.0.0, chalk@^4.1.0:
|
||||||
ansi-styles "^4.1.0"
|
ansi-styles "^4.1.0"
|
||||||
supports-color "^7.1.0"
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
|
chance@1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/chance/-/chance-1.1.3.tgz#414f08634ee479c7a316b569050ea20751b82dd3"
|
||||||
|
integrity sha512-XeJsdoVAzDb1WRPRuMBesRSiWpW1uNTo5Fd7mYxPJsAfgX71+jfuCOHOdbyBz2uAUZ8TwKcXgWk3DMedFfJkbg==
|
||||||
|
|
||||||
char-regex@^1.0.2:
|
char-regex@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
|
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "2.0.24-alpha.3",
|
"version": "2.0.30-alpha.1",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||||
"@budibase/string-templates": "2.0.24-alpha.3",
|
"@budibase/string-templates": "2.0.30-alpha.1",
|
||||||
"@spectrum-css/actionbutton": "^1.0.1",
|
"@spectrum-css/actionbutton": "^1.0.1",
|
||||||
"@spectrum-css/actiongroup": "^1.0.1",
|
"@spectrum-css/actiongroup": "^1.0.1",
|
||||||
"@spectrum-css/avatar": "^3.0.2",
|
"@spectrum-css/avatar": "^3.0.2",
|
||||||
|
|
|
@ -2,7 +2,7 @@ import filterTests from "../support/filterTests"
|
||||||
const interact = require('../support/interact')
|
const interact = require('../support/interact')
|
||||||
|
|
||||||
filterTests(['smoke', 'all'], () => {
|
filterTests(['smoke', 'all'], () => {
|
||||||
context("Auto Screens UI", () => {
|
xcontext("Auto Screens UI", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
cy.deleteAllApps()
|
cy.deleteAllApps()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import filterTests from "../../support/filterTests"
|
import filterTests from "../../support/filterTests"
|
||||||
|
|
||||||
filterTests(["all"], () => {
|
filterTests(["all"], () => {
|
||||||
context("PostgreSQL Datasource Testing", () => {
|
xcontext("PostgreSQL Datasource Testing", () => {
|
||||||
if (Cypress.env("TEST_ENV")) {
|
if (Cypress.env("TEST_ENV")) {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "2.0.24-alpha.3",
|
"version": "2.0.30-alpha.1",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -71,10 +71,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.0.24-alpha.3",
|
"@budibase/bbui": "2.0.30-alpha.1",
|
||||||
"@budibase/client": "2.0.24-alpha.3",
|
"@budibase/client": "2.0.30-alpha.1",
|
||||||
"@budibase/frontend-core": "2.0.24-alpha.3",
|
"@budibase/frontend-core": "2.0.30-alpha.1",
|
||||||
"@budibase/string-templates": "2.0.24-alpha.3",
|
"@budibase/string-templates": "2.0.30-alpha.1",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
|
|
@ -17,12 +17,21 @@
|
||||||
$: selectedRoleId = selectedRole._id
|
$: selectedRoleId = selectedRole._id
|
||||||
$: otherRoles = editableRoles.filter(role => role._id !== selectedRoleId)
|
$: otherRoles = editableRoles.filter(role => role._id !== selectedRoleId)
|
||||||
$: isCreating = selectedRoleId == null || selectedRoleId === ""
|
$: isCreating = selectedRoleId == null || selectedRoleId === ""
|
||||||
|
|
||||||
|
$: hasUniqueRoleName = !otherRoles
|
||||||
|
?.map(role => role.name)
|
||||||
|
?.includes(selectedRole.name)
|
||||||
|
|
||||||
$: valid =
|
$: valid =
|
||||||
selectedRole.name &&
|
selectedRole.name &&
|
||||||
selectedRole.inherits &&
|
selectedRole.inherits &&
|
||||||
selectedRole.permissionId &&
|
selectedRole.permissionId &&
|
||||||
!builtInRoles.includes(selectedRole.name)
|
!builtInRoles.includes(selectedRole.name)
|
||||||
|
|
||||||
|
$: shouldDisableRoleInput =
|
||||||
|
builtInRoles.includes(selectedRole.name) &&
|
||||||
|
selectedRole.name?.toLowerCase() === selectedRoleId?.toLowerCase()
|
||||||
|
|
||||||
const fetchBasePermissions = async () => {
|
const fetchBasePermissions = async () => {
|
||||||
try {
|
try {
|
||||||
basePermissions = await API.getBasePermissions()
|
basePermissions = await API.getBasePermissions()
|
||||||
|
@ -99,7 +108,7 @@
|
||||||
title="Edit Roles"
|
title="Edit Roles"
|
||||||
confirmText={isCreating ? "Create" : "Save"}
|
confirmText={isCreating ? "Create" : "Save"}
|
||||||
onConfirm={saveRole}
|
onConfirm={saveRole}
|
||||||
disabled={!valid}
|
disabled={!valid || !hasUniqueRoleName}
|
||||||
>
|
>
|
||||||
{#if errors.length}
|
{#if errors.length}
|
||||||
<ErrorsBox {errors} />
|
<ErrorsBox {errors} />
|
||||||
|
@ -119,15 +128,16 @@
|
||||||
<Input
|
<Input
|
||||||
label="Name"
|
label="Name"
|
||||||
bind:value={selectedRole.name}
|
bind:value={selectedRole.name}
|
||||||
disabled={builtInRoles.includes(selectedRole.name)}
|
disabled={shouldDisableRoleInput}
|
||||||
|
error={!hasUniqueRoleName ? "Select a unique role name." : null}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
label="Inherits Role"
|
label="Inherits Role"
|
||||||
bind:value={selectedRole.inherits}
|
bind:value={selectedRole.inherits}
|
||||||
options={otherRoles}
|
options={selectedRole._id === "BASIC" ? $roles : otherRoles}
|
||||||
getOptionValue={role => role._id}
|
getOptionValue={role => role._id}
|
||||||
getOptionLabel={role => role.name}
|
getOptionLabel={role => role.name}
|
||||||
disabled={builtInRoles.includes(selectedRole.name)}
|
disabled={shouldDisableRoleInput}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
label="Base Permissions"
|
label="Base Permissions"
|
||||||
|
@ -135,11 +145,11 @@
|
||||||
options={basePermissions}
|
options={basePermissions}
|
||||||
getOptionValue={x => x._id}
|
getOptionValue={x => x._id}
|
||||||
getOptionLabel={x => x.name}
|
getOptionLabel={x => x.name}
|
||||||
disabled={builtInRoles.includes(selectedRole.name)}
|
disabled={shouldDisableRoleInput}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div slot="footer">
|
<div slot="footer">
|
||||||
{#if !isCreating}
|
{#if !isCreating && !builtInRoles.includes(selectedRole.name)}
|
||||||
<Button warning on:click={deleteRole}>Delete</Button>
|
<Button warning on:click={deleteRole}>Delete</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -209,27 +209,29 @@
|
||||||
{:else}
|
{:else}
|
||||||
<Body size="S"><i>No tables found.</i></Body>
|
<Body size="S"><i>No tables found.</i></Body>
|
||||||
{/if}
|
{/if}
|
||||||
<Divider />
|
{#if integration.relationships !== false}
|
||||||
<div class="query-header">
|
<Divider />
|
||||||
<Heading size="S">Relationships</Heading>
|
<div class="query-header">
|
||||||
<Button primary on:click={() => openRelationshipModal()}>
|
<Heading size="S">Relationships</Heading>
|
||||||
Define relationship
|
<Button primary on:click={() => openRelationshipModal()}>
|
||||||
</Button>
|
Define relationship
|
||||||
</div>
|
</Button>
|
||||||
<Body>
|
</div>
|
||||||
Tell budibase how your tables are related to get even more smart features.
|
<Body>
|
||||||
</Body>
|
Tell budibase how your tables are related to get even more smart features.
|
||||||
{#if relationshipInfo && relationshipInfo.length > 0}
|
</Body>
|
||||||
<Table
|
{#if relationshipInfo && relationshipInfo.length > 0}
|
||||||
on:click={({ detail }) => openRelationshipModal(detail.from, detail.to)}
|
<Table
|
||||||
schema={relationshipSchema}
|
on:click={({ detail }) => openRelationshipModal(detail.from, detail.to)}
|
||||||
data={relationshipInfo}
|
schema={relationshipSchema}
|
||||||
allowEditColumns={false}
|
data={relationshipInfo}
|
||||||
allowEditRows={false}
|
allowEditColumns={false}
|
||||||
allowSelectRows={false}
|
allowEditRows={false}
|
||||||
/>
|
allowSelectRows={false}
|
||||||
{:else}
|
/>
|
||||||
<Body size="S"><i>No relationships configured.</i></Body>
|
{:else}
|
||||||
|
<Body size="S"><i>No relationships configured.</i></Body>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -156,8 +156,8 @@
|
||||||
page={$usersFetch.pageNumber + 1}
|
page={$usersFetch.pageNumber + 1}
|
||||||
hasPrevPage={$usersFetch.hasPrevPage}
|
hasPrevPage={$usersFetch.hasPrevPage}
|
||||||
hasNextPage={$usersFetch.hasNextPage}
|
hasNextPage={$usersFetch.hasNextPage}
|
||||||
goToPrevPage={$usersFetch.loading ? null : fetch.prevPage}
|
goToPrevPage={$usersFetch.loading ? null : usersFetch.prevPage}
|
||||||
goToNextPage={$usersFetch.loading ? null : fetch.nextPage}
|
goToNextPage={$usersFetch.loading ? null : usersFetch.nextPage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "2.0.24-alpha.3",
|
"version": "2.0.30-alpha.1",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -26,9 +26,9 @@
|
||||||
"outputPath": "build"
|
"outputPath": "build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "2.0.24-alpha.3",
|
"@budibase/backend-core": "2.0.30-alpha.1",
|
||||||
"@budibase/string-templates": "2.0.24-alpha.3",
|
"@budibase/string-templates": "2.0.30-alpha.1",
|
||||||
"@budibase/types": "2.0.24-alpha.3",
|
"@budibase/types": "2.0.30-alpha.1",
|
||||||
"axios": "0.21.2",
|
"axios": "0.21.2",
|
||||||
"chalk": "4.1.0",
|
"chalk": "4.1.0",
|
||||||
"cli-progress": "3.11.2",
|
"cli-progress": "3.11.2",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "2.0.24-alpha.3",
|
"version": "2.0.30-alpha.1",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.0.24-alpha.3",
|
"@budibase/bbui": "2.0.30-alpha.1",
|
||||||
"@budibase/frontend-core": "2.0.24-alpha.3",
|
"@budibase/frontend-core": "2.0.30-alpha.1",
|
||||||
"@budibase/string-templates": "2.0.24-alpha.3",
|
"@budibase/string-templates": "2.0.30-alpha.1",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
"@spectrum-css/button": "^3.0.3",
|
||||||
"@spectrum-css/card": "^3.0.3",
|
"@spectrum-css/card": "^3.0.3",
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
"@spectrum-css/divider": "^1.0.3",
|
||||||
|
|
|
@ -47,6 +47,9 @@ const createBuilderStore = () => {
|
||||||
duplicateComponent: id => {
|
duplicateComponent: id => {
|
||||||
dispatchEvent("duplicate-component", { id })
|
dispatchEvent("duplicate-component", { id })
|
||||||
},
|
},
|
||||||
|
deleteComponent: id => {
|
||||||
|
dispatchEvent("delete-component", { id })
|
||||||
|
},
|
||||||
notifyLoaded: () => {
|
notifyLoaded: () => {
|
||||||
dispatchEvent("preview-loaded")
|
dispatchEvent("preview-loaded")
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/frontend-core",
|
"name": "@budibase/frontend-core",
|
||||||
"version": "2.0.24-alpha.3",
|
"version": "2.0.30-alpha.1",
|
||||||
"description": "Budibase frontend core libraries used in builder and client",
|
"description": "Budibase frontend core libraries used in builder and client",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.0.24-alpha.3",
|
"@budibase/bbui": "2.0.30-alpha.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"svelte": "^3.46.2"
|
"svelte": "^3.46.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/sdk",
|
"name": "@budibase/sdk",
|
||||||
"version": "2.0.24-alpha.3",
|
"version": "2.0.30-alpha.1",
|
||||||
"description": "Budibase Public API SDK",
|
"description": "Budibase Public API SDK",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "2.0.24-alpha.3",
|
"version": "2.0.30-alpha.1",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -77,11 +77,11 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "10.0.3",
|
"@apidevtools/swagger-parser": "10.0.3",
|
||||||
"@budibase/backend-core": "2.0.24-alpha.3",
|
"@budibase/backend-core": "2.0.30-alpha.1",
|
||||||
"@budibase/client": "2.0.24-alpha.3",
|
"@budibase/client": "2.0.30-alpha.1",
|
||||||
"@budibase/pro": "2.0.24-alpha.3",
|
"@budibase/pro": "2.0.30-alpha.1",
|
||||||
"@budibase/string-templates": "2.0.24-alpha.3",
|
"@budibase/string-templates": "2.0.30-alpha.1",
|
||||||
"@budibase/types": "2.0.24-alpha.3",
|
"@budibase/types": "2.0.30-alpha.1",
|
||||||
"@bull-board/api": "3.7.0",
|
"@bull-board/api": "3.7.0",
|
||||||
"@bull-board/koa": "3.9.4",
|
"@bull-board/koa": "3.9.4",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
|
|
@ -17,7 +17,6 @@ import {
|
||||||
getProdAppDB,
|
getProdAppDB,
|
||||||
getDevAppDB,
|
getDevAppDB,
|
||||||
} from "@budibase/backend-core/context"
|
} from "@budibase/backend-core/context"
|
||||||
import { quotas } from "@budibase/pro"
|
|
||||||
import { events } from "@budibase/backend-core"
|
import { events } from "@budibase/backend-core"
|
||||||
|
|
||||||
// the max time we can wait for an invalidation to complete before considering it failed
|
// the max time we can wait for an invalidation to complete before considering it failed
|
||||||
|
|
|
@ -103,7 +103,7 @@ exports.revert = async ctx => {
|
||||||
target: appId,
|
target: appId,
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
if (!env.isCypress()) {
|
if (env.COUCH_DB_URL) {
|
||||||
// in-memory db stalls on rollback
|
// in-memory db stalls on rollback
|
||||||
await replication.rollback()
|
await replication.rollback()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,9 @@
|
||||||
const { getDefinitions } = require("../../integrations")
|
const { getDefinitions } = require("../../integrations")
|
||||||
const { SourceName } = require("@budibase/types")
|
|
||||||
const googlesheets = require("../../integrations/googlesheets")
|
|
||||||
const { featureFlags } = require("@budibase/backend-core")
|
|
||||||
|
|
||||||
exports.fetch = async function (ctx) {
|
exports.fetch = async function (ctx) {
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
const defs = await getDefinitions()
|
const defs = await getDefinitions()
|
||||||
|
|
||||||
// for google sheets integration google verification
|
|
||||||
if (featureFlags.isEnabled(featureFlags.TenantFeatureFlag.GOOGLE_SHEETS)) {
|
|
||||||
defs[SourceName.GOOGLE_SHEETS] = googlesheets.schema
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.body = defs
|
ctx.body = defs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { Table } from "@budibase/types"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import { isEqual } from "lodash"
|
import { isEqual } from "lodash"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import env from "../../../environment"
|
||||||
|
|
||||||
function checkAutoColumns(table: Table, oldTable: Table) {
|
function checkAutoColumns(table: Table, oldTable: Table) {
|
||||||
if (!table.schema) {
|
if (!table.schema) {
|
||||||
|
@ -167,7 +168,7 @@ export async function destroy(ctx: any) {
|
||||||
await db.remove(tableToDelete)
|
await db.remove(tableToDelete)
|
||||||
|
|
||||||
// remove table search index
|
// remove table search index
|
||||||
if (!isTest()) {
|
if (!isTest() || env.COUCH_DB_URL) {
|
||||||
const currentIndexes = await db.getIndexes()
|
const currentIndexes = await db.getIndexes()
|
||||||
const existingIndex = currentIndexes.indexes.find(
|
const existingIndex = currentIndexes.indexes.find(
|
||||||
(existing: any) => existing.name === `search:${ctx.params.tableId}`
|
(existing: any) => existing.name === `search:${ctx.params.tableId}`
|
||||||
|
|
|
@ -31,6 +31,7 @@ export interface BearerAuthConfig {
|
||||||
|
|
||||||
export interface RestConfig {
|
export interface RestConfig {
|
||||||
url: string
|
url: string
|
||||||
|
rejectUnauthorized: boolean
|
||||||
defaultHeaders: {
|
defaultHeaders: {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ function runServer() {
|
||||||
checkDevelopmentEnvironment()
|
checkDevelopmentEnvironment()
|
||||||
fixPath()
|
fixPath()
|
||||||
// this will setup http and https proxies form env variables
|
// this will setup http and https proxies form env variables
|
||||||
|
process.env.GLOBAL_AGENT_FORCE_GLOBAL_AGENT = "false"
|
||||||
bootstrap()
|
bootstrap()
|
||||||
require("./app")
|
require("./app")
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ const DEFINITIONS: { [key: string]: Integration } = {
|
||||||
[SourceName.ARANGODB]: arangodb.schema,
|
[SourceName.ARANGODB]: arangodb.schema,
|
||||||
[SourceName.REST]: rest.schema,
|
[SourceName.REST]: rest.schema,
|
||||||
[SourceName.FIRESTORE]: firebase.schema,
|
[SourceName.FIRESTORE]: firebase.schema,
|
||||||
|
[SourceName.GOOGLE_SHEETS]: googlesheets.schema,
|
||||||
[SourceName.REDIS]: redis.schema,
|
[SourceName.REDIS]: redis.schema,
|
||||||
[SourceName.SNOWFLAKE]: snowflake.schema,
|
[SourceName.SNOWFLAKE]: snowflake.schema,
|
||||||
}
|
}
|
||||||
|
@ -66,10 +67,6 @@ if (
|
||||||
INTEGRATIONS[SourceName.ORACLE] = oracle.integration
|
INTEGRATIONS[SourceName.ORACLE] = oracle.integration
|
||||||
}
|
}
|
||||||
|
|
||||||
if (environment.SELF_HOSTED) {
|
|
||||||
DEFINITIONS[SourceName.GOOGLE_SHEETS] = googlesheets.schema
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getDefinitions: async () => {
|
getDefinitions: async () => {
|
||||||
const pluginSchemas: { [key: string]: Integration } = {}
|
const pluginSchemas: { [key: string]: Integration } = {}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
BearerAuthConfig,
|
BearerAuthConfig,
|
||||||
} from "../definitions/datasource"
|
} from "../definitions/datasource"
|
||||||
import { get } from "lodash"
|
import { get } from "lodash"
|
||||||
|
import * as https from "https"
|
||||||
import qs from "querystring"
|
import qs from "querystring"
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
const { formatBytes } = require("../utilities")
|
const { formatBytes } = require("../utilities")
|
||||||
|
@ -76,6 +77,11 @@ const SCHEMA: Integration = {
|
||||||
required: false,
|
required: false,
|
||||||
default: {},
|
default: {},
|
||||||
},
|
},
|
||||||
|
rejectUnauthorized: {
|
||||||
|
type: DatasourceFieldType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
legacyHttpParser: {
|
legacyHttpParser: {
|
||||||
display: "Legacy HTTP Support",
|
display: "Legacy HTTP Support",
|
||||||
type: DatasourceFieldType.BOOLEAN,
|
type: DatasourceFieldType.BOOLEAN,
|
||||||
|
@ -218,8 +224,12 @@ class RestIntegration implements IntegrationBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure the query string is fully encoded
|
if (queryString) {
|
||||||
const main = `${path}?${qs.encode(qs.decode(queryString))}`
|
// make sure the query string is fully encoded
|
||||||
|
queryString = "?" + qs.encode(qs.decode(queryString))
|
||||||
|
}
|
||||||
|
const main = `${path}${queryString}`
|
||||||
|
|
||||||
let complete = main
|
let complete = main
|
||||||
if (this.config.url && !main.startsWith("http")) {
|
if (this.config.url && !main.startsWith("http")) {
|
||||||
complete = !this.config.url ? main : `${this.config.url}/${main}`
|
complete = !this.config.url ? main : `${this.config.url}/${main}`
|
||||||
|
@ -381,6 +391,12 @@ class RestIntegration implements IntegrationBase {
|
||||||
paginationValues
|
paginationValues
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (this.config.rejectUnauthorized == false) {
|
||||||
|
input.agent = new https.Agent({
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (this.config.legacyHttpParser) {
|
if (this.config.legacyHttpParser) {
|
||||||
// https://github.com/nodejs/node/issues/43798
|
// https://github.com/nodejs/node/issues/43798
|
||||||
input.extraHttpOptions = { insecureHTTPParser: true }
|
input.extraHttpOptions = { insecureHTTPParser: true }
|
||||||
|
|
|
@ -256,7 +256,7 @@ describe("REST Integration", () => {
|
||||||
authConfigId: "c59c14bd1898a43baa08da68959b24686",
|
authConfigId: "c59c14bd1898a43baa08da68959b24686",
|
||||||
}
|
}
|
||||||
await config.integration.read(query)
|
await config.integration.read(query)
|
||||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/?`, {
|
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Basic dXNlcjpwYXNzd29yZA==",
|
Authorization: "Basic dXNlcjpwYXNzd29yZA==",
|
||||||
|
@ -269,7 +269,7 @@ describe("REST Integration", () => {
|
||||||
authConfigId: "0d91d732f34e4befabeff50b392a8ff3",
|
authConfigId: "0d91d732f34e4befabeff50b392a8ff3",
|
||||||
}
|
}
|
||||||
await config.integration.read(query)
|
await config.integration.read(query)
|
||||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/?`, {
|
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer mytoken",
|
Authorization: "Bearer mytoken",
|
||||||
|
@ -327,7 +327,7 @@ describe("REST Integration", () => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
await config.integration.create(query)
|
await config.integration.create(query)
|
||||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?`, {
|
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
[pageParam]: pageValue,
|
[pageParam]: pageValue,
|
||||||
[sizeParam]: sizeValue,
|
[sizeParam]: sizeValue,
|
||||||
|
@ -359,7 +359,7 @@ describe("REST Integration", () => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
await config.integration.create(query)
|
await config.integration.create(query)
|
||||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?`, {
|
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, {
|
||||||
body: expect.any(FormData),
|
body: expect.any(FormData),
|
||||||
headers: {},
|
headers: {},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -390,7 +390,7 @@ describe("REST Integration", () => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
await config.integration.create(query)
|
await config.integration.create(query)
|
||||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?`, {
|
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, {
|
||||||
body: expect.any(URLSearchParams),
|
body: expect.any(URLSearchParams),
|
||||||
headers: {},
|
headers: {},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -456,7 +456,7 @@ describe("REST Integration", () => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const res = await config.integration.create(query)
|
const res = await config.integration.create(query)
|
||||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?`, {
|
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
[pageParam]: pageValue,
|
[pageParam]: pageValue,
|
||||||
[sizeParam]: sizeValue,
|
[sizeParam]: sizeValue,
|
||||||
|
@ -490,7 +490,7 @@ describe("REST Integration", () => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const res = await config.integration.create(query)
|
const res = await config.integration.create(query)
|
||||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?`, {
|
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, {
|
||||||
body: expect.any(FormData),
|
body: expect.any(FormData),
|
||||||
headers: {},
|
headers: {},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -523,7 +523,7 @@ describe("REST Integration", () => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const res = await config.integration.create(query)
|
const res = await config.integration.create(query)
|
||||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?`, {
|
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, {
|
||||||
body: expect.any(URLSearchParams),
|
body: expect.any(URLSearchParams),
|
||||||
headers: {},
|
headers: {},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -563,7 +563,7 @@ describe("REST Integration", () => {
|
||||||
legacyHttpParser: true,
|
legacyHttpParser: true,
|
||||||
})
|
})
|
||||||
await config.integration.read({})
|
await config.integration.read({})
|
||||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/?`, {
|
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {},
|
headers: {},
|
||||||
extraHttpOptions: {
|
extraHttpOptions: {
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import { migrations, redis } from "@budibase/backend-core"
|
import { locks, migrations } from "@budibase/backend-core"
|
||||||
import { Migration, MigrationOptions, MigrationName } from "@budibase/types"
|
import {
|
||||||
|
Migration,
|
||||||
|
MigrationOptions,
|
||||||
|
MigrationName,
|
||||||
|
LockType,
|
||||||
|
LockName,
|
||||||
|
} from "@budibase/types"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
|
|
||||||
// migration functions
|
// migration functions
|
||||||
|
@ -86,33 +92,14 @@ export const migrate = async (options?: MigrationOptions) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrateWithLock = async (options?: MigrationOptions) => {
|
const migrateWithLock = async (options?: MigrationOptions) => {
|
||||||
// get a new lock client
|
await locks.doWithLock(
|
||||||
const redlock = await redis.clients.getMigrationsRedlock()
|
{
|
||||||
// lock for 15 minutes
|
type: LockType.TRY_ONCE,
|
||||||
const ttl = 1000 * 60 * 15
|
name: LockName.MIGRATIONS,
|
||||||
|
ttl: 1000 * 60 * 15, // auto expire the migration lock after 15 minutes
|
||||||
let migrationLock
|
},
|
||||||
|
async () => {
|
||||||
// acquire lock
|
await migrations.runMigrations(MIGRATIONS, options)
|
||||||
try {
|
|
||||||
migrationLock = await redlock.lock("migrations", ttl)
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e.name === "LockError") {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
// run migrations
|
|
||||||
try {
|
|
||||||
await migrations.runMigrations(MIGRATIONS, options)
|
|
||||||
} finally {
|
|
||||||
// release lock
|
|
||||||
try {
|
|
||||||
await migrationLock.unlock()
|
|
||||||
} catch (e) {
|
|
||||||
console.error("unable to release migration lock")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,17 @@ const { BUILTIN_ROLE_IDS } = require("@budibase/backend-core/roles")
|
||||||
exports.getFullUser = async (ctx, userId) => {
|
exports.getFullUser = async (ctx, userId) => {
|
||||||
const global = await getGlobalUser(userId)
|
const global = await getGlobalUser(userId)
|
||||||
let metadata = {}
|
let metadata = {}
|
||||||
|
|
||||||
|
// always prefer the user metadata _id and _rev
|
||||||
|
delete global._id
|
||||||
|
delete global._rev
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// this will throw an error if the db doesn't exist, or there is no appId
|
// this will throw an error if the db doesn't exist, or there is no appId
|
||||||
const db = getAppDB()
|
const db = getAppDB()
|
||||||
metadata = await db.get(userId)
|
metadata = await db.get(userId)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// it is fine if there is no user metadata, just remove global db info
|
// it is fine if there is no user metadata yet
|
||||||
delete global._id
|
|
||||||
delete global._rev
|
|
||||||
}
|
}
|
||||||
delete metadata.csrfToken
|
delete metadata.csrfToken
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1094,12 +1094,12 @@
|
||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
"@budibase/backend-core@2.0.24-alpha.3":
|
"@budibase/backend-core@2.0.30-alpha.1":
|
||||||
version "2.0.24-alpha.3"
|
version "2.0.30-alpha.1"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.0.24-alpha.3.tgz#49a92082d1ca6bed0eb82519a6c02d7d14ead751"
|
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.0.30-alpha.1.tgz#6e5cbc448cd819f67c93451d35c8793748380649"
|
||||||
integrity sha512-Zm/ddRDMzMuXCoEXZa0CzA/B1SnpQ+yjZCNAPH7Y4yYTIKfeE/DQ6COLlUixQxVMAUN1L5+GXML+px6fRigA5w==
|
integrity sha512-AuXk5IZBQ9zFTjTL2FsqqYdqqVjrSv19ozXSsb1w2z8Pyymz57WgzUiZ6uzHbHzUXNVD4Nei7cgfgSyCJ27Phg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/types" "2.0.24-alpha.3"
|
"@budibase/types" "2.0.30-alpha.1"
|
||||||
"@shopify/jest-koa-mocks" "5.0.1"
|
"@shopify/jest-koa-mocks" "5.0.1"
|
||||||
"@techpass/passport-openidconnect" "0.3.2"
|
"@techpass/passport-openidconnect" "0.3.2"
|
||||||
aws-sdk "2.1030.0"
|
aws-sdk "2.1030.0"
|
||||||
|
@ -1180,13 +1180,13 @@
|
||||||
svelte-flatpickr "^3.2.3"
|
svelte-flatpickr "^3.2.3"
|
||||||
svelte-portal "^1.0.0"
|
svelte-portal "^1.0.0"
|
||||||
|
|
||||||
"@budibase/pro@2.0.24-alpha.3":
|
"@budibase/pro@2.0.30-alpha.1":
|
||||||
version "2.0.24-alpha.3"
|
version "2.0.30-alpha.1"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.0.24-alpha.3.tgz#16605029663b07d0e3bcf0c08893878b542b6752"
|
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.0.30-alpha.1.tgz#12e4c2115d3e647d365b46e6b5df59746148d4ea"
|
||||||
integrity sha512-gBdqJzvtEAMJkhB2YTAyVYKoCANA25F1wT/K7kQtTOy5LrjDmcXah7CZoetfZQGbsKxUYEAZwJzxLSXmIxvWmQ==
|
integrity sha512-K/iy3Z3f9AVodxtuAk8yemSQ2R08tjoot4wogDCwsTMRlXxq/WChCcm9prkb2DzwOzQQSsJ3bLgL1p/9sGzagg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/backend-core" "2.0.24-alpha.3"
|
"@budibase/backend-core" "2.0.30-alpha.1"
|
||||||
"@budibase/types" "2.0.24-alpha.3"
|
"@budibase/types" "2.0.30-alpha.1"
|
||||||
"@koa/router" "8.0.8"
|
"@koa/router" "8.0.8"
|
||||||
joi "17.6.0"
|
joi "17.6.0"
|
||||||
node-fetch "^2.6.1"
|
node-fetch "^2.6.1"
|
||||||
|
@ -1209,10 +1209,10 @@
|
||||||
svelte-apexcharts "^1.0.2"
|
svelte-apexcharts "^1.0.2"
|
||||||
svelte-flatpickr "^3.1.0"
|
svelte-flatpickr "^3.1.0"
|
||||||
|
|
||||||
"@budibase/types@2.0.24-alpha.3":
|
"@budibase/types@2.0.30-alpha.1":
|
||||||
version "2.0.24-alpha.3"
|
version "2.0.30-alpha.1"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.0.24-alpha.3.tgz#84eec5f991a2cfaf48d968b07a075b343f847289"
|
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.0.30-alpha.1.tgz#1320eb9c2996e371b47b2a845ce1b81e1b21b67c"
|
||||||
integrity sha512-f9PhtqzmqPI76ITXttuvxsvqMUJtkrDYf/4MHlI2v5ssNL9r0C/hbQEXllff3L3JqViEHWxkKFmfvfnDTV8rRQ==
|
integrity sha512-tw49Jwo7Dnhv9IuZq68DVvHsKcaVpggZ/1BYkQsdzUw+0yoIe1k8t+4XPbHqBP328mpp8oPx4rUQF3yFpJv41g==
|
||||||
|
|
||||||
"@bull-board/api@3.7.0":
|
"@bull-board/api@3.7.0":
|
||||||
version "3.7.0"
|
version "3.7.0"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/string-templates",
|
"name": "@budibase/string-templates",
|
||||||
"version": "2.0.24-alpha.3",
|
"version": "2.0.30-alpha.1",
|
||||||
"description": "Handlebars wrapper for Budibase templating.",
|
"description": "Handlebars wrapper for Budibase templating.",
|
||||||
"main": "src/index.cjs",
|
"main": "src/index.cjs",
|
||||||
"module": "dist/bundle.mjs",
|
"module": "dist/bundle.mjs",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/types",
|
"name": "@budibase/types",
|
||||||
"version": "2.0.24-alpha.3",
|
"version": "2.0.30-alpha.1",
|
||||||
"description": "Budibase types",
|
"description": "Budibase types",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
export * from "./user"
|
export * from "./user"
|
||||||
|
export * from "./license"
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { QuotaUsage } from "../../documents"
|
||||||
|
|
||||||
|
export interface GetLicenseRequest {
|
||||||
|
quotaUsage: QuotaUsage
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuotaTriggeredRequest {
|
||||||
|
percentage: number
|
||||||
|
name: string
|
||||||
|
resetDate?: string
|
||||||
|
}
|
|
@ -1,4 +1,12 @@
|
||||||
import { Feature, Hosting, PlanType, Quotas } from "../../sdk"
|
import {
|
||||||
|
Feature,
|
||||||
|
Hosting,
|
||||||
|
MonthlyQuotaName,
|
||||||
|
PlanType,
|
||||||
|
Quotas,
|
||||||
|
StaticQuotaName,
|
||||||
|
} from "../../sdk"
|
||||||
|
import { MonthlyUsage, QuotaUsage, StaticUsage } from "../global"
|
||||||
|
|
||||||
export interface CreateAccount {
|
export interface CreateAccount {
|
||||||
email: string
|
email: string
|
||||||
|
@ -42,6 +50,7 @@ export interface Account extends CreateAccount {
|
||||||
licenseKey?: string
|
licenseKey?: string
|
||||||
licenseKeyActivatedAt?: number
|
licenseKeyActivatedAt?: number
|
||||||
licenseOverrides?: LicenseOverrides
|
licenseOverrides?: LicenseOverrides
|
||||||
|
quotaUsage?: QuotaUsage
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PasswordAccount extends Account {
|
export interface PasswordAccount extends Account {
|
||||||
|
|
|
@ -24,19 +24,34 @@ export interface UsageBreakdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MonthlyUsage = {
|
export type QuotaTriggers = {
|
||||||
|
[key: string]: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StaticUsage {
|
||||||
|
[StaticQuotaName.APPS]: number
|
||||||
|
[StaticQuotaName.PLUGINS]: number
|
||||||
|
[StaticQuotaName.USER_GROUPS]: number
|
||||||
|
[StaticQuotaName.ROWS]: number
|
||||||
|
triggers: {
|
||||||
|
[key in StaticQuotaName]?: QuotaTriggers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MonthlyUsage {
|
||||||
[MonthlyQuotaName.QUERIES]: number
|
[MonthlyQuotaName.QUERIES]: number
|
||||||
[MonthlyQuotaName.AUTOMATIONS]: number
|
[MonthlyQuotaName.AUTOMATIONS]: number
|
||||||
[MonthlyQuotaName.DAY_PASSES]: number
|
[MonthlyQuotaName.DAY_PASSES]: number
|
||||||
|
triggers: {
|
||||||
|
[key in MonthlyQuotaName]?: QuotaTriggers
|
||||||
|
}
|
||||||
breakdown?: {
|
breakdown?: {
|
||||||
[key in BreakdownQuotaName]?: UsageBreakdown
|
[key in BreakdownQuotaName]?: UsageBreakdown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseQuotaUsage {
|
export interface BaseQuotaUsage {
|
||||||
usageQuota: {
|
usageQuota: StaticUsage
|
||||||
[key in StaticQuotaName]: number
|
|
||||||
}
|
|
||||||
monthly: {
|
monthly: {
|
||||||
[key: string]: MonthlyUsage
|
[key: string]: MonthlyUsage
|
||||||
}
|
}
|
||||||
|
@ -51,6 +66,13 @@ export interface QuotaUsage extends BaseQuotaUsage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SetUsageValues = {
|
||||||
|
total: number
|
||||||
|
app?: number
|
||||||
|
breakdown?: number
|
||||||
|
triggers?: QuotaTriggers
|
||||||
|
}
|
||||||
|
|
||||||
export type UsageValues = {
|
export type UsageValues = {
|
||||||
total: number
|
total: number
|
||||||
app?: number
|
app?: number
|
||||||
|
|
|
@ -7,3 +7,4 @@ export * from "./datasources"
|
||||||
export * from "./search"
|
export * from "./search"
|
||||||
export * from "./koa"
|
export * from "./koa"
|
||||||
export * from "./auth"
|
export * from "./auth"
|
||||||
|
export * from "./locks"
|
||||||
|
|
|
@ -61,26 +61,40 @@ export type PlanQuotas = {
|
||||||
[PlanType.ENTERPRISE]: Quotas
|
[PlanType.ENTERPRISE]: Quotas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MonthlyQuotas = {
|
||||||
|
[MonthlyQuotaName.QUERIES]: Quota
|
||||||
|
[MonthlyQuotaName.AUTOMATIONS]: Quota
|
||||||
|
[MonthlyQuotaName.DAY_PASSES]: Quota
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StaticQuotas = {
|
||||||
|
[StaticQuotaName.ROWS]: Quota
|
||||||
|
[StaticQuotaName.APPS]: Quota
|
||||||
|
[StaticQuotaName.USER_GROUPS]: Quota
|
||||||
|
[StaticQuotaName.PLUGINS]: Quota
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConstantQuotas = {
|
||||||
|
[ConstantQuotaName.AUTOMATION_LOG_RETENTION_DAYS]: Quota
|
||||||
|
}
|
||||||
|
|
||||||
export type Quotas = {
|
export type Quotas = {
|
||||||
[QuotaType.USAGE]: {
|
[QuotaType.USAGE]: {
|
||||||
[QuotaUsageType.MONTHLY]: {
|
[QuotaUsageType.MONTHLY]: MonthlyQuotas
|
||||||
[MonthlyQuotaName.QUERIES]: Quota
|
[QuotaUsageType.STATIC]: StaticQuotas
|
||||||
[MonthlyQuotaName.AUTOMATIONS]: Quota
|
|
||||||
[MonthlyQuotaName.DAY_PASSES]: Quota
|
|
||||||
}
|
|
||||||
[QuotaUsageType.STATIC]: {
|
|
||||||
[StaticQuotaName.ROWS]: Quota
|
|
||||||
[StaticQuotaName.APPS]: Quota
|
|
||||||
[StaticQuotaName.USER_GROUPS]: Quota
|
|
||||||
[StaticQuotaName.PLUGINS]: Quota
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[QuotaType.CONSTANT]: {
|
|
||||||
[ConstantQuotaName.AUTOMATION_LOG_RETENTION_DAYS]: Quota
|
|
||||||
}
|
}
|
||||||
|
[QuotaType.CONSTANT]: ConstantQuotas
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Quota {
|
export interface Quota {
|
||||||
name: string
|
name: string
|
||||||
value: number
|
value: number
|
||||||
|
/**
|
||||||
|
* Array of whole numbers (1-100) that dictate the percentage that this quota should trigger
|
||||||
|
* at in relation to the corresponding usage inside budibase.
|
||||||
|
*
|
||||||
|
* Triggering results in a budibase installation sending a request to account-portal,
|
||||||
|
* which can have subsequent effects such as sending emails to users.
|
||||||
|
*/
|
||||||
|
triggers: number[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
export enum LockType {
|
||||||
|
/**
|
||||||
|
* If this lock is already held the attempted operation will not be performed.
|
||||||
|
* No retries will take place and no error will be thrown.
|
||||||
|
*/
|
||||||
|
TRY_ONCE = "try_once",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LockName {
|
||||||
|
MIGRATIONS = "migrations",
|
||||||
|
TRIGGER_QUOTA = "trigger_quota",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LockOptions {
|
||||||
|
/**
|
||||||
|
* The lock type determines which client to use
|
||||||
|
*/
|
||||||
|
type: LockType
|
||||||
|
/**
|
||||||
|
* The name for the lock
|
||||||
|
*/
|
||||||
|
name: LockName
|
||||||
|
/**
|
||||||
|
* The ttl to auto-expire the lock if not unlocked manually
|
||||||
|
*/
|
||||||
|
ttl: number
|
||||||
|
/**
|
||||||
|
* The suffix to add to the lock name for additional uniqueness
|
||||||
|
*/
|
||||||
|
nameSuffix?: string
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/worker",
|
"name": "@budibase/worker",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "2.0.24-alpha.3",
|
"version": "2.0.30-alpha.1",
|
||||||
"description": "Budibase background service",
|
"description": "Budibase background service",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -36,10 +36,10 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "2.0.24-alpha.3",
|
"@budibase/backend-core": "2.0.30-alpha.1",
|
||||||
"@budibase/pro": "2.0.24-alpha.3",
|
"@budibase/pro": "2.0.30-alpha.1",
|
||||||
"@budibase/string-templates": "2.0.24-alpha.3",
|
"@budibase/string-templates": "2.0.30-alpha.1",
|
||||||
"@budibase/types": "2.0.24-alpha.3",
|
"@budibase/types": "2.0.30-alpha.1",
|
||||||
"@koa/router": "8.0.8",
|
"@koa/router": "8.0.8",
|
||||||
"@sentry/node": "6.17.7",
|
"@sentry/node": "6.17.7",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
const { StaticDatabases, doWithDB } = require("@budibase/backend-core/db")
|
|
||||||
const { getTenantId } = require("@budibase/backend-core/tenancy")
|
|
||||||
const { deleteTenant } = require("@budibase/backend-core/deprovision")
|
|
||||||
const { quotas } = require("@budibase/pro")
|
|
||||||
|
|
||||||
exports.exists = async ctx => {
|
|
||||||
const tenantId = ctx.request.params
|
|
||||||
ctx.body = {
|
|
||||||
exists: await doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => {
|
|
||||||
let exists = false
|
|
||||||
try {
|
|
||||||
const tenantsDoc = await db.get(
|
|
||||||
StaticDatabases.PLATFORM_INFO.docs.tenants
|
|
||||||
)
|
|
||||||
if (tenantsDoc) {
|
|
||||||
exists = tenantsDoc.tenantIds.indexOf(tenantId) !== -1
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// if error it doesn't exist
|
|
||||||
}
|
|
||||||
return exists
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.fetch = async ctx => {
|
|
||||||
ctx.body = await doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => {
|
|
||||||
let tenants = []
|
|
||||||
try {
|
|
||||||
const tenantsDoc = await db.get(
|
|
||||||
StaticDatabases.PLATFORM_INFO.docs.tenants
|
|
||||||
)
|
|
||||||
if (tenantsDoc) {
|
|
||||||
tenants = tenantsDoc.tenantIds
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// if error it doesn't exist
|
|
||||||
}
|
|
||||||
return tenants
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.delete = async ctx => {
|
|
||||||
const tenantId = getTenantId()
|
|
||||||
|
|
||||||
if (ctx.params.tenantId !== tenantId) {
|
|
||||||
ctx.throw(403, "Unauthorized")
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await deleteTenant(tenantId)
|
|
||||||
await quotas.bustCache()
|
|
||||||
ctx.status = 204
|
|
||||||
} catch (err) {
|
|
||||||
ctx.log.error(err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
const { StaticDatabases, doWithDB } = require("@budibase/backend-core/db")
|
||||||
|
const { getTenantId } = require("@budibase/backend-core/tenancy")
|
||||||
|
const { deleteTenant } = require("@budibase/backend-core/deprovision")
|
||||||
|
import { quotas } from "@budibase/pro"
|
||||||
|
|
||||||
|
export const exists = async (ctx: any) => {
|
||||||
|
const tenantId = ctx.request.params
|
||||||
|
ctx.body = {
|
||||||
|
exists: await doWithDB(
|
||||||
|
StaticDatabases.PLATFORM_INFO.name,
|
||||||
|
async (db: any) => {
|
||||||
|
let exists = false
|
||||||
|
try {
|
||||||
|
const tenantsDoc = await db.get(
|
||||||
|
StaticDatabases.PLATFORM_INFO.docs.tenants
|
||||||
|
)
|
||||||
|
if (tenantsDoc) {
|
||||||
|
exists = tenantsDoc.tenantIds.indexOf(tenantId) !== -1
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// if error it doesn't exist
|
||||||
|
}
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetch = async (ctx: any) => {
|
||||||
|
ctx.body = await doWithDB(
|
||||||
|
StaticDatabases.PLATFORM_INFO.name,
|
||||||
|
async (db: any) => {
|
||||||
|
let tenants = []
|
||||||
|
try {
|
||||||
|
const tenantsDoc = await db.get(
|
||||||
|
StaticDatabases.PLATFORM_INFO.docs.tenants
|
||||||
|
)
|
||||||
|
if (tenantsDoc) {
|
||||||
|
tenants = tenantsDoc.tenantIds
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// if error it doesn't exist
|
||||||
|
}
|
||||||
|
return tenants
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const _delete = async (ctx: any) => {
|
||||||
|
const tenantId = getTenantId()
|
||||||
|
|
||||||
|
if (ctx.params.tenantId !== tenantId) {
|
||||||
|
ctx.throw(403, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteTenant(tenantId)
|
||||||
|
await quotas.bustCache()
|
||||||
|
ctx.status = 204
|
||||||
|
} catch (err) {
|
||||||
|
ctx.log.error(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { _delete as delete }
|
|
@ -1,5 +1,11 @@
|
||||||
import { migrations, redis } from "@budibase/backend-core"
|
import { migrations, locks } from "@budibase/backend-core"
|
||||||
import { Migration, MigrationOptions, MigrationName } from "@budibase/types"
|
import {
|
||||||
|
Migration,
|
||||||
|
MigrationOptions,
|
||||||
|
MigrationName,
|
||||||
|
LockType,
|
||||||
|
LockName,
|
||||||
|
} from "@budibase/types"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
|
|
||||||
// migration functions
|
// migration functions
|
||||||
|
@ -42,33 +48,14 @@ export const migrate = async (options?: MigrationOptions) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrateWithLock = async (options?: MigrationOptions) => {
|
const migrateWithLock = async (options?: MigrationOptions) => {
|
||||||
// get a new lock client
|
await locks.doWithLock(
|
||||||
const redlock = await redis.clients.getMigrationsRedlock()
|
{
|
||||||
// lock for 15 minutes
|
type: LockType.TRY_ONCE,
|
||||||
const ttl = 1000 * 60 * 15
|
name: LockName.MIGRATIONS,
|
||||||
|
ttl: 1000 * 60 * 15, // auto expire the migration lock after 15 minutes
|
||||||
let migrationLock
|
},
|
||||||
|
async () => {
|
||||||
// acquire lock
|
await migrations.runMigrations(MIGRATIONS, options)
|
||||||
try {
|
|
||||||
migrationLock = await redlock.lock("migrations", ttl)
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e.name === "LockError") {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
// run migrations
|
|
||||||
try {
|
|
||||||
await migrations.runMigrations(MIGRATIONS, options)
|
|
||||||
} finally {
|
|
||||||
// release lock
|
|
||||||
try {
|
|
||||||
await migrationLock.unlock()
|
|
||||||
} catch (e) {
|
|
||||||
console.error("unable to release migration lock")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import env from "../../environment"
|
import env from "../../environment"
|
||||||
import { events, accounts, tenancy } from "@budibase/backend-core"
|
import { events, accounts, tenancy } from "@budibase/backend-core"
|
||||||
import { User, UserRoles, CloudAccount } from "@budibase/types"
|
import { User, UserRoles, CloudAccount } from "@budibase/types"
|
||||||
import { users as pro } from "@budibase/pro"
|
|
||||||
|
|
||||||
export const handleDeleteEvents = async (user: any) => {
|
export const handleDeleteEvents = async (user: any) => {
|
||||||
await events.user.deleted(user)
|
await events.user.deleted(user)
|
||||||
|
|
|
@ -291,12 +291,12 @@
|
||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
"@budibase/backend-core@2.0.24-alpha.3":
|
"@budibase/backend-core@2.0.30-alpha.1":
|
||||||
version "2.0.24-alpha.3"
|
version "2.0.30-alpha.1"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.0.24-alpha.3.tgz#49a92082d1ca6bed0eb82519a6c02d7d14ead751"
|
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.0.30-alpha.1.tgz#6e5cbc448cd819f67c93451d35c8793748380649"
|
||||||
integrity sha512-Zm/ddRDMzMuXCoEXZa0CzA/B1SnpQ+yjZCNAPH7Y4yYTIKfeE/DQ6COLlUixQxVMAUN1L5+GXML+px6fRigA5w==
|
integrity sha512-AuXk5IZBQ9zFTjTL2FsqqYdqqVjrSv19ozXSsb1w2z8Pyymz57WgzUiZ6uzHbHzUXNVD4Nei7cgfgSyCJ27Phg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/types" "2.0.24-alpha.3"
|
"@budibase/types" "2.0.30-alpha.1"
|
||||||
"@shopify/jest-koa-mocks" "5.0.1"
|
"@shopify/jest-koa-mocks" "5.0.1"
|
||||||
"@techpass/passport-openidconnect" "0.3.2"
|
"@techpass/passport-openidconnect" "0.3.2"
|
||||||
aws-sdk "2.1030.0"
|
aws-sdk "2.1030.0"
|
||||||
|
@ -327,21 +327,21 @@
|
||||||
uuid "8.3.2"
|
uuid "8.3.2"
|
||||||
zlib "1.0.5"
|
zlib "1.0.5"
|
||||||
|
|
||||||
"@budibase/pro@2.0.24-alpha.3":
|
"@budibase/pro@2.0.30-alpha.1":
|
||||||
version "2.0.24-alpha.3"
|
version "2.0.30-alpha.1"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.0.24-alpha.3.tgz#16605029663b07d0e3bcf0c08893878b542b6752"
|
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.0.30-alpha.1.tgz#12e4c2115d3e647d365b46e6b5df59746148d4ea"
|
||||||
integrity sha512-gBdqJzvtEAMJkhB2YTAyVYKoCANA25F1wT/K7kQtTOy5LrjDmcXah7CZoetfZQGbsKxUYEAZwJzxLSXmIxvWmQ==
|
integrity sha512-K/iy3Z3f9AVodxtuAk8yemSQ2R08tjoot4wogDCwsTMRlXxq/WChCcm9prkb2DzwOzQQSsJ3bLgL1p/9sGzagg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/backend-core" "2.0.24-alpha.3"
|
"@budibase/backend-core" "2.0.30-alpha.1"
|
||||||
"@budibase/types" "2.0.24-alpha.3"
|
"@budibase/types" "2.0.30-alpha.1"
|
||||||
"@koa/router" "8.0.8"
|
"@koa/router" "8.0.8"
|
||||||
joi "17.6.0"
|
joi "17.6.0"
|
||||||
node-fetch "^2.6.1"
|
node-fetch "^2.6.1"
|
||||||
|
|
||||||
"@budibase/types@2.0.24-alpha.3":
|
"@budibase/types@2.0.30-alpha.1":
|
||||||
version "2.0.24-alpha.3"
|
version "2.0.30-alpha.1"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.0.24-alpha.3.tgz#84eec5f991a2cfaf48d968b07a075b343f847289"
|
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.0.30-alpha.1.tgz#1320eb9c2996e371b47b2a845ce1b81e1b21b67c"
|
||||||
integrity sha512-f9PhtqzmqPI76ITXttuvxsvqMUJtkrDYf/4MHlI2v5ssNL9r0C/hbQEXllff3L3JqViEHWxkKFmfvfnDTV8rRQ==
|
integrity sha512-tw49Jwo7Dnhv9IuZq68DVvHsKcaVpggZ/1BYkQsdzUw+0yoIe1k8t+4XPbHqBP328mpp8oPx4rUQF3yFpJv41g==
|
||||||
|
|
||||||
"@cspotcode/source-map-consumer@0.8.0":
|
"@cspotcode/source-map-consumer@0.8.0":
|
||||||
version "0.8.0"
|
version "0.8.0"
|
||||||
|
|
|
@ -3,6 +3,11 @@ import { App } from "@budibase/types"
|
||||||
import { Response } from "node-fetch"
|
import { Response } from "node-fetch"
|
||||||
import InternalAPIClient from "./InternalAPIClient"
|
import InternalAPIClient from "./InternalAPIClient"
|
||||||
import FormData from "form-data"
|
import FormData from "form-data"
|
||||||
|
import { RouteConfig } from "../fixtures/types/routing"
|
||||||
|
import { AppPackageResponse } from "../fixtures/types/appPackage"
|
||||||
|
import { DeployConfig } from "../fixtures/types/deploy"
|
||||||
|
|
||||||
|
type messageResponse = { message: string }
|
||||||
|
|
||||||
export default class AppApi {
|
export default class AppApi {
|
||||||
api: InternalAPIClient
|
api: InternalAPIClient
|
||||||
|
@ -23,13 +28,13 @@ export default class AppApi {
|
||||||
return [response, Object.keys(json.routes).length > 0]
|
return [response, Object.keys(json.routes).length > 0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAppPackage(appId: string): Promise<[Response, any]> {
|
async getAppPackage(appId: string): Promise<[Response, AppPackageResponse]> {
|
||||||
const response = await this.api.get(`/applications/${appId}/appPackage`)
|
const response = await this.api.get(`/applications/${appId}/appPackage`)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
return [response, json]
|
return [response, json]
|
||||||
}
|
}
|
||||||
|
|
||||||
async publish(): Promise<[Response, string]> {
|
async publish(): Promise<[Response, DeployConfig]> {
|
||||||
const response = await this.api.post("/deploy")
|
const response = await this.api.post("/deploy")
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
return [response, json]
|
return [response, json]
|
||||||
|
@ -47,7 +52,49 @@ export default class AppApi {
|
||||||
return [response, json.data]
|
return [response, json.data]
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRoutes(): Promise<[Response, any]> {
|
async sync(appId: string): Promise<[Response, messageResponse]> {
|
||||||
|
const response = await this.api.post(`/applications/${appId}/sync`)
|
||||||
|
const json = await response.json()
|
||||||
|
return [response, json]
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateClient(
|
||||||
|
appId: string,
|
||||||
|
body: any
|
||||||
|
): Promise<[Response, Application]> {
|
||||||
|
const response = await this.api.put(
|
||||||
|
`/applications/${appId}/client/update`,
|
||||||
|
{ body }
|
||||||
|
)
|
||||||
|
const json = await response.json()
|
||||||
|
return [response, json]
|
||||||
|
}
|
||||||
|
|
||||||
|
async revert(appId: string): Promise<[Response, messageResponse]> {
|
||||||
|
const response = await this.api.post(`/dev/${appId}/revert`)
|
||||||
|
const json = await response.json()
|
||||||
|
return [response, json]
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(appId: string): Promise<[Response, any]> {
|
||||||
|
const response = await this.api.del(`/applications/${appId}`)
|
||||||
|
const json = await response.json()
|
||||||
|
return [response, json]
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(appId: string, body: any): Promise<[Response, Application]> {
|
||||||
|
const response = await this.api.put(`/applications/${appId}`, { body })
|
||||||
|
const json = await response.json()
|
||||||
|
return [response, json]
|
||||||
|
}
|
||||||
|
|
||||||
|
async addScreentoApp(body: any): Promise<[Response, Application]> {
|
||||||
|
const response = await this.api.post(`/screens`, { body })
|
||||||
|
const json = await response.json()
|
||||||
|
return [response, json]
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRoutes(): Promise<[Response, RouteConfig]> {
|
||||||
const response = await this.api.get(`/routing`)
|
const response = await this.api.get(`/routing`)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
return [response, json]
|
return [response, json]
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Application } from "@budibase/server/api/controllers/public/mapping/types"
|
||||||
|
import { Layout } from "@budibase/types"
|
||||||
|
import { Screen } from "@budibase/types"
|
||||||
|
// Create type for getAppPackage response
|
||||||
|
export interface AppPackageResponse {
|
||||||
|
application: Partial<Application>
|
||||||
|
layout: Layout
|
||||||
|
screens: Screen[]
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface DeployConfig {
|
||||||
|
appUrl: string
|
||||||
|
status: string
|
||||||
|
_id: string
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
export interface RouteConfig {
|
||||||
|
routes: Record<string, Route>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Route {
|
||||||
|
subpaths: Record<string, Subpath>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subpath {
|
||||||
|
screens: ScreenRouteConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScreenRouteConfig {
|
||||||
|
BASIC?: string
|
||||||
|
POWER?: string
|
||||||
|
ADMIN?: string
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import { db } from "@budibase/backend-core"
|
||||||
import InternalAPIClient from "../../../config/internal-api/TestConfiguration/InternalAPIClient"
|
import InternalAPIClient from "../../../config/internal-api/TestConfiguration/InternalAPIClient"
|
||||||
import generateApp from "../../../config/internal-api/fixtures/applications"
|
import generateApp from "../../../config/internal-api/fixtures/applications"
|
||||||
import generator from "../../../config/generator"
|
import generator from "../../../config/generator"
|
||||||
|
import generateScreen from "../../../config/internal-api/fixtures/screens"
|
||||||
|
|
||||||
describe("Internal API - /applications endpoints", () => {
|
describe("Internal API - /applications endpoints", () => {
|
||||||
const api = new InternalAPIClient()
|
const api = new InternalAPIClient()
|
||||||
|
@ -84,4 +85,111 @@ describe("Internal API - /applications endpoints", () => {
|
||||||
await config.applications.canRender()
|
await config.applications.canRender()
|
||||||
expect(publishedAppRenders).toBe(true)
|
expect(publishedAppRenders).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("POST - Sync application before deployment", async () => {
|
||||||
|
const [response, app] = await config.applications.create(generateApp())
|
||||||
|
expect(response).toHaveStatusCode(200)
|
||||||
|
expect(app.appId).toBeDefined()
|
||||||
|
config.applications.api.appId = app.appId
|
||||||
|
|
||||||
|
const [syncResponse, sync] = await config.applications.sync(
|
||||||
|
<string>app.appId
|
||||||
|
)
|
||||||
|
expect(syncResponse).toHaveStatusCode(200)
|
||||||
|
expect(sync).toEqual({
|
||||||
|
message: "App sync not required, app not deployed.",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("POST - Sync application after deployment", async () => {
|
||||||
|
const [response, app] = await config.applications.create(generateApp())
|
||||||
|
expect(response).toHaveStatusCode(200)
|
||||||
|
expect(app.appId).toBeDefined()
|
||||||
|
config.applications.api.appId = app.appId
|
||||||
|
|
||||||
|
// publish app
|
||||||
|
await config.applications.publish()
|
||||||
|
|
||||||
|
const [syncResponse, sync] = await config.applications.sync(
|
||||||
|
<string>app.appId
|
||||||
|
)
|
||||||
|
expect(syncResponse).toHaveStatusCode(200)
|
||||||
|
expect(sync).toEqual({
|
||||||
|
message: "App sync completed successfully.",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("PUT - Update an application", async () => {
|
||||||
|
const [response, app] = await config.applications.create(generateApp())
|
||||||
|
expect(response).toHaveStatusCode(200)
|
||||||
|
expect(app.appId).toBeDefined()
|
||||||
|
config.applications.api.appId = app.appId
|
||||||
|
|
||||||
|
const [updateResponse, updatedApp] = await config.applications.update(
|
||||||
|
<string>app.appId,
|
||||||
|
{
|
||||||
|
name: generator.word(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(updateResponse).toHaveStatusCode(200)
|
||||||
|
expect(updatedApp.name).not.toEqual(app.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("POST - Revert Changes without changes", async () => {
|
||||||
|
const [response, app] = await config.applications.create(generateApp())
|
||||||
|
expect(response).toHaveStatusCode(200)
|
||||||
|
expect(app.appId).toBeDefined()
|
||||||
|
config.applications.api.appId = app.appId
|
||||||
|
|
||||||
|
const [revertResponse, revert] = await config.applications.revert(
|
||||||
|
<string>app.appId
|
||||||
|
)
|
||||||
|
expect(revertResponse).toHaveStatusCode(400)
|
||||||
|
expect(revert).toEqual({
|
||||||
|
message: "App has not yet been deployed",
|
||||||
|
status: 400,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("POST - Revert Changes", async () => {
|
||||||
|
const [response, app] = await config.applications.create(generateApp())
|
||||||
|
expect(response).toHaveStatusCode(200)
|
||||||
|
expect(app.appId).toBeDefined()
|
||||||
|
config.applications.api.appId = app.appId
|
||||||
|
|
||||||
|
// publish app
|
||||||
|
const [publishResponse, publish] = await config.applications.publish()
|
||||||
|
expect(publishResponse).toHaveStatusCode(200)
|
||||||
|
expect(publish.status).toEqual("SUCCESS")
|
||||||
|
|
||||||
|
// Change/add component to the app
|
||||||
|
const [screenResponse, screen] = await config.applications.addScreentoApp(
|
||||||
|
generateScreen()
|
||||||
|
)
|
||||||
|
expect(screenResponse).toHaveStatusCode(200)
|
||||||
|
expect(screen._id).toBeDefined()
|
||||||
|
|
||||||
|
// // Revert the app to published state
|
||||||
|
const [revertResponse, revert] = await config.applications.revert(
|
||||||
|
<string>app.appId
|
||||||
|
)
|
||||||
|
expect(revertResponse).toHaveStatusCode(200)
|
||||||
|
expect(revert).toEqual({
|
||||||
|
message: "Reverted changes successfully.",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check screen is removed
|
||||||
|
const [routesResponse, routes] = await config.applications.getRoutes()
|
||||||
|
expect(routesResponse).toHaveStatusCode(200)
|
||||||
|
expect(routes.routes["/test"]).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("DELETE - Delete an application", async () => {
|
||||||
|
const [response, app] = await config.applications.create(generateApp())
|
||||||
|
expect(response).toHaveStatusCode(200)
|
||||||
|
expect(app.appId).toBeDefined()
|
||||||
|
|
||||||
|
const [deleteResponse] = await config.applications.delete(<string>app.appId)
|
||||||
|
expect(deleteResponse).toHaveStatusCode(200)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue