Skip to content

Skip migrations doc on replication #16663

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions packages/backend-core/src/db/Replication.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { DocumentType } from "@budibase/types"
import { DesignDocuments } from "../constants"
import Replication from "./Replication"

const mockSourceDb = {
replicate: {
to: jest.fn(),
},
name: "source_db",
}

const mockTargetDb = {
destroy: jest.fn(),
name: "target_db",
}

jest.mock("./couch", () => ({
getPouchDB: jest.fn((name: string) =>
name === mockSourceDb.name ? mockSourceDb : mockTargetDb
),
}))

describe("Replication", () => {
describe("appReplicateOpts", () => {
it("should skip migrations document when not a creation", () => {
const replication = new Replication({
source: `${DocumentType.APP_DEV}_source`,
target: `${DocumentType.APP}_target`,
})

const opts = replication.appReplicateOpts({})

const migrationsDoc = {
_id: DesignDocuments.MIGRATIONS,
type: "migration",
}

// Should default to false (skip migrations)
expect((opts.filter as Function)(migrationsDoc, {})).toBe(false)
})

it("should skip migrations document when isCreation is set to false", () => {
const replication = new Replication({
source: `${DocumentType.APP_DEV}_source`,
target: `${DocumentType.APP}_target`,
})

const opts = replication.appReplicateOpts({ isCreation: false })

const migrationsDoc = {
_id: DesignDocuments.MIGRATIONS,
type: "migration",
}

expect((opts.filter as Function)(migrationsDoc, {})).toBe(false)
expect(opts).not.toHaveProperty("isCreation")
})

it("should include migrations document on creation", () => {
const replication = new Replication({
source: `${DocumentType.APP_DEV}_source`,
target: `${DocumentType.APP}_target`,
})

const opts = replication.appReplicateOpts({ isCreation: true })

const migrationsDoc = {
_id: DesignDocuments.MIGRATIONS,
type: "migration",
}

expect((opts.filter as Function)(migrationsDoc, {})).toBe(true)
expect(opts).not.toHaveProperty("isCreation")
})

it("should always replicate deleted documents", () => {
const replication = new Replication({
source: `${DocumentType.APP_DEV}_source`,
target: `${DocumentType.APP}_target`,
})

const opts = replication.appReplicateOpts({ isCreation: false })

const deletedDoc = {
_id: "some_doc",
_deleted: true,
}

expect((opts.filter as Function)(deletedDoc, {})).toBe(true)
})

it("should filter out automation logs", () => {
const replication = new Replication({
source: `${DocumentType.APP_DEV}_source`,
target: `${DocumentType.APP}_target`,
})

const opts = replication.appReplicateOpts({ isCreation: true })

const automationLogDoc = {
_id: `${DocumentType.AUTOMATION_LOG}_123`,
type: "automation_log",
}

expect((opts.filter as Function)(automationLogDoc, {})).toBe(false)
})

it("should filter out app metadata", () => {
const replication = new Replication({
source: `${DocumentType.APP_DEV}_source`,
target: `${DocumentType.APP}_target`,
})

const opts = replication.appReplicateOpts({ isCreation: true })

const appMetadataDoc = {
_id: DocumentType.APP_METADATA,
type: "app_metadata",
}

expect((opts.filter as Function)(appMetadataDoc, {})).toBe(false)
})

it("should filter out design documents when replicating to dev", () => {
const replication = new Replication({
source: `${DocumentType.APP}_source`,
target: `${DocumentType.APP_DEV}_target`,
})

const opts = replication.appReplicateOpts({ isCreation: true })

const designDoc = {
_id: "_design/database",
type: "design_doc",
}

expect((opts.filter as Function)(designDoc, {})).toBe(false)
})

it("should include design documents when replicating to production", () => {
const replication = new Replication({
source: `${DocumentType.APP_DEV}_source`,
target: `${DocumentType.APP}_target`,
})

const opts = replication.appReplicateOpts({ isCreation: true })

const designDoc = {
_id: "_design/database",
type: "design_doc",
}

expect((opts.filter as Function)(designDoc, {})).toBe(true)
})

it("should use custom filter when provided", () => {
const customFilter = jest.fn().mockReturnValue(false)

const replication = new Replication({
source: `${DocumentType.APP_DEV}_source`,
target: `${DocumentType.APP}_target`,
})

const opts = replication.appReplicateOpts({
isCreation: true,
filter: customFilter,
})

const regularDoc = {
_id: "regular_doc",
type: "regular",
}

const result = (opts.filter as Function)(regularDoc, {})

expect(customFilter).toHaveBeenCalledWith(regularDoc, {})
expect(result).toBe(false)
})

it("should return opts unchanged when filter is string", () => {
const replication = new Replication({
source: `${DocumentType.APP_DEV}_source`,
target: `${DocumentType.APP}_target`,
})

const inputOpts = {
filter: "design/myfilter",
isCreation: true,
}

const opts = replication.appReplicateOpts(inputOpts)

expect(opts).toBe(inputOpts)
})
})
})
9 changes: 8 additions & 1 deletion packages/backend-core/src/db/Replication.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import PouchDB from "pouchdb"
import { getPouchDB, closePouchDB } from "./couch"
import { DocumentType, Document } from "@budibase/types"
import { DesignDocuments } from "../constants"

enum ReplicationDirection {
TO_PRODUCTION = "toProduction",
Expand Down Expand Up @@ -50,7 +51,7 @@ class Replication {
}

appReplicateOpts(
opts: PouchDB.Replication.ReplicateOptions = {}
opts: PouchDB.Replication.ReplicateOptions & { isCreation?: boolean } = {}
): PouchDB.Replication.ReplicateOptions {
if (typeof opts.filter === "string") {
return opts
Expand All @@ -61,9 +62,15 @@ class Replication {
const toDev = direction === ReplicationDirection.TO_DEV
delete opts.filter

const isCreation = opts.isCreation
delete opts.isCreation

return {
...opts,
filter: (doc: Document, params: any) => {
if (!isCreation && doc._id === DesignDocuments.MIGRATIONS) {
return false
}
// don't sync design documents
if (toDev && doc._id?.startsWith("_design")) {
return false
Expand Down
6 changes: 5 additions & 1 deletion packages/server/src/api/controllers/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ export const publishApp = async function (
const devAppId = dbCore.getDevelopmentAppID(appId)
const productionAppId = dbCore.getProdAppID(appId)

const isPublished = await sdk.applications.isAppPublished(productionAppId)

// don't try this if feature isn't allowed, will error
if (await backups.isEnabled()) {
// trigger backup initially
Expand All @@ -209,7 +211,9 @@ export const publishApp = async function (
replication = new dbCore.Replication(config)
const devDb = context.getDevAppDB()
await devDb.compact()
await replication.replicate(replication.appReplicateOpts({}))
await replication.replicate(
replication.appReplicateOpts({ isCreation: !isPublished })
)
// app metadata is excluded as it is likely to be in conflict
// replicate the app metadata document manually
const db = context.getProdAppDB()
Expand Down
Loading