revert functionality working

This commit is contained in:
Martin McKeaveney 2021-05-16 21:25:37 +01:00
parent 332f0555a3
commit 0f2bcf581d
26 changed files with 266 additions and 73 deletions

View File

@ -31,7 +31,7 @@ class Replication {
* Two way replication operation, intended to be promise based.
* @param {Object} opts - PouchDB replication options
*/
sync(opts) {
sync(opts = {}) {
this.replication = this.promisify(this.source.sync, opts)
return this.replication
}
@ -40,7 +40,7 @@ class Replication {
* One way replication operation, intended to be promise based.
* @param {Object} opts - PouchDB replication options
*/
replicate(opts) {
replicate(opts = {}) {
this.replication = this.promisify(this.source.replicate.to, opts)
return this.replication
}
@ -61,8 +61,13 @@ class Replication {
})
}
/**
* Rollback the target DB back to the state of the source DB
*/
async rollback() {
await this.target.destroy()
// Recreate the DB again
this.target = getDB(this.target.name)
await this.replicate()
}

View File

@ -12,6 +12,9 @@ exports.StaticDatabases = {
GLOBAL: {
name: "global-db",
},
DEPLOYMENTS: {
name: "deployments",
},
}
const DocumentTypes = {
@ -21,6 +24,7 @@ const DocumentTypes = {
TEMPLATE: "template",
APP: "app",
APP_DEV: "app_dev",
APP_METADATA: "app_metadata",
}
exports.DocumentTypes = DocumentTypes

View File

@ -51,14 +51,14 @@ export const getFrontendStore = () => {
store.actions = {
initialise: async pkg => {
const { layouts, screens, application, clientLibPath } = pkg
const components = await fetchComponentLibDefinitions(application._id)
const components = await fetchComponentLibDefinitions(application.appId)
store.update(state => ({
...state,
libraries: application.componentLibraries,
components,
name: application.name,
description: application.description,
appId: application._id,
appId: application.appId,
url: application.url,
layouts,
screens,

View File

@ -0,0 +1,110 @@
<script>
import { onMount, onDestroy } from "svelte"
import { Button, Modal, notifications, ModalContent } from "@budibase/bbui"
import { store } from "builderStore"
import api from "builderStore/api"
import analytics from "analytics"
import FeedbackIframe from "components/feedback/FeedbackIframe.svelte"
const DeploymentStatus = {
SUCCESS: "SUCCESS",
PENDING: "PENDING",
FAILURE: "FAILURE",
}
const POLL_INTERVAL = 1000
let loading = false
let feedbackModal
let deployments = []
let poll
let publishModal
$: appId = $store.appId
async function deployApp() {
try {
notifications.info(`Deployment started. Please wait.`)
const response = await api.post("/api/deploy")
const json = await response.json()
if (response.status !== 200) {
throw new Error()
}
if (analytics.requestFeedbackOnDeploy()) {
feedbackModal.show()
}
} catch (err) {
analytics.captureException(err)
notifications.error("Deployment unsuccessful. Please try again later.")
}
}
async function fetchDeployments() {
try {
const response = await api.get(`/api/deployments`)
const json = await response.json()
if (deployments.length > 0) {
checkIncomingDeploymentStatus(deployments, json)
}
deployments = json
} catch (err) {
console.error(err)
clearInterval(poll)
notifications.error(
"Error fetching deployment history. Please try again."
)
}
}
// Required to check any updated deployment statuses between polls
function checkIncomingDeploymentStatus(current, incoming) {
for (let incomingDeployment of incoming) {
if (incomingDeployment.status === DeploymentStatus.FAILURE || incomingDeployment.status === DeploymentStatus.SUCCESS) {
const currentDeployment = current.find(
deployment => deployment._id === incomingDeployment._id
)
// We have just been notified of an ongoing deployments status change
if (
!currentDeployment ||
currentDeployment.status === DeploymentStatus.PENDING
) {
if (incomingDeployment.status === DeploymentStatus.FAILURE) {
notifications.error(incomingDeployment.err)
} else {
notifications.send("Published to Production.", "success", "CheckmarkCircle")
}
}
}
}
}
onMount(() => {
fetchDeployments()
poll = setInterval(fetchDeployments, POLL_INTERVAL)
})
onDestroy(() => clearInterval(poll))
</script>
<Button
secondary
on:click={publishModal.show}
>
Publish
</Button>
<Modal bind:this={publishModal}>
<ModalContent
title="Publish to Production"
confirmText="Publish"
onConfirm={deployApp}
>
<span>The changes you have made will be published to the production version of the application.</span>
</ModalContent>
</Modal>

View File

@ -0,0 +1,35 @@
<script>
import { onMount, onDestroy } from "svelte"
import { Button, Icon, Modal, notifications, ModalContent } from "@budibase/bbui"
import { store } from "builderStore"
import { apps } from "stores/portal"
import api from "builderStore/api"
let revertModal
$: appId = $store.appId
const revert = async () => {
try {
const response = await api.post(`/api/dev/${appId}/revert`)
const json = await response.json()
if (response.status !== 200) throw json.message
notifications.info("Changes reverted.")
} catch (err) {
notifications.error(`Error reverting changes: ${err}`)
}
}
</script>
<Icon name="Revert" hoverable on:click={revertModal.show} />
<Modal bind:this={revertModal}>
<ModalContent
title="Revert Changes"
confirmText="Revert"
onConfirm={revert}
>
<span>The changes you have made will be deleted and the application reverted back to its production state.</span>
</ModalContent>
</Modal>

View File

@ -41,7 +41,7 @@
</MenuItem>
{/if}
{#if app.lockedBy && app.lockedBy?.email === $auth.user?.email}
<MenuItem on:click={() => releaseLock(app._id)} icon="LockOpen">
<MenuItem on:click={() => releaseLock(app.appId)} icon="LockOpen">
Release Lock
</MenuItem>
{/if}

View File

@ -50,7 +50,7 @@
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem>
{/if}
{#if app.lockedBy && app.lockedBy?.email === $auth.user?.email}
<MenuItem on:click={() => releaseLock(app._id)} icon="LockOpen">
<MenuItem on:click={() => releaseLock(app.appId)} icon="LockOpen">
Release Lock
</MenuItem>
{/if}

View File

@ -96,7 +96,7 @@
// Select Correct Application/DB in prep for creating user
const applicationPkg = await get(
`/api/applications/${appJson._id}/appPackage`
`/api/applications/${appJson.instance._id}/appPackage`
)
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
@ -112,7 +112,7 @@
}
const userResp = await api.post(`/api/users/metadata/self`, user)
await userResp.json()
$goto(`/builder/app/${appJson._id}`)
$goto(`/builder/app/${appJson.instance._id}`)
} catch (error) {
console.error(error)
notifications.error(error)

View File

@ -1,10 +1,12 @@
<script>
import { store, automationStore } from "builderStore"
import { roles } from "stores/backend"
import { Button, ActionGroup, ActionButton, Tabs, Tab } from "@budibase/bbui"
import { Button, Icon, Modal, ModalContent, ActionGroup, ActionButton, Tabs, Tab } from "@budibase/bbui"
import SettingsLink from "components/settings/Link.svelte"
import ThemeEditorDropdown from "components/settings/ThemeEditorDropdown.svelte"
import FeedbackNavLink from "components/feedback/FeedbackNavLink.svelte"
import DeployModal from "components/deploy/DeployModal.svelte"
import RevertModal from "components/deploy/RevertModal.svelte"
import { get } from "builderStore/api"
import { isActive, goto, layout } from "@roxi/routify"
import Logo from "/assets/bb-logo.svg"
@ -81,25 +83,13 @@
<ActionGroup />
</div>
<div class="toprightnav">
<ThemeEditorDropdown />
<FeedbackNavLink />
<div class="topnavitemright">
<a
target="_blank"
href="https://github.com/Budibase/budibase/discussions"
>
<i class="ri-github-fill" />
</a>
</div>
<SettingsLink />
<Button
secondary
<RevertModal />
<Icon name="Play" hoverable
on:click={() => {
window.open(`/${application}`)
}}
>
Preview
</Button>
/>
<DeployModal />
</div>
</div>
<div class="beta">
@ -153,6 +143,7 @@
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: var(--spacing-xl);
}
.topleftnav {

View File

@ -62,16 +62,16 @@
const openApp = app => {
if (appStatus === AppStatus.DEV) {
$goto(`../../app/${app._id}`)
$goto(`../../app/${app.appId}`)
} else {
window.open(`/${app._id}`, '_blank');
window.open(`/${app.appId}`, '_blank');
}
}
const exportApp = app => {
try {
download(
`/api/backups/export?appId=${app._id}&appname=${encodeURIComponent(
`/api/backups/export?appId=${app.appId}&appname=${encodeURIComponent(
app.name
)}`
)
@ -157,7 +157,7 @@
class:appGrid={layout === "grid"}
class:appTable={layout === "table"}
>
{#each $apps as app, idx (app._id)}
{#each $apps as app, idx (app.appId)}
<svelte:component
this={layout === "grid" ? AppCard : AppRow}
deletable={appStatus === AppStatus.PUBLISHED}

View File

@ -13,6 +13,7 @@ export function createAppStore() {
} else {
store.set([])
}
return json
} catch (error) {
store.set([])
}

View File

@ -1,4 +1,4 @@
export { organisation } from "./organisation"
export { admin } from "./admin"
export { apps } from "./apps"
export { email } from "./email"
export { email } from "./email"

View File

@ -6,6 +6,7 @@
*/
const CouchDB = require("../src/db")
const { DocumentTypes } = require("../src/db/utils")
const appName = process.argv[2].toLowerCase()
const remoteUrl = process.argv[3]
@ -18,7 +19,7 @@ const run = async () => {
let apps = []
for (let dbName of appDbNames) {
const db = new CouchDB(dbName)
apps.push(db.get(dbName))
apps.push(db.get(DocumentTypes.APP_METADATA))
}
apps = await Promise.all(apps)
const app = apps.find(
@ -32,7 +33,7 @@ const run = async () => {
return
}
const instanceDb = new CouchDB(app._id)
const instanceDb = new CouchDB(app.appId)
const remoteDb = new CouchDB(`${remoteUrl}/${appName}`)
instanceDb.replicate

View File

@ -88,7 +88,6 @@ async function getAppUrlIfNotInUse(ctx) {
}
async function createInstance(template) {
// TODO: Do we need the normal app ID?
const baseAppId = generateAppID()
const appId = generateDevAppID(baseAppId)
@ -126,16 +125,16 @@ exports.fetch = async function (ctx) {
const isDev = ctx.query.status === AppStatus.DEV
apps = apps.filter(app => {
if (isDev) {
return app._id.startsWith(DocumentTypes.APP_DEV)
return app.appId.startsWith(DocumentTypes.APP_DEV)
}
return !app._id.startsWith(DocumentTypes.APP_DEV)
return !app.appId.startsWith(DocumentTypes.APP_DEV)
})
// get the locks for all the dev apps
if (isDev) {
const locks = await getAllLocks()
for (let app of apps) {
const lock = locks.find(lock => lock.appId === app._id)
const lock = locks.find(lock => lock.appId === app.appId)
if (lock) {
app.lockedBy = lock.user
} else {
@ -166,7 +165,7 @@ exports.fetchAppDefinition = async function (ctx) {
exports.fetchAppPackage = async function (ctx) {
const db = new CouchDB(ctx.params.appId)
const application = await db.get(ctx.params.appId)
const application = await db.get(DocumentTypes.APP_METADATA)
const [layouts, screens] = await Promise.all([getLayouts(db), getScreens(db)])
ctx.body = {
@ -191,7 +190,8 @@ exports.create = async function (ctx) {
const url = await getAppUrlIfNotInUse(ctx)
const appId = instance._id
const newApplication = {
_id: appId,
_id: DocumentTypes.APP_METADATA,
appId: instance._id,
type: "app",
version: packageJson.version,
componentLibraries: ["@budibase/standard-components"],
@ -254,7 +254,7 @@ exports.delete = async function (ctx) {
}
const createEmptyAppPackage = async (ctx, app) => {
const db = new CouchDB(app._id)
const db = new CouchDB(app.instance._id)
let screensAndLayouts = []
for (let layout of BASE_LAYOUTS) {

View File

@ -1,10 +1,11 @@
const CouchDB = require("../../db")
const { DocumentTypes } = require("../../db/utils")
const { getComponentLibraryManifest } = require("../../utilities/fileSystem")
exports.fetchAppComponentDefinitions = async function (ctx) {
const appId = ctx.params.appId || ctx.appId
const db = new CouchDB(appId)
const app = await db.get(appId)
const app = await db.get(DocumentTypes.APP_METADATA)
let componentManifests = await Promise.all(
app.componentLibraries.map(async library => {

View File

@ -1,6 +1,8 @@
const PouchDB = require("../../../db")
const Deployment = require("./Deployment")
const { Replication } = require("@budibase/auth").db
const { Replication, StaticDatabases } = require("@budibase/auth").db
const { DocumentTypes } = require("../../../db/utils")
// the max time we can wait for an invalidation to complete before considering it failed
const MAX_PENDING_TIME_MS = 30 * 60000
const DeploymentStatus = {
@ -26,16 +28,16 @@ async function checkAllDeployments(deployments) {
return { updated, deployments }
}
async function storeLocalDeploymentHistory(deployment) {
async function storeDeploymentHistory(deployment) {
const appId = deployment.getAppId()
const deploymentJSON = deployment.getJSON()
const db = new PouchDB(appId)
const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name)
let deploymentDoc
try {
deploymentDoc = await db.get("_local/deployments")
deploymentDoc = await db.get(appId)
} catch (err) {
deploymentDoc = { _id: "_local/deployments", history: {} }
deploymentDoc = { _id: appId, history: {} }
}
const deploymentId = deploymentJSON._id
@ -65,12 +67,9 @@ async function deployApp(deployment) {
})
await replication.replicate()
// Strip the _dev prefix and update the appID document in the new DB
const db = new PouchDB(productionAppId)
const appDoc = await db.get(deployment.appId)
appDoc._id = productionAppId
delete appDoc._rev
const appDoc = await db.get(DocumentTypes.APP_METADATA)
appDoc.appId = productionAppId
appDoc.instance._id = productionAppId
await db.put(appDoc)
@ -79,13 +78,17 @@ async function deployApp(deployment) {
source: productionAppId,
target: deployment.appId,
})
liveReplication.subscribe()
liveReplication.subscribe({
filter: function (doc) {
return doc._id !== DocumentTypes.APP_METADATA
},
})
deployment.setStatus(DeploymentStatus.SUCCESS)
await storeLocalDeploymentHistory(deployment)
await storeDeploymentHistory(deployment)
} catch (err) {
deployment.setStatus(DeploymentStatus.FAILURE, err.message)
await storeLocalDeploymentHistory(deployment)
await storeDeploymentHistory(deployment)
throw {
...err,
message: `Deployment Failed: ${err.message}`,
@ -95,8 +98,8 @@ async function deployApp(deployment) {
exports.fetchDeployments = async function (ctx) {
try {
const db = new PouchDB(ctx.appId)
const deploymentDoc = await db.get("_local/deployments")
const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name)
const deploymentDoc = await db.get(ctx.appId)
const { updated, deployments } = await checkAllDeployments(
deploymentDoc,
ctx.user
@ -112,8 +115,8 @@ exports.fetchDeployments = async function (ctx) {
exports.deploymentProgress = async function (ctx) {
try {
const db = new PouchDB(ctx.appId)
const deploymentDoc = await db.get("_local/deployments")
const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name)
const deploymentDoc = await db.get(ctx.appId)
ctx.body = deploymentDoc[ctx.params.deploymentId]
} catch (err) {
ctx.throw(
@ -126,7 +129,7 @@ exports.deploymentProgress = async function (ctx) {
exports.deployApp = async function (ctx) {
let deployment = new Deployment(ctx.appId)
deployment.setStatus(DeploymentStatus.PENDING)
deployment = await storeLocalDeploymentHistory(deployment)
deployment = await storeDeploymentHistory(deployment)
await deployApp(deployment)

View File

@ -1,8 +1,11 @@
const fetch = require("node-fetch")
const CouchDB = require("../../db")
const env = require("../../environment")
const { checkSlashesInUrl } = require("../../utilities")
const { request } = require("../../utilities/workerRequests")
const { clearLock } = require("../../utilities/redis")
const { Replication } = require("@budibase/auth").db
const { DocumentTypes } = require("../../db/utils")
async function redirect(ctx, method) {
const { devPath } = ctx.params
@ -45,3 +48,37 @@ exports.clearLock = async ctx => {
message: "Lock released successfully.",
}
}
exports.revert = async ctx => {
const { appId } = ctx.params
const productionAppId = appId.replace("_dev", "")
// App must have been deployed first
try {
const db = new CouchDB(productionAppId, { skip_setup: true })
const info = await db.info()
if (info.error) throw info.error
} catch (err) {
return ctx.throw(400, "App has not yet been deployed")
}
try {
const replication = new Replication({
source: productionAppId,
target: appId,
})
await replication.rollback()
// update appID in reverted app to be dev version again
const db = new CouchDB(appId)
const appDoc = await db.get(DocumentTypes.APP_METADATA)
appDoc.appId = appId
appDoc.instance._id = appId
await db.put(appDoc)
ctx.body = {
message: "Reverted changes successfully.",
}
} catch (err) {
ctx.throw(400, `Unable to revert. ${err}`)
}
}

View File

@ -18,6 +18,7 @@ const env = require("../../../environment")
const { objectStoreUrl, clientLibraryPath } = require("../../../utilities")
const { upload } = require("../../../utilities/fileSystem")
const { attachmentsRelativeURL } = require("../../../utilities")
const { DocumentTypes } = require("../../../db/utils")
async function prepareUpload({ s3Key, bucket, metadata, file }) {
const response = await upload({
@ -85,7 +86,7 @@ exports.serveApp = async function (ctx) {
}
const App = require("./templates/BudibaseApp.svelte").default
const db = new CouchDB(appId, { skip_setup: true })
const appInfo = await db.get(appId)
const appInfo = await db.get(DocumentTypes.APP_METADATA)
const { head, html, css } = App.render({
title: appInfo.name,
@ -125,7 +126,7 @@ exports.serveComponentLibrary = async function (ctx) {
return send(ctx, "/index.js", { root: componentLibraryPath })
}
const db = new CouchDB(appId)
const appInfo = await db.get(appId)
const appInfo = await db.get(DocumentTypes.APP_METADATA)
let componentLib = "componentlibrary"
if (appInfo && appInfo.version) {

View File

@ -13,10 +13,8 @@ if (env.isDev() || env.isTest()) {
.delete("/api/admin/:devPath(.*)", controller.redirectDelete)
}
router.delete(
"/api/dev/:appId/lock",
authorized(BUILDER),
controller.clearLock
)
router
.delete("/api/dev/:appId/lock", authorized(BUILDER), controller.clearLock)
.post("/api/dev/:appId/revert", authorized(BUILDER), controller.revert)
module.exports = router

View File

@ -21,7 +21,7 @@ exports.clearAllApps = async () => {
return
}
for (let app of apps) {
const appId = app._id
const { appId } = app
await appController.delete(new Request(null, { appId }))
}
}

View File

@ -23,6 +23,7 @@ const AppStatus = {
const DocumentTypes = {
APP: CoreDocTypes.APP,
APP_DEV: CoreDocTypes.APP_DEV,
APP_METADATA: CoreDocTypes.APP_METADATA,
TABLE: "ta",
ROW: "ro",
USER: "us",

View File

@ -89,7 +89,7 @@ class TestConfiguration {
if (this.server) {
this.server.close()
}
cleanup(this.allApps.map(app => app._id))
cleanup(this.allApps.map(app => app.appId))
}
defaultHeaders() {
@ -141,7 +141,7 @@ class TestConfiguration {
async createApp(appName) {
this.app = await this._req({ name: appName }, null, controllers.app.create)
this.appId = this.app._id
this.appId = this.app.appId
this.allApps.push(this.app)
return this.app
}

View File

@ -1,5 +1,5 @@
const env = require("../environment")
const { APP_PREFIX } = require("../db/utils")
const { APP_PREFIX, DocumentTypes } = require("../db/utils")
const CouchDB = require("../db")
const { OBJ_STORE_DIRECTORY, ObjectStoreBuckets } = require("../constants")
@ -19,7 +19,9 @@ exports.isDev = env.isDev
exports.getAllApps = async () => {
let allDbs = await CouchDB.allDbs()
const appDbNames = allDbs.filter(dbName => dbName.startsWith(APP_PREFIX))
const appPromises = appDbNames.map(db => new CouchDB(db).get(db))
const appPromises = appDbNames.map(db =>
new CouchDB(db).get(DocumentTypes.APP_METADATA)
)
if (appPromises.length === 0) {
return []
} else {

View File

@ -1,4 +1,5 @@
const fetch = require("node-fetch")
const { DocumentTypes } = require("@budibase/auth").db
const CouchDB = require("../../db")
const env = require("../../environment")
@ -14,7 +15,9 @@ exports.getApps = async ctx => {
allDbs = await CouchDB.allDbs()
}
const appDbNames = allDbs.filter(dbName => dbName.startsWith(APP_PREFIX))
const appPromises = appDbNames.map(db => new CouchDB(db).get(db))
const appPromises = appDbNames.map(db =>
new CouchDB(db).get(DocumentTypes.APP_METADATA)
)
const apps = await Promise.allSettled(appPromises)
const body = {}
@ -26,7 +29,7 @@ exports.getApps = async ctx => {
let url = app.url || encodeURI(`${app.name}`)
url = `/${url.replace(URL_REGEX_SLASH, "")}`
body[url] = {
appId: app._id,
appId: app.instance._id,
name: app.name,
url,
}