fixes for google sheets, admin checklist, and deleting an app from API (#8846)

* fixes for google sheets, admin checklist, and deleting an app from API

* code review

* splitting unpublish endpoint, moving deploy endpoint to applications controller. Still to do public API work and move deployment controller into application controller

* updating REST method for unpublish in API test

* unpublish and publish endpoint on public API, delete endpoint unpublishes and deletes app

* removing skip_setup from prodAppDb call

* removing commented code

* unit tests and open API spec updates

* unpublish, publish unit tests - delete still in progress

* remove line updating app name in API test

* unit tests

* v2.1.46

* Update pro version to 2.1.46

* v2.2.0

* Update pro version to 2.2.0

* Fix for budibase plugin skeleton, which utilises the old import style.

* Fix side nav styles

* v2.2.1

* Update pro version to 2.2.1

* using dist folder to allow importing constants for openAPI specs

* v2.2.2

* Update pro version to 2.2.2

* Fix for user enrichment call (updating to @budibase/nano fork) (#9038)

* Fix for #9029 - this should fix the issue users have been experiencing with user enrichment calls in apps, essentially it utilises a fork of the nano library we use to interact with CouchDB, which has been updated to use a POST request rather than a GET request as it supports a larger set of data being sent as query parameters.

* Incrementing Nano version to attempt to fix yarn registry issues.

* v2.2.3

* Update pro version to 2.2.3

* Fix SQL table `_id` filtering (#9030)

* Re-add support for filtering on _id using external SQL tables and fix filter key prefixes not working with _id field

* Remove like operator from internal tables and only allow basic operators on SQL table _id column

* Update data section filtering to respect new rules

* Update automation section filtering to respect new rules

* Update dynamic filter component to respect new rules

* v2.2.4

* Update pro version to 2.2.4

* lock changes (#9047)

* v2.2.5

* Update pro version to 2.2.5

* Make looping arrow point in right direction (#9053)

* v2.2.6

* Update pro version to 2.2.6

* Types/attaching license to account (#9065)

* adding license type to account

* removing planDuration

* v2.2.7

* Update pro version to 2.2.7

* Environment variable type coercion fix (#9074)

* Environment variable type coercion fix

* Update .gitignore

* v2.2.8

* Update pro version to 2.2.8

* tests passing

* all tests passing, updates to public API response

* update unpublish call to return 204, openAPI spec and unit

* fixing API tests

Co-authored-by: Budibase Release Bot <>
Co-authored-by: mike12345567 <me@michaeldrury.co.uk>
Co-authored-by: Andrew Kingston <andrew@kingston.dev>
Co-authored-by: melohagan <101575380+melohagan@users.noreply.github.com>
Co-authored-by: Rory Powell <rory.codes@gmail.com>
This commit is contained in:
Martin McKeaveney 2022-12-19 13:18:00 +00:00 committed by GitHub
parent 5e95e6060e
commit 84ab7862d1
48 changed files with 538 additions and 303 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@ builder/*
packages/server/runtime_apps/
.idea/
bb-airgapped.tar.gz
*.iml
# Logs
logs

View File

@ -15,4 +15,4 @@
]
}
}
}
}

View File

@ -79,4 +79,4 @@
"typescript": "4.7.3"
},
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
}
}

View File

@ -13,6 +13,18 @@ const getClient = async (type: LockType): Promise<Redlock> => {
}
return noRetryRedlock
}
case LockType.DEFAULT: {
if (!noRetryRedlock) {
noRetryRedlock = await newRedlock(OPTIONS.DEFAULT)
}
return noRetryRedlock
}
case LockType.DELAY_500: {
if (!noRetryRedlock) {
noRetryRedlock = await newRedlock(OPTIONS.DELAY_500)
}
return noRetryRedlock
}
default: {
throw new Error(`Could not get redlock client: ${type}`)
}
@ -41,6 +53,9 @@ export const OPTIONS = {
// see https://www.awsarchitectureblog.com/2015/03/backoff.html
retryJitter: 100, // time in ms
},
DELAY_500: {
retryDelay: 500,
},
}
export const newRedlock = async (opts: Options = {}) => {
@ -55,19 +70,17 @@ export const doWithLock = async (opts: LockOptions, task: any) => {
let lock
try {
// aquire lock
let name: string
if (opts.systemLock) {
name = opts.name
} else {
name = `${tenancy.getTenantId()}_${opts.name}`
}
let name: string = `lock:${tenancy.getTenantId()}_${opts.name}`
if (opts.nameSuffix) {
name = name + `_${opts.nameSuffix}`
}
lock = await redlock.lock(name, opts.ttl)
// perform locked task
return task()
// need to await to ensure completion before unlocking
const result = await task()
return result
} catch (e: any) {
console.log("lock error")
// lock limit exceeded
if (e.name === "LockError") {
if (opts.type === LockType.TRY_ONCE) {

View File

@ -89,4 +89,4 @@
"loader-utils": "1.4.1"
},
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
}
}

View File

@ -123,4 +123,4 @@
"vite": "^3.0.8"
},
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
}
}

View File

@ -180,7 +180,7 @@
onSelect(block)
}}
>
<Icon name={showLooping ? "ChevronDown" : "ChevronUp"} />
<Icon name={showLooping ? "ChevronUp" : "ChevronDown"} />
</div>
</div>
</div>

View File

@ -12,7 +12,6 @@
import { ProgressCircle } from "@budibase/bbui"
import CopyInput from "components/common/inputs/CopyInput.svelte"
let feedbackModal
let publishModal
let asyncModal
let publishCompleteModal
@ -23,13 +22,13 @@
export let onOk
async function deployApp() {
async function publishApp() {
try {
//In Progress
asyncModal.show()
publishModal.hide()
published = await API.deployAppChanges()
published = await API.publishAppChanges($store.appId)
if (typeof onOk === "function") {
await onOk()
@ -56,20 +55,11 @@
</script>
<Button cta on:click={publishModal.show}>Publish</Button>
<Modal bind:this={feedbackModal}>
<ModalContent
title="Enjoying Budibase?"
size="L"
showConfirmButton={false}
showCancelButton={false}
/>
</Modal>
<Modal bind:this={publishModal}>
<ModalContent
title="Publish to Production"
confirmText="Publish"
onConfirm={deployApp}
onConfirm={publishApp}
dataCy={"deploy-app-modal"}
>
<span

View File

@ -186,7 +186,9 @@
<span>{$organisation?.company || "Budibase"}</span>
</div>
<div class="onboarding">
<ConfigChecklist />
{#if $auth.user?.admin?.global}
<ConfigChecklist />
{/if}
</div>
</div>
<div class="menu">

View File

@ -54,4 +54,4 @@
"eslint": "^7.20.0",
"renamer": "^4.0.0"
}
}
}

View File

@ -63,4 +63,4 @@
"loader-utils": "1.4.1"
},
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
}
}

View File

@ -10,4 +10,4 @@
"lodash": "^4.17.21",
"svelte": "^3.46.2"
}
}
}

View File

@ -22,11 +22,11 @@ export const buildAppEndpoints = API => ({
},
/**
* Deploys the current app.
* Publishes the current app.
*/
deployAppChanges: async () => {
publishAppChanges: async appId => {
return await API.post({
url: "/api/deploy",
url: `/api/applications/${appId}/publish`,
})
},
@ -98,8 +98,8 @@ export const buildAppEndpoints = API => ({
* @param appId the production ID of the app to unpublish
*/
unpublishApp: async appId => {
return await API.delete({
url: `/api/applications/${appId}?unpublish=1`,
return await API.post({
url: `/api/applications/${appId}/unpublish`,
})
},

View File

@ -20,4 +20,4 @@
"rollup-plugin-polyfill-node": "^0.8.0",
"rollup-plugin-terser": "^7.0.2"
}
}
}

View File

@ -168,4 +168,4 @@
"oracledb": "5.3.0"
},
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
}
}

View File

@ -567,6 +567,40 @@
"data"
]
},
"deploymentOutput": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"_id": {
"description": "The ID of the app.",
"type": "string"
},
"status": {
"description": "Status of the deployment, whether it succeeded or failed",
"type": "string",
"enum": [
"SUCCESS",
"FAILURE"
]
},
"appUrl": {
"description": "The URL of the published app",
"type": "string"
}
},
"required": [
"_id",
"status",
"appUrl"
]
}
},
"required": [
"data"
]
},
"row": {
"description": "The row to be created/updated, based on the table schema.",
"type": "object",
@ -1933,6 +1967,56 @@
}
}
},
"/applications/{appId}/unpublish": {
"post": {
"operationId": "unpublish",
"summary": "Unpublish an application",
"tags": [
"applications"
],
"parameters": [
{
"$ref": "#/components/parameters/appIdUrl"
}
],
"responses": {
"204": {
"description": "The app was published successfully."
}
}
}
},
"/applications/{appId}/publish": {
"post": {
"operationId": "publish",
"summary": "Unpublish an application",
"tags": [
"applications"
],
"parameters": [
{
"$ref": "#/components/parameters/appIdUrl"
}
],
"responses": {
"200": {
"description": "Returns the deployment object.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/deploymentOutput"
},
"examples": {
"deployment": {
"$ref": "#/components/examples/deploymentOutput"
}
}
}
}
}
}
}
},
"/applications/search": {
"post": {
"operationId": "search",

View File

@ -411,6 +411,30 @@ components:
- version
required:
- data
deploymentOutput:
type: object
properties:
data:
type: object
properties:
_id:
description: The ID of the app.
type: string
status:
description: Status of the deployment, whether it succeeded or failed
type: string
enum:
- SUCCESS
- FAILURE
appUrl:
description: The URL of the published app
type: string
required:
- _id
- status
- appUrl
required:
- data
row:
description: The row to be created/updated, based on the table schema.
type: object
@ -1453,6 +1477,35 @@ paths:
examples:
application:
$ref: "#/components/examples/application"
"/applications/{appId}/unpublish":
post:
operationId: unpublish
summary: Unpublish an application
tags:
- applications
parameters:
- $ref: "#/components/parameters/appIdUrl"
responses:
"204":
description: The app was published successfully.
"/applications/{appId}/publish":
post:
operationId: publish
summary: Unpublish an application
tags:
- applications
parameters:
- $ref: "#/components/parameters/appIdUrl"
responses:
"200":
description: Returns the deployment object.
content:
application/json:
schema:
$ref: "#/components/schemas/deploymentOutput"
examples:
deployment:
$ref: "#/components/examples/deploymentOutput"
/applications/search:
post:
operationId: search

View File

@ -80,6 +80,22 @@ const applicationOutputSchema = object(
}
)
const deploymentOutputSchema = object({
_id: {
description: "The ID of the app.",
type: "string",
},
status: {
description: "Status of the deployment, whether it succeeded or failed",
type: "string",
enum: ["SUCCESS", "FAILURE"],
},
appUrl: {
description: "The URL of the published app",
type: "string",
},
})
module.exports = new Resource()
.setExamples({
application: {
@ -104,4 +120,7 @@ module.exports = new Resource()
items: applicationOutputSchema,
},
}),
deploymentOutput: object({
data: deploymentOutputSchema,
}),
})

View File

@ -50,6 +50,7 @@ import {
} from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
import sdk from "../../sdk"
import { getDB } from "@budibase/backend-core/src/db"
// utility function, need to do away with this
async function getLayouts() {
@ -464,41 +465,47 @@ export async function revertClient(ctx: BBContext) {
ctx.body = app
}
async function destroyApp(ctx: BBContext) {
const unpublishApp = async (ctx: any) => {
let appId = ctx.params.appId
let isUnpublish = ctx.query && ctx.query.unpublish
appId = dbCore.getProdAppID(appId)
if (isUnpublish) {
appId = dbCore.getProdAppID(appId)
const devAppId = dbCore.getDevAppID(appId)
// sync before removing the published app
await sdk.applications.syncApp(devAppId)
}
const db = isUnpublish ? context.getProdAppDB() : context.getAppDB()
const app = await db.get(DocumentType.APP_METADATA)
const db = context.getProdAppDB()
const result = await db.destroy()
if (isUnpublish) {
await events.app.unpublished(app)
} else {
await quotas.removeApp()
await events.app.deleted(app)
await events.app.unpublished({ appId } as App)
// automations only in production
await cleanupAutomations(appId)
await cache.app.invalidateAppMetadata(appId)
return result
}
async function destroyApp(ctx: BBContext) {
let appId = ctx.params.appId
appId = dbCore.getProdAppID(appId)
const devAppId = dbCore.getDevAppID(appId)
// check if we need to unpublish first
if (await dbCore.dbExists(appId)) {
// app is deployed, run through unpublish flow
await sdk.applications.syncApp(devAppId)
await unpublishApp(ctx)
}
/* istanbul ignore next */
if (!env.isTest() && !isUnpublish) {
const db = dbCore.getDB(devAppId)
// standard app deletion flow
const app = await db.get(DocumentType.APP_METADATA)
const result = await db.destroy()
await quotas.removeApp()
await events.app.deleted(app)
if (!env.isTest()) {
await deleteApp(appId)
}
// automations only in production
if (isUnpublish) {
await cleanupAutomations(appId)
}
// remove app role when the dev app is deleted (no trace of app anymore)
else {
await removeAppFromUserRoles(ctx, appId)
}
await cache.app.invalidateAppMetadata(appId)
await removeAppFromUserRoles(ctx, appId)
await cache.app.invalidateAppMetadata(devAppId)
return result
}
@ -523,6 +530,21 @@ export async function destroy(ctx: BBContext) {
ctx.body = result
}
export const unpublish = async (ctx: BBContext) => {
const prodAppId = dbCore.getProdAppID(ctx.params.appId)
const dbExists = await dbCore.dbExists(prodAppId)
// check app has been published
if (!dbExists) {
return ctx.throw(400, "App has not been published.")
}
await preDestroyApp(ctx)
await unpublishApp(ctx)
await postDestroyApp(ctx)
ctx.status = 204
}
export async function sync(ctx: BBContext) {
const appId = ctx.params.appId
try {

View File

@ -82,7 +82,7 @@ export async function importApps(ctx: Ctx) {
"Import file is required and environment must be fresh to import apps."
)
}
const file = ctx.request.files.importFile
const file = ctx.request.files.importFile as any
if (Array.isArray(file)) {
ctx.throw(400, "Single file is required")
}

View File

@ -9,6 +9,7 @@ export default class Deployment {
verification: any
status?: string
err?: any
appUrl?: string
constructor(id = null) {
this._id = id || newid()

View File

@ -94,7 +94,44 @@ async function initDeployedApp(prodAppId: any) {
})
}
async function deployApp(deployment: any, userId: string) {
export async function fetchDeployments(ctx: any) {
try {
const db = context.getAppDB()
const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
const { updated, deployments } = await checkAllDeployments(deploymentDoc)
if (updated) {
await db.put(deployments)
}
ctx.body = Object.values(deployments.history).reverse()
} catch (err) {
ctx.body = []
}
}
export async function deploymentProgress(ctx: any) {
try {
const db = context.getAppDB()
const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
ctx.body = deploymentDoc[ctx.params.deploymentId]
} catch (err) {
ctx.throw(
500,
`Error fetching data for deployment ${ctx.params.deploymentId}`
)
}
}
export const publishApp = async function (ctx: any) {
let deployment = new Deployment()
console.log("Deployment object created")
deployment.setStatus(DeploymentStatus.PENDING)
console.log("Deployment object set to pending")
deployment = await storeDeploymentHistory(deployment)
console.log("Stored deployment history")
console.log("Deploying app...")
let app
let replication
try {
const appId = context.getAppId()!
@ -108,7 +145,7 @@ async function deployApp(deployment: any, userId: string) {
productionAppId,
AppBackupTrigger.PUBLISH,
{
createdBy: userId,
createdBy: ctx.user._id,
}
)
}
@ -147,7 +184,7 @@ async function deployApp(deployment: any, userId: string) {
console.log("Deployed app initialised, setting deployment to successful")
deployment.setStatus(DeploymentStatus.SUCCESS)
await storeDeploymentHistory(deployment)
return appDoc
app = appDoc
} catch (err: any) {
deployment.setStatus(DeploymentStatus.FAILURE, err.message)
await storeDeploymentHistory(deployment)
@ -160,62 +197,7 @@ async function deployApp(deployment: any, userId: string) {
await replication.close()
}
}
}
export async function fetchDeployments(ctx: any) {
try {
const db = context.getAppDB()
const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
const { updated, deployments } = await checkAllDeployments(deploymentDoc)
if (updated) {
await db.put(deployments)
}
ctx.body = Object.values(deployments.history).reverse()
} catch (err) {
ctx.body = []
}
}
export async function deploymentProgress(ctx: any) {
try {
const db = context.getAppDB()
const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
ctx.body = deploymentDoc[ctx.params.deploymentId]
} catch (err) {
ctx.throw(
500,
`Error fetching data for deployment ${ctx.params.deploymentId}`
)
}
}
const isFirstDeploy = async () => {
try {
const db = context.getProdAppDB()
await db.get(DocumentType.APP_METADATA)
} catch (e: any) {
if (e.status === 404) {
return true
}
throw e
}
return false
}
const _deployApp = async function (ctx: any) {
let deployment = new Deployment()
console.log("Deployment object created")
deployment.setStatus(DeploymentStatus.PENDING)
console.log("Deployment object set to pending")
deployment = await storeDeploymentHistory(deployment)
console.log("Stored deployment history")
console.log("Deploying app...")
let app = await deployApp(deployment, ctx.user._id)
await events.app.published(app)
ctx.body = deployment
}
export { _deployApp as deployApp }

View File

@ -1,6 +1,7 @@
import { db as dbCore, context } from "@budibase/backend-core"
import { search as stringSearch, addRev } from "./utils"
import * as controller from "../application"
import * as deployController from "../deploy"
import { Application } from "../../../definitions/common"
function fixAppID(app: Application, params: any) {
@ -74,10 +75,26 @@ export async function destroy(ctx: any, next: any) {
})
}
export async function unpublish(ctx: any, next: any) {
await context.doInAppContext(ctx.params.appId, async () => {
await controller.unpublish(ctx)
await next()
})
}
export async function publish(ctx: any, next: any) {
await context.doInAppContext(ctx.params.appId, async () => {
await deployController.publishApp(ctx)
await next()
})
}
export default {
create,
update,
read,
destroy,
search,
publish,
unpublish,
}

View File

@ -1,5 +1,6 @@
import Router from "@koa/router"
import * as controller from "../controllers/application"
import * as deploymentController from "../controllers/deploy"
import authorized from "../../middleware/authorized"
import { permissions } from "@budibase/backend-core"
import { applicationValidator } from "./utils/validators"
@ -37,6 +38,16 @@ router
authorized(permissions.BUILDER),
controller.revertClient
)
.post(
"/api/applications/:appId/publish",
authorized(permissions.BUILDER),
deploymentController.publishApp
)
.post(
"/api/applications/:appId/unpublish",
authorized(permissions.BUILDER),
controller.unpublish
)
.delete(
"/api/applications/:appId",
authorized(permissions.BUILDER),

View File

@ -16,6 +16,5 @@ router
authorized(permissions.BUILDER),
controller.deploymentProgress
)
.post("/api/deploy", authorized(permissions.BUILDER), controller.deployApp)
export = router

View File

@ -1,6 +1,7 @@
import controller from "../../controllers/public/applications"
import Endpoint from "./utils/Endpoint"
const { nameValidator, applicationValidator } = require("../utils/validators")
import { db } from "@budibase/backend-core"
const read = [],
write = []
@ -94,6 +95,49 @@ write.push(
*/
write.push(new Endpoint("delete", "/applications/:appId", controller.destroy))
/**
* @openapi
* /applications/{appId}/unpublish:
* post:
* operationId: unpublish
* summary: Unpublish an application
* tags:
* - applications
* parameters:
* - $ref: '#/components/parameters/appIdUrl'
* responses:
* 204:
* description: The app was published successfully.
*/
write.push(
new Endpoint("post", "/applications/:appId/unpublish", controller.unpublish)
)
/**
* @openapi
* /applications/{appId}/publish:
* post:
* operationId: publish
* summary: Unpublish an application
* tags:
* - applications
* parameters:
* - $ref: '#/components/parameters/appIdUrl'
* responses:
* 200:
* description: Returns the deployment object.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/deploymentOutput'
* examples:
* deployment:
* $ref: '#/components/examples/deploymentOutput'
*/
write.push(
new Endpoint("post", "/applications/:appId/publish", controller.publish)
)
/**
* @openapi
* /applications/{appId}:

View File

@ -54,9 +54,13 @@ function processQueries(ctx: any) {
}
export default async (ctx: any, next: any) => {
if (!ctx.body) {
return await next()
}
let urlParts = ctx.url.split("/")
urlParts = urlParts.slice(4, urlParts.length)
let body = {}
switch (urlParts[0]) {
case Resources.APPLICATION:
body = processApplications(ctx)

View File

@ -11,7 +11,6 @@ jest.mock("../../../utilities/redis", () => ({
checkDebounce: jest.fn(),
shutdown: jest.fn(),
}))
import { clearAllApps, checkBuilderEndpoint } from "./utilities/TestFunctions"
import * as setup from "./utilities"
import { AppStatus } from "../../../db/utils"
@ -160,33 +159,30 @@ describe("/applications", () => {
})
})
describe("delete", () => {
it("should delete app", async () => {
await config.createApp("to-delete")
describe("publish", () => {
it("should publish app with dev app ID", async () => {
const appId = config.getAppId()
await request
.delete(`/api/applications/${appId}`)
.post(`/api/applications/${appId}/publish`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(events.app.deleted).toBeCalledTimes(1)
expect(events.app.published).toBeCalledTimes(1)
})
it("should unpublish app", async () => {
await config.createApp("to-unpublish")
it("should publish app with prod app ID", async () => {
const appId = config.getProdAppId()
await request
.delete(`/api/applications/${appId}?unpublish=true`)
.post(`/api/applications/${appId}/publish`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(events.app.unpublished).toBeCalledTimes(1)
expect(events.app.published).toBeCalledTimes(1)
})
})
describe("manage client library version", () => {
it("should be able to update the app client library version", async () => {
console.log(config.getAppId())
await request
.post(`/api/applications/${config.getAppId()}/client/update`)
.set(config.defaultHeaders())
@ -194,6 +190,7 @@ describe("/applications", () => {
.expect(200)
expect(events.app.versionUpdated).toBeCalledTimes(1)
})
it("should be able to revert the app client library version", async () => {
// We need to first update the version so that we can then revert
await request
@ -267,4 +264,50 @@ describe("/applications", () => {
env._set("DISABLE_AUTO_PROD_APP_SYNC", false)
})
})
describe("unpublish", () => {
it("should unpublish app with dev app ID", async () => {
const appId = config.getAppId()
await request
.post(`/api/applications/${appId}/unpublish`)
.set(config.defaultHeaders())
.expect(204)
expect(events.app.unpublished).toBeCalledTimes(1)
})
it("should unpublish app with prod app ID", async () => {
const appId = config.getProdAppId()
await request
.post(`/api/applications/${appId}/unpublish`)
.set(config.defaultHeaders())
.expect(204)
expect(events.app.unpublished).toBeCalledTimes(1)
})
})
describe("delete", () => {
it("should delete published app and dev apps with dev app ID", async () => {
await config.createApp("to-delete")
const appId = config.getAppId()
await request
.delete(`/api/applications/${appId}`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(events.app.deleted).toBeCalledTimes(1)
expect(events.app.unpublished).toBeCalledTimes(1)
})
it("should delete published app and dev app with prod app ID", async () => {
await config.createApp("to-delete")
const appId = config.getProdAppId()
await request
.delete(`/api/applications/${appId}`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(events.app.deleted).toBeCalledTimes(1)
expect(events.app.unpublished).toBeCalledTimes(1)
})
})
})

View File

@ -23,14 +23,13 @@ describe("/cloud", () => {
// first we need to delete any existing apps on the system so it looks clean otherwise the
// import will not run
await request
.delete(
.post(
`/api/applications/${dbCore.getProdAppID(
config.getAppId()
)}?unpublish=true`
)}/unpublish`
)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
.expect(204)
await request
.delete(`/api/applications/${config.getAppId()}`)
.set(config.defaultHeaders())

View File

@ -1,25 +0,0 @@
import * as setup from "./utilities"
import { events } from "@budibase/backend-core"
describe("/deployments", () => {
let request = setup.getRequest()
let config = setup.getConfig()
afterAll(setup.afterAll)
beforeEach(async () => {
await config.init()
jest.clearAllMocks()
})
describe("deploy", () => {
it("should deploy the application", async () => {
await request
.post(`/api/deploy`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect((events.app.published as jest.Mock).mock.calls.length).toBe(1)
})
})
})

View File

@ -92,7 +92,7 @@ describe("/permission", () => {
describe("check public user allowed", () => {
it("should be able to read the row", async () => {
// replicate changes before checking permissions
await config.deploy()
await config.publish()
const res = await request
.get(`/api/${table._id}/rows`)

View File

@ -25,7 +25,7 @@ describe("/routing", () => {
screen2.routing.roleId = BUILTIN_ROLE_IDS.POWER
screen2.routing.route = route
screen2 = await config.createScreen(screen2)
await config.deploy()
await config.publish()
})
describe("fetch", () => {

View File

@ -113,7 +113,7 @@ describe("/webhooks", () => {
describe("trigger", () => {
it("should allow triggering from public", async () => {
// replicate changes before checking webhook
await config.deploy()
await config.publish()
const res = await request
.post(`/api/webhooks/trigger/${config.prodAppId}/${webhook._id}`)

View File

@ -102,6 +102,16 @@ export interface components {
lockedBy?: { [key: string]: unknown };
};
};
deploymentOutput: {
data: {
/** @description The ID of the deployment. */
_id: string;
/** @description The status of the deployment. */
status: "SUCCESS" | "FAILURE";
/** @description The URL by which the published app is accessed. */
appUrl?: string;
}
};
applicationSearch: {
data: {
/** @description The name of the app. */

View File

@ -107,7 +107,7 @@ const environment = {
}
// threading can cause memory issues with node-ts in development
if (isDev() && module.exports.DISABLE_THREADING == null) {
if (isDev() && environment.DISABLE_THREADING == null) {
environment._set("DISABLE_THREADING", "1")
}

View File

@ -284,7 +284,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
async createTable(name?: string) {
try {
await this.connect()
return await this.client.addSheet({ title: name })
return await this.client.addSheet({ title: name, headerValues: ["test"] })
} catch (err) {
console.error("Error creating new table in google sheets", err)
throw err

View File

@ -360,7 +360,6 @@ class TestConfiguration {
}
// APP
async createApp(appName: string) {
// create dev app
// clear any old app
@ -373,7 +372,7 @@ class TestConfiguration {
await context.updateAppId(this.appId)
// create production app
this.prodApp = await this.deploy()
this.prodApp = await this.publish()
this.allApps.push(this.prodApp)
this.allApps.push(this.app)
@ -381,8 +380,8 @@ class TestConfiguration {
return this.app
}
async deploy() {
await this._req(null, null, controllers.deploy.deployApp)
async publish() {
await this._req(null, null, controllers.deploy.publishApp)
// @ts-ignore
const prodAppId = this.getAppId().replace("_dev", "")
this.prodAppId = prodAppId
@ -393,6 +392,17 @@ class TestConfiguration {
})
}
async unpublish() {
const response = await this._req(
null,
{ appId: this.appId },
controllers.app.unpublish
)
this.prodAppId = null
this.prodApp = null
return response
}
// TABLE
async updateTable(config?: any) {

View File

@ -47,4 +47,4 @@
"typescript": "4.7.3"
},
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
}
}

View File

@ -14,12 +14,12 @@
"jest": {},
"devDependencies": {
"@budibase/nano": "10.1.1",
"@types/formidable": "^1.0.31",
"@types/json5": "2.2.0",
"@types/koa": "2.13.4",
"@types/node": "14.18.20",
"@types/pouchdb": "6.4.0",
"koa-body": "4.2.0",
"rimraf": "3.0.2",
"typescript": "4.7.3"
}
}
}

View File

@ -1,8 +1,10 @@
import {
Feature,
Hosting,
License,
MonthlyQuotaName,
PlanType,
PriceDuration,
Quotas,
StaticQuotaName,
} from "../../sdk"
@ -46,6 +48,7 @@ export interface Account extends CreateAccount {
tier: string // deprecated
planType?: PlanType
planTier?: number
license?: License
stripeCustomerId?: string
licenseKey?: string
licenseKeyActivatedAt?: number

View File

@ -4,11 +4,14 @@ export enum LockType {
* No retries will take place and no error will be thrown.
*/
TRY_ONCE = "try_once",
DEFAULT = "default",
DELAY_500 = "delay_500",
}
export enum LockName {
MIGRATIONS = "migrations",
TRIGGER_QUOTA = "trigger_quota",
SYNC_ACCOUNT_LICENSE = "sync_account_license",
}
export interface LockOptions {

View File

@ -364,11 +364,6 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
bytes@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
call-bind@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@ -377,16 +372,6 @@ call-bind@^1.0.0:
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
co-body@^5.1.1:
version "5.2.0"
resolved "https://registry.yarnpkg.com/co-body/-/co-body-5.2.0.tgz#5a0a658c46029131e0e3a306f67647302f71c124"
integrity sha512-sX/LQ7LqUhgyaxzbe7IqwPeTr2yfpfUIQ/dgpKo6ZI4y4lpQA0YxAomWIY+7I7rHWcG02PG+OuPREzMW/5tszQ==
dependencies:
inflation "^2.0.0"
qs "^6.4.0"
raw-body "^2.2.0"
type-is "^1.6.14"
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@ -411,11 +396,6 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
depd@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
@ -430,11 +410,6 @@ form-data@^4.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
formidable@^1.1.1:
version "1.2.6"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168"
integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@ -485,29 +460,6 @@ http-cookie-agent@^4.0.2:
dependencies:
agent-base "^6.0.2"
http-errors@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
dependencies:
depd "2.0.0"
inherits "2.0.4"
setprototypeof "1.2.0"
statuses "2.0.1"
toidentifier "1.0.1"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
inflation@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f"
integrity sha512-m3xv4hJYR2oXw4o4Y5l6P5P16WYmazYof+el6Al3f+YlggGj6qT9kImBAnzDelRALnP5d3h4jGBPKzYCizjZZw==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@ -516,7 +468,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@2.0.4:
inherits@2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@ -526,26 +478,12 @@ json5@*:
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
koa-body@4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/koa-body/-/koa-body-4.2.0.tgz#37229208b820761aca5822d14c5fc55cee31b26f"
integrity sha512-wdGu7b9amk4Fnk/ytH8GuWwfs4fsB5iNkY8kZPpgQVb04QZSv85T0M8reb+cJmvLE8cjPYvBzRikD3s6qz8OoA==
dependencies:
"@types/formidable" "^1.0.31"
co-body "^5.1.1"
formidable "^1.1.1"
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12, mime-types@~2.1.24:
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
@ -601,7 +539,7 @@ punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
qs@^6.11.0, qs@^6.4.0:
qs@^6.11.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
@ -613,16 +551,6 @@ querystringify@^2.1.1:
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
raw-body@^2.2.0:
version "2.5.1"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"
iconv-lite "0.4.24"
unpipe "1.0.0"
requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
@ -635,16 +563,6 @@ rimraf@3.0.2:
dependencies:
glob "^7.1.3"
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
setprototypeof@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
@ -654,16 +572,6 @@ side-channel@^1.0.4:
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
statuses@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
toidentifier@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
tough-cookie@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874"
@ -674,14 +582,6 @@ tough-cookie@^4.1.2:
universalify "^0.2.0"
url-parse "^1.5.3"
type-is@^1.6.14:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.24"
typescript@4.7.3:
version "4.7.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d"
@ -692,11 +592,6 @@ universalify@^0.2.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
unpipe@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
url-parse@^1.5.3:
version "1.5.10"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"

View File

@ -96,4 +96,4 @@
"update-dotenv": "1.1.1"
},
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
}
}

View File

@ -305,7 +305,7 @@ export async function upload(ctx: UserCtx) {
if (ctx.request.files == null || Array.isArray(ctx.request.files.file)) {
ctx.throw(400, "One file must be uploaded.")
}
const file = ctx.request.files.file
const file = ctx.request.files.file as any
const { type, name } = ctx.params
let bucket = coreEnv.GLOBAL_BUCKET_NAME

View File

@ -47,15 +47,10 @@ export default class AppApi {
return [response, json]
}
async publish(appUrl: string): Promise<[Response, DeployConfig]> {
const response = await this.api.post("/deploy")
async publish(appId: string | undefined): Promise<[Response, DeployConfig]> {
const response = await this.api.post(`/applications/${appId}/publish`)
const json = await response.json()
expect(response).toHaveStatusCode(200)
expect(json).toEqual({
_id: expect.any(String),
appUrl: appUrl,
status: "SUCCESS",
})
return [response, json]
}
@ -152,13 +147,9 @@ export default class AppApi {
return [response, json]
}
async unpublish(appId: string): Promise<[Response, UnpublishAppResponse]> {
const response = await this.api.del(`/applications/${appId}?unpublish=1`)
expect(response).toHaveStatusCode(200)
const json = await response.json()
expect(json.data.ok).toBe(true)
expect(json.ok).toBe(true)
expect(json.status).toBe(200)
return [response, json]
async unpublish(appId: string): Promise<[Response]> {
const response = await this.api.post(`/applications/${appId}/unpublish`)
expect(response).toHaveStatusCode(204)
return [response]
}
}

View File

@ -46,4 +46,21 @@ export default class AppApi {
const json = await response.json()
return [response, json.data]
}
async delete(id: string): Promise<[Response, Application]> {
const response = await this.api.del(`/applications/${id}`)
const json = await response.json()
return [response, json.data]
}
async publish(id: string): Promise<[Response, any]> {
const response = await this.api.post(`/applications/${id}/publish`)
const json = await response.json()
return [response, json.data]
}
async unpublish(id: string): Promise<[Response]> {
const response = await this.api.post(`/applications/${id}/unpublish`)
return [response]
}
}

View File

@ -67,7 +67,7 @@ describe("Internal API - Application creation, update, publish and delete", () =
await config.applications.canRender()
// publish app
await config.applications.publish(<string>app.url)
await config.applications.publish(<string>app.appId)
// check published app renders
config.applications.api.appId = db.getProdAppID(app.appId!)
@ -94,7 +94,7 @@ describe("Internal API - Application creation, update, publish and delete", () =
config.applications.api.appId = app.appId
// publish app
await config.applications.publish(<string>app.url)
await config.applications.publish(<string>app._id)
const [syncResponse, sync] = await config.applications.sync(
<string>app.appId
@ -126,7 +126,7 @@ describe("Internal API - Application creation, update, publish and delete", () =
config.applications.api.appId = app.appId
// publish app
await config.applications.publish(<string>app.url)
await config.applications.publish(<string>app._id)
// Change/add component to the app
await config.screen.create(generateScreen("BASIC"))

View File

@ -2,6 +2,7 @@ import TestConfiguration from "../../../config/public-api/TestConfiguration"
import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient"
import generateApp from "../../../config/public-api/fixtures/applications"
import { Application } from "@budibase/server/api/controllers/public/mapping/types"
import { db as dbCore } from "@budibase/backend-core"
describe("Public API - /applications endpoints", () => {
const api = new PublicAPIClient()
@ -47,4 +48,50 @@ describe("Public API - /applications endpoints", () => {
expect(app.updatedAt).not.toEqual(config.context.updatedAt)
expect(app.name).toEqual(config.context.name)
})
it("POST - publish an application", async () => {
config.context.name = "UpdatedName"
const [response, deployment] = await config.applications.publish(
config.context._id
)
expect(response).toHaveStatusCode(200)
expect(deployment).toEqual({
status: "SUCCESS",
})
// Verify publish
const prodAppId = dbCore.getProdAppID(config.context._id)
const [_, publishedApp] = await config.applications.read(prodAppId)
expect(response).toHaveStatusCode(200)
expect(publishedApp._id).toEqual(prodAppId)
})
it("POST - unpublish a published application", async () => {
await config.applications.publish(config.context._id)
const [response] = await config.applications.unpublish(config.context._id)
expect(response).toHaveStatusCode(204)
})
it("POST - unpublish an unpublished application", async () => {
const [response] = await config.applications.unpublish(
config.context._id
)
expect(response).toHaveStatusCode(400)
})
it("DELETE - delete a published application and the dev application", async () => {
await config.applications.publish(config.context._id)
const [response, deletion] = await config.applications.delete(config.context._id)
expect(response).toHaveStatusCode(200)
expect(deletion._id).toEqual(config.context._id)
// verify dev app deleted
const [devAppResponse] = await config.applications.read(config.context._id)
expect(devAppResponse).toHaveStatusCode(404)
// verify prod app deleted
const prodAppId = dbCore.getProdAppID(config.context._id)
const [publishedAppResponse] = await config.applications.read(prodAppId)
expect(publishedAppResponse).toHaveStatusCode(404)
})
})