UNPKG

@budibase/server

Version:
610 lines (518 loc) • 18.9 kB
import { constants, context, db as dbCore } from "@budibase/backend-core" import { structures } from "@budibase/backend-core/tests" import { Automation, FieldType, FormulaType, PublishResourceState, Row, Table, WorkspaceApp, } from "@budibase/types" import { cloneDeep } from "lodash/fp" import { createAutomationBuilder } from "../../../automations/tests/utilities/AutomationTestBuilder" import { getRowParams } from "../../../db/utils" import { basicTable } from "../../../tests/utilities/structures" import * as setup from "./utilities" describe("/api/deploy", () => { let config = setup.getConfig() afterAll(() => { setup.afterAll() }) beforeAll(async () => { await config.init() }) beforeEach(async () => { await config.newTenant() }) describe("GET /api/deploy/status", () => { it("returns empty state when unpublished", async () => { await config.api.workspace.unpublish(config.devWorkspaceId!) const res = await config.api.deploy.publishStatus() for (const automation of Object.values(res.automations)) { expect(automation.published).toBe(false) } // default screens will appear here for (const workspaceApp of Object.values(res.workspaceApps)) { expect(workspaceApp.published).toBe(false) } }) it("returns disabled state for development-only resources", async () => { const table = await config.api.table.save(basicTable()) // Create automation const { automation } = await createAutomationBuilder(config) .onRowSaved({ tableId: table._id! }) .serverLog({ text: "Test automation" }) .save() // Create workspace app const { workspaceApp } = await config.api.workspaceApp.create( structures.workspaceApps.createRequest({ name: "Test Workspace App", url: "/testapp", }) ) const res = await config.api.deploy.publishStatus() expect(res.automations[automation._id!]).toEqual({ published: false, name: automation.name, unpublishedChanges: true, state: "disabled", }) expect(res.workspaceApps[workspaceApp._id!]).toEqual({ published: false, name: workspaceApp.name, unpublishedChanges: true, state: "disabled", }) }) it("returns published state after full publish", async () => { const table = await config.api.table.save(basicTable()) const { automation } = await createAutomationBuilder(config) .onRowSaved({ tableId: table._id! }) .serverLog({ text: "Test automation" }) .save() const { workspaceApp } = await config.api.workspaceApp.create( structures.workspaceApps.createRequest({ name: "Test Workspace App", url: "/testapp", }) ) await config.api.workspace.publish(config.devWorkspace!.appId) const res = await config.api.deploy.publishStatus() expect(res.automations[automation._id!]).toEqual({ publishedAt: expect.any(String), published: true, name: automation.name, unpublishedChanges: false, state: "published", }) expect(res.workspaceApps[workspaceApp._id!]).toEqual({ publishedAt: expect.any(String), published: true, name: workspaceApp.name, unpublishedChanges: false, state: "published", }) expect(res.tables[table._id!]).toEqual({ publishedAt: expect.any(String), published: true, name: table.name, unpublishedChanges: false, state: "published", }) }) it("returns mixed state after filtered publish", async () => { const table = await config.api.table.save(basicTable()) // Create two automations const { automation: publishedAutomation } = await createAutomationBuilder( config ) .onRowSaved({ tableId: table._id! }) .serverLog({ text: "Published automation" }) .save() const { workspaceApp: publishedWorkspaceApp } = await config.api.workspaceApp.create( structures.workspaceApps.createRequest({ name: "Published Workspace App", url: "/publishedapp", }) ) await config.api.workspace.publish(config.devWorkspace!.appId) const { automation: unpublishedAutomation } = await createAutomationBuilder(config) .onRowSaved({ tableId: table._id! }) .serverLog({ text: "Unpublished automation" }) .save() const { workspaceApp: unpublishedWorkspaceApp } = await config.api.workspaceApp.create( structures.workspaceApps.createRequest({ name: "Unpublished Workspace App", url: "/unpublishedapp", }) ) const res = await config.api.deploy.publishStatus() expect(res.automations[publishedAutomation._id!]).toEqual({ published: true, name: publishedAutomation.name, publishedAt: expect.any(String), unpublishedChanges: false, state: "published", }) expect(res.workspaceApps[publishedWorkspaceApp._id!]).toEqual({ published: true, name: publishedWorkspaceApp.name, publishedAt: expect.any(String), unpublishedChanges: false, state: "published", }) expect(res.automations[unpublishedAutomation._id!]).toEqual({ published: false, name: unpublishedAutomation.name, unpublishedChanges: true, state: "disabled", }) expect(res.workspaceApps[unpublishedWorkspaceApp._id!]).toEqual({ published: false, name: unpublishedWorkspaceApp.name, unpublishedChanges: true, state: "disabled", }) }) it("handles app with disabled automation/workspace app", async () => { const table = await config.api.table.save(basicTable()) const { automation } = await createAutomationBuilder(config) .onRowSaved({ tableId: table._id! }) .serverLog({ text: "Test automation" }) .save({ disabled: true }) const { workspaceApp } = await config.api.workspaceApp.create( structures.workspaceApps.createRequest({ name: "Test Workspace App", url: "/testapp", disabled: true, }) ) await config.api.workspace.publish(config.devWorkspace!.appId) const res = await config.api.deploy.publishStatus() expect(res.automations[automation._id!]).toEqual({ published: true, publishedAt: expect.any(String), name: automation.name, unpublishedChanges: false, state: "disabled", }) expect(res.workspaceApps[workspaceApp._id!]).toEqual({ published: true, publishedAt: expect.any(String), name: workspaceApp.name, unpublishedChanges: false, state: "disabled", }) }) it("returns only development resources that exist", async () => { const table = await config.api.table.save(basicTable()) const { automation } = await createAutomationBuilder(config) .onRowSaved({ tableId: table._id! }) .serverLog({ text: "Test automation" }) .save() await config.api.workspace.publish(config.devWorkspace!.appId) // Delete automation from development await config.api.automation.delete(automation) const res = await config.api.deploy.publishStatus() // Should not include deleted automation expect(res.automations[automation._id!]).toBeUndefined() expect(Object.keys(res.automations)).toHaveLength(0) }) }) describe("POST /api/deploy", () => { beforeAll(async () => { await config.init() }) beforeEach(async () => { await config.unpublish() }) function expectApp(workspace: WorkspaceApp) { return { disabled: async ( disabled: boolean | undefined, state: PublishResourceState ) => { expect( (await config.api.workspaceApp.find(workspace._id!)).disabled ).toBe(disabled) const status = await config.api.deploy.publishStatus() expect(status.workspaceApps[workspace._id!]).toEqual( expect.objectContaining({ state, }) ) }, } } function expectAutomation(automation: Automation) { return { disabled: async ( disabled: boolean | undefined, state: PublishResourceState ) => { expect( (await config.api.automation.get(automation._id!)).disabled ).toBe(disabled) const status = await config.api.deploy.publishStatus() expect(status.automations[automation._id!]).toEqual( expect.objectContaining({ state, }) ) }, } } async function publishProdApp() { await config.api.workspace.publish(config.getDevWorkspaceId()) await config.api.workspace.sync(config.getDevWorkspaceId()) } it("should define the disable value for all workspace apps when publishing for the first time", async () => { const { workspaceApp: publishedApp } = await config.api.workspaceApp.create({ name: "Test App 1", url: "/app1", disabled: false, }) const { workspaceApp: appWithoutInfo } = await config.api.workspaceApp.create({ name: "Test App 2", url: "/app2", }) const { workspaceApp: disabledApp } = await config.api.workspaceApp.create( structures.workspaceApps.createRequest({ name: "Disabled App", url: "/disabled", disabled: true, }) ) expect(publishedApp.disabled).toBe(false) expect(appWithoutInfo.disabled).toBeUndefined() expect(disabledApp.disabled).toBe(true) // Publish the app for the first time await publishProdApp() await expectApp(publishedApp).disabled( false, PublishResourceState.PUBLISHED ) await expectApp(appWithoutInfo).disabled( true, PublishResourceState.DISABLED ) await expectApp(disabledApp).disabled(true, PublishResourceState.DISABLED) }) it("should define the disable value for all automations when publishing for the first time", async () => { const table = await config.api.table.save(basicTable()) const { automation: disabledAutomation } = await createAutomationBuilder( config ) .onRowSaved({ tableId: table._id! }) .save({ disabled: true }) const { automation: enabledAutomation } = await createAutomationBuilder( config ) .onRowSaved({ tableId: table._id! }) .save({ disabled: false }) const { automation: automationWithoutInfo } = await createAutomationBuilder(config) .onRowSaved({ tableId: table._id! }) .save({ disabled: undefined }) // Verify apps are not disabled before publishing expect(disabledAutomation.disabled).toBe(true) expect(enabledAutomation.disabled).toBe(false) expect(automationWithoutInfo.disabled).toBe(undefined) // Publish the app for the first time await publishProdApp() await expectAutomation(disabledAutomation).disabled( true, PublishResourceState.DISABLED ) await expectAutomation(enabledAutomation).disabled( false, PublishResourceState.PUBLISHED ) await expectAutomation(automationWithoutInfo).disabled( true, PublishResourceState.DISABLED ) }) it("should not disable workspace apps on subsequent publishes", async () => { const { workspaceApp: initialApp } = await config.api.workspaceApp.create( { name: "Test App 1", url: "/app1", disabled: undefined, } ) await publishProdApp() // Remove disabled flag, simulating old apps const db = dbCore.getDB(config.getDevWorkspaceId()) await db.put({ ...(await config.api.workspaceApp.find(initialApp._id)), disabled: undefined, }) const { workspaceApp: secondApp } = await config.api.workspaceApp.create({ name: "Test App 2", url: "/app2", disabled: true, }) await publishProdApp() await expectApp(initialApp).disabled( undefined, PublishResourceState.PUBLISHED ) await expectApp(secondApp).disabled(true, PublishResourceState.DISABLED) }) it("should not disable automations on subsequent publishes", async () => { const table = await config.api.table.save(basicTable()) const { automation: initialAutomation } = await createAutomationBuilder( config ) .onRowSaved({ tableId: table._id! }) .save({ disabled: undefined }) await publishProdApp() // Remove disabled flag, simulating old automations const db = dbCore.getDB(config.getDevWorkspaceId()) await db.put({ ...(await config.api.automation.get(initialAutomation._id!)), disabled: undefined, }) const { automation: secondAutomation } = await createAutomationBuilder( config ) .onRowSaved({ tableId: table._id! }) .save({ disabled: true }) await publishProdApp() await expectAutomation(initialAutomation).disabled( undefined, PublishResourceState.PUBLISHED ) await expectAutomation(secondAutomation).disabled( true, PublishResourceState.DISABLED ) }) }) it("updates production rows with new static formulas when published", async () => { const amountFieldName = "amount" const tableDefinition = basicTable(undefined, { schema: { [amountFieldName]: { name: amountFieldName, type: FieldType.NUMBER, constraints: {}, }, }, }) const table = await config.api.table.save(tableDefinition) // Initial publish so a production workspace exists await config.api.workspace.publish(config.devWorkspace!.appId) // Create a row directly in production to simulate live data const productionRow = await config.withHeaders( { [constants.Header.APP_ID]: config.getProdWorkspaceId() }, async () => await config.api.row.save(table._id!, { tableId: table._id!, name: "Prod row", description: "Prod description", [amountFieldName]: 5, }) ) const formulaFieldName = "amountPlusOne" const formula = "{{ add amount 1 }}" const updatedTable = cloneDeep(table) updatedTable.schema[formulaFieldName] = { name: formulaFieldName, type: FieldType.FORMULA, formula, formulaType: FormulaType.STATIC, responseType: FieldType.NUMBER, } await config.api.table.save(updatedTable) await config.api.workspace.publish(config.devWorkspace!.appId) const prodRowAfterPublish = await config.withHeaders( { [constants.Header.APP_ID]: config.getProdWorkspaceId() }, async () => await config.api.row.get(table._id!, productionRow._id!) ) expect(prodRowAfterPublish[formulaFieldName]).toBe(6) }) it("migrates production row data when a column is renamed in development", async () => { const table = await config.api.table.save(basicTable()) await config.api.row.save(table._id!, { name: "Test Row", description: "original value", }) await config.api.workspace.publish(config.devWorkspace!.appId) const renamedSchema = { ...table.schema, details: { ...table.schema.description, name: "details", }, } // casting to any here because TS can't infer the description property properly. delete (renamedSchema as any).description const renamedTable = await config.api.table.save({ ...table, schema: renamedSchema, _rename: { old: "description", updated: "details" }, }) const devRows = await config.api.row.search(renamedTable._id!, { query: {}, }) expect(devRows.rows[0].details).toBe("original value") await config.api.workspace.publish(config.devWorkspace!.appId) await config.withProdApp(async () => { const prodRows = await config.api.row.search(renamedTable._id!, { query: {}, }) expect(prodRows.rows[0].details).toBe("original value") expect(prodRows.rows[0].description).toBeUndefined() }) }) it("applies pending renames even when the replicated schema is stale", async () => { const table = await config.api.table.save(basicTable()) await config.api.row.save(table._id!, { name: "Test Row", description: "original value", }) await config.api.workspace.publish(config.devWorkspace!.appId) const rename = { old: "description", updated: "details" } const renamedSchema = { ...table.schema, details: { ...table.schema.description, name: "details", }, } delete (renamedSchema as any).description const renamedTable = await config.api.table.save({ ...table, schema: renamedSchema, _rename: rename, }) // Simulating that we got a stale schema that still uses the old column. await config.doInContext(config.getDevWorkspaceId(), async () => { const db = context.getWorkspaceDB() const tableDoc = await db.tryGet<Table>(renamedTable._id!) if (tableDoc) { tableDoc.schema = { ...tableDoc.schema, description: { ...table.schema.description, name: "description" }, } delete tableDoc.schema.details await db.put(tableDoc) } const rows = ( await db.allDocs<Row>( getRowParams(renamedTable._id!, null, { include_docs: true }) ) ).rows.map(row => row.doc!) await db.bulkDocs( rows.map(row => { const updated: Row = { ...row, description: row.details || row.description, } delete updated.details return updated }) ) }) await config.api.workspace.publish(config.devWorkspace!.appId) await config.withProdApp(async () => { const prodRows = await config.api.row.search(renamedTable._id!, { query: {}, }) expect(prodRows.rows[0].details).toBe("original value") expect(prodRows.rows[0].description).toBeUndefined() const prodTable = await config.api.table.get(renamedTable._id!) expect(prodTable.pendingColumnRenames || []).toHaveLength(0) expect(prodTable.schema.details).toBeDefined() }) }) })