diff --git a/lerna.json b/lerna.json
index 6df4a4c4cd..cb92b3ba0d 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "2.12.1",
+ "version": "2.12.2",
"npmClient": "yarn",
"packages": [
"packages/*"
diff --git a/packages/backend-core/__mocks__/aws-sdk.ts b/packages/backend-core/__mocks__/aws-sdk.ts
index b8d91dbaa9..e3be511d08 100644
--- a/packages/backend-core/__mocks__/aws-sdk.ts
+++ b/packages/backend-core/__mocks__/aws-sdk.ts
@@ -3,6 +3,7 @@ const mockS3 = {
deleteObject: jest.fn().mockReturnThis(),
deleteObjects: jest.fn().mockReturnThis(),
createBucket: jest.fn().mockReturnThis(),
+ getObject: jest.fn().mockReturnThis(),
listObject: jest.fn().mockReturnThis(),
getSignedUrl: jest.fn((operation: string, params: any) => {
return `http://s3.example.com/${params.Bucket}/${params.Key}`
diff --git a/packages/backend-core/tests/core/utilities/mocks/date.ts b/packages/backend-core/tests/core/utilities/mocks/date.ts
index f580b68349..1e6d105d93 100644
--- a/packages/backend-core/tests/core/utilities/mocks/date.ts
+++ b/packages/backend-core/tests/core/utilities/mocks/date.ts
@@ -1,2 +1,3 @@
export const MOCK_DATE = new Date("2020-01-01T00:00:00.000Z")
+
export const MOCK_DATE_TIMESTAMP = 1577836800000
diff --git a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
index e65d2cf90b..f7e9a0d2ed 100644
--- a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
+++ b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
@@ -220,15 +220,11 @@
{/if}
- {#if description}
-
- {/if}
{/if}
+ {#if description}
+
+ {/if}
{#key fields}
{#each fields as field, idx}
diff --git a/packages/server/__mocks__/aws-sdk.ts b/packages/server/__mocks__/aws-sdk.ts
index 8a66f0e213..fa6d099f56 100644
--- a/packages/server/__mocks__/aws-sdk.ts
+++ b/packages/server/__mocks__/aws-sdk.ts
@@ -70,6 +70,13 @@ module AwsMock {
Contents: {},
})
)
+
+ // @ts-ignore
+ this.getObject = jest.fn(
+ response({
+ Body: "",
+ })
+ )
}
aws.DynamoDB = { DocumentClient }
diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts
index 4afd7b23f9..4e4c66858e 100644
--- a/packages/server/src/api/controllers/application.ts
+++ b/packages/server/src/api/controllers/application.ts
@@ -32,11 +32,8 @@ import {
tenancy,
users,
} from "@budibase/backend-core"
-import { USERS_TABLE_SCHEMA } from "../../constants"
-import {
- buildDefaultDocs,
- DEFAULT_BB_DATASOURCE_ID,
-} from "../../db/defaultData/datasource_bb_default"
+import { USERS_TABLE_SCHEMA, DEFAULT_BB_DATASOURCE_ID } from "../../constants"
+import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default"
import { removeAppFromUserRoles } from "../../utilities/workerRequests"
import { stringToReadStream } from "../../utilities"
import { doesUserHaveLock } from "../../utilities/redis"
diff --git a/packages/server/src/api/routes/tests/backup.spec.ts b/packages/server/src/api/routes/tests/backup.spec.ts
index 92e0176060..d12b5e1507 100644
--- a/packages/server/src/api/routes/tests/backup.spec.ts
+++ b/packages/server/src/api/routes/tests/backup.spec.ts
@@ -5,6 +5,8 @@ import sdk from "../../../sdk"
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
import { mocks } from "@budibase/backend-core/tests"
+mocks.licenses.useBackups()
+
describe("/backups", () => {
let request = setup.getRequest()
let config = setup.getConfig()
@@ -12,16 +14,17 @@ describe("/backups", () => {
afterAll(setup.afterAll)
beforeEach(async () => {
+ tk.reset()
await config.init()
})
- describe("exportAppDump", () => {
+ describe("/api/backups/export", () => {
it("should be able to export app", async () => {
- const res = await request
- .post(`/api/backups/export?appId=${config.getAppId()}`)
- .set(config.defaultHeaders())
- .expect(200)
- expect(res.headers["content-type"]).toEqual("application/gzip")
+ const { body, headers } = await config.api.backup.exportBasicBackup(
+ config.getAppId()!
+ )
+ expect(body instanceof Buffer).toBe(true)
+ expect(headers["content-type"]).toEqual("application/gzip")
expect(events.app.exported).toBeCalledTimes(1)
})
@@ -36,11 +39,11 @@ describe("/backups", () => {
it("should infer the app name from the app", async () => {
tk.freeze(mocks.date.MOCK_DATE)
- const res = await request
- .post(`/api/backups/export?appId=${config.getAppId()}`)
- .set(config.defaultHeaders())
+ const { headers } = await config.api.backup.exportBasicBackup(
+ config.getAppId()!
+ )
- expect(res.headers["content-disposition"]).toEqual(
+ expect(headers["content-disposition"]).toEqual(
`attachment; filename="${
config.getApp()!.name
}-export-${mocks.date.MOCK_DATE.getTime()}.tar.gz"`
@@ -48,6 +51,21 @@ describe("/backups", () => {
})
})
+ describe("/api/backups/import", () => {
+ it("should be able to import an app", async () => {
+ const appId = config.getAppId()!
+ const automation = await config.createAutomation()
+ await config.createAutomationLog(automation, appId)
+ await config.createScreen()
+ const exportRes = await config.api.backup.createBackup(appId)
+ expect(exportRes.backupId).toBeDefined()
+ const importRes = await config.api.backup.importBackup(
+ appId,
+ exportRes.backupId
+ )
+ })
+ })
+
describe("calculateBackupStats", () => {
it("should be able to calculate the backup statistics", async () => {
await config.createAutomation()
diff --git a/packages/server/src/constants/index.ts b/packages/server/src/constants/index.ts
index 1104fb9b29..fb5c42e7b8 100644
--- a/packages/server/src/constants/index.ts
+++ b/packages/server/src/constants/index.ts
@@ -172,3 +172,8 @@ export enum AutomationErrors {
export const ObjectStoreBuckets = objectStore.ObjectStoreBuckets
export const MAX_AUTOMATION_RECURRING_ERRORS = 5
export const GOOGLE_SHEETS_PRIMARY_KEY = "rowNumber"
+export const DEFAULT_JOBS_TABLE_ID = "ta_bb_jobs"
+export const DEFAULT_INVENTORY_TABLE_ID = "ta_bb_inventory"
+export const DEFAULT_EXPENSES_TABLE_ID = "ta_bb_expenses"
+export const DEFAULT_EMPLOYEE_TABLE_ID = "ta_bb_employee"
+export const DEFAULT_BB_DATASOURCE_ID = "datasource_internal_bb_default"
diff --git a/packages/server/src/db/defaultData/datasource_bb_default.ts b/packages/server/src/db/defaultData/datasource_bb_default.ts
index 584b76f879..b430f9ffb6 100644
--- a/packages/server/src/db/defaultData/datasource_bb_default.ts
+++ b/packages/server/src/db/defaultData/datasource_bb_default.ts
@@ -1,4 +1,12 @@
-import { AutoFieldSubTypes, FieldTypes } from "../../constants"
+import {
+ AutoFieldSubTypes,
+ FieldTypes,
+ DEFAULT_BB_DATASOURCE_ID,
+ DEFAULT_INVENTORY_TABLE_ID,
+ DEFAULT_EMPLOYEE_TABLE_ID,
+ DEFAULT_EXPENSES_TABLE_ID,
+ DEFAULT_JOBS_TABLE_ID,
+} from "../../constants"
import { importToRows } from "../../api/controllers/table/utils"
import { cloneDeep } from "lodash/fp"
import LinkDocument from "../linkedRows/LinkDocument"
@@ -16,12 +24,6 @@ import {
TableSourceType,
} from "@budibase/types"
-export const DEFAULT_JOBS_TABLE_ID = "ta_bb_jobs"
-export const DEFAULT_INVENTORY_TABLE_ID = "ta_bb_inventory"
-export const DEFAULT_EXPENSES_TABLE_ID = "ta_bb_expenses"
-export const DEFAULT_EMPLOYEE_TABLE_ID = "ta_bb_employee"
-export const DEFAULT_BB_DATASOURCE_ID = "datasource_internal_bb_default"
-
const defaultDatasource = {
_id: DEFAULT_BB_DATASOURCE_ID,
type: dbCore.BUDIBASE_DATASOURCE_TYPE,
diff --git a/packages/server/src/integrations/utils.ts b/packages/server/src/integrations/utils.ts
index b4fff0737a..fe8a9055b0 100644
--- a/packages/server/src/integrations/utils.ts
+++ b/packages/server/src/integrations/utils.ts
@@ -7,10 +7,12 @@ import {
TableSourceType,
} from "@budibase/types"
import { DocumentType, SEPARATOR } from "../db/utils"
-import { InvalidColumns, NoEmptyFilterStrings } from "../constants"
+import {
+ InvalidColumns,
+ NoEmptyFilterStrings,
+ DEFAULT_BB_DATASOURCE_ID,
+} from "../constants"
import { helpers } from "@budibase/shared-core"
-import * as external from "../api/controllers/table/external"
-import * as internal from "../api/controllers/table/internal"
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
const ROW_ID_REGEX = /^\[.*]$/g
@@ -96,7 +98,8 @@ export function isInternalTableID(tableId: string) {
export function isExternalTable(table: Table) {
if (
table?.sourceId &&
- table.sourceId.includes(DocumentType.DATASOURCE + SEPARATOR)
+ table.sourceId.includes(DocumentType.DATASOURCE + SEPARATOR) &&
+ table?.sourceId !== DEFAULT_BB_DATASOURCE_ID
) {
return true
} else if (table?.sourceType === TableSourceType.EXTERNAL) {
diff --git a/packages/server/src/sdk/app/backups/exports.ts b/packages/server/src/sdk/app/backups/exports.ts
index d5ea31cdf5..c349dcb927 100644
--- a/packages/server/src/sdk/app/backups/exports.ts
+++ b/packages/server/src/sdk/app/backups/exports.ts
@@ -26,7 +26,6 @@ export interface DBDumpOpts {
export interface ExportOpts extends DBDumpOpts {
tar?: boolean
excludeRows?: boolean
- excludeLogs?: boolean
encryptPassword?: string
}
@@ -83,14 +82,15 @@ export async function exportDB(
})
}
-function defineFilter(excludeRows?: boolean, excludeLogs?: boolean) {
- const ids = [USER_METDATA_PREFIX, LINK_USER_METADATA_PREFIX]
+function defineFilter(excludeRows?: boolean) {
+ const ids = [
+ USER_METDATA_PREFIX,
+ LINK_USER_METADATA_PREFIX,
+ AUTOMATION_LOG_PREFIX,
+ ]
if (excludeRows) {
ids.push(TABLE_ROW_PREFIX)
}
- if (excludeLogs) {
- ids.push(AUTOMATION_LOG_PREFIX)
- }
return (doc: any) =>
!ids.map(key => doc._id.includes(key)).reduce((prev, curr) => prev || curr)
}
@@ -118,7 +118,7 @@ export async function exportApp(appId: string, config?: ExportOpts) {
fs.writeFileSync(join(tmpPath, path), contents)
}
}
- // get all of the files
+ // get all the files
else {
tmpPath = await objectStore.retrieveDirectory(
ObjectStoreBuckets.APPS,
@@ -141,7 +141,7 @@ export async function exportApp(appId: string, config?: ExportOpts) {
// enforce an export of app DB to the tmp path
const dbPath = join(tmpPath, DB_EXPORT_FILE)
await exportDB(appId, {
- filter: defineFilter(config?.excludeRows, config?.excludeLogs),
+ filter: defineFilter(config?.excludeRows),
exportPath: dbPath,
})
@@ -191,7 +191,6 @@ export async function streamExportApp({
}) {
const tmpPath = await exportApp(appId, {
excludeRows,
- excludeLogs: true,
tar: true,
encryptPassword,
})
diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts
index 2260c0ca92..04c0552457 100644
--- a/packages/server/src/tests/utilities/TestConfiguration.ts
+++ b/packages/server/src/tests/utilities/TestConfiguration.ts
@@ -805,8 +805,9 @@ class TestConfiguration {
// AUTOMATION LOG
- async createAutomationLog(automation: Automation) {
- return await context.doInAppContext(this.getProdAppId(), async () => {
+ async createAutomationLog(automation: Automation, appId?: string) {
+ appId = appId || this.getProdAppId()
+ return await context.doInAppContext(appId!, async () => {
return await pro.sdk.automations.logs.storeLog(
automation,
basicAutomationResults(automation._id!)
diff --git a/packages/server/src/tests/utilities/api/backup.ts b/packages/server/src/tests/utilities/api/backup.ts
new file mode 100644
index 0000000000..f9cbc7086e
--- /dev/null
+++ b/packages/server/src/tests/utilities/api/backup.ts
@@ -0,0 +1,45 @@
+import {
+ CreateAppBackupResponse,
+ ImportAppBackupResponse,
+} from "@budibase/types"
+import TestConfiguration from "../TestConfiguration"
+import { TestAPI } from "./base"
+
+export class BackupAPI extends TestAPI {
+ constructor(config: TestConfiguration) {
+ super(config)
+ }
+
+ exportBasicBackup = async (appId: string) => {
+ const result = await this.request
+ .post(`/api/backups/export?appId=${appId}`)
+ .set(this.config.defaultHeaders())
+ .expect("Content-Type", /application\/gzip/)
+ .expect(200)
+ return {
+ body: result.body as Buffer,
+ headers: result.headers,
+ }
+ }
+
+ createBackup = async (appId: string) => {
+ const result = await this.request
+ .post(`/api/apps/${appId}/backups`)
+ .set(this.config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ return result.body as CreateAppBackupResponse
+ }
+
+ importBackup = async (
+ appId: string,
+ backupId: string
+ ): Promise => {
+ const result = await this.request
+ .post(`/api/apps/${appId}/backups/${backupId}/import`)
+ .set(this.config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ return result.body as ImportAppBackupResponse
+ }
+}
diff --git a/packages/server/src/tests/utilities/api/index.ts b/packages/server/src/tests/utilities/api/index.ts
index 30ef7c478d..c553e7b8f4 100644
--- a/packages/server/src/tests/utilities/api/index.ts
+++ b/packages/server/src/tests/utilities/api/index.ts
@@ -7,6 +7,7 @@ import { DatasourceAPI } from "./datasource"
import { LegacyViewAPI } from "./legacyView"
import { ScreenAPI } from "./screen"
import { ApplicationAPI } from "./application"
+import { BackupAPI } from "./backup"
import { AttachmentAPI } from "./attachment"
export default class API {
@@ -18,6 +19,7 @@ export default class API {
datasource: DatasourceAPI
screen: ScreenAPI
application: ApplicationAPI
+ backup: BackupAPI
attachment: AttachmentAPI
constructor(config: TestConfiguration) {
@@ -29,6 +31,7 @@ export default class API {
this.datasource = new DatasourceAPI(config)
this.screen = new ScreenAPI(config)
this.application = new ApplicationAPI(config)
+ this.backup = new BackupAPI(config)
this.attachment = new AttachmentAPI(config)
}
}
diff --git a/packages/types/src/api/web/app/backup.ts b/packages/types/src/api/web/app/backup.ts
index c9a8d07f5e..f77707e9c6 100644
--- a/packages/types/src/api/web/app/backup.ts
+++ b/packages/types/src/api/web/app/backup.ts
@@ -20,3 +20,8 @@ export interface CreateAppBackupResponse {
export interface UpdateAppBackupRequest {
name: string
}
+
+export interface ImportAppBackupResponse {
+ restoreId: string
+ message: string
+}