Skip to content

Commit 0eaa5a3

Browse files
dkrizanclaude
andcommitted
feat: add branch support to CDN content delivery configs (#3466)
## Summary - Links CDN configs to branches via a foreign key (`branch_id`) instead of the plain `filterBranch` string column - Adds branch selector to the CDN config dialog when branching is enabled - Shows branch name chip on CDN list items - Cleans up CDN configs when a branch is deleted - Migrates existing configs to the default branch during branching enablement - Feature guard rejects CDN create/update with `filterBranch` when BRANCHING feature is disabled ## Test plan - [x] All 12 `ContentDeliveryConfigBranchingTest` tests pass, including: - Create/update CDN config with specific branch - Create without branch defaults to default branch - Non-existent branch returns 404 - Delete branch cascades to CDN configs - Feature guard rejects create when branching not enabled - Feature guard rejects update when branching not enabled 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f22a786 commit 0eaa5a3

20 files changed

Lines changed: 538 additions & 11 deletions

File tree

backend/api/src/main/kotlin/io/tolgee/hateoas/contentDelivery/ContentDeliveryConfigModel.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.tolgee.hateoas.contentDelivery
22

3+
import io.swagger.v3.oas.annotations.media.Schema
34
import io.tolgee.dtos.IExportParams
45
import io.tolgee.formats.ExportFormat
56
import io.tolgee.formats.ExportMessageFormat
@@ -23,6 +24,10 @@ class ContentDeliveryConfigModel(
2324
val lastPublished: Long?,
2425
val lastPublishedFiles: Collection<String>,
2526
override var escapeHtml: Boolean?,
27+
@Schema(
28+
description = "Branch name this CDN config is associated with. Null means default branch or no branching.",
29+
)
30+
val branchName: String? = null,
2631
) : RepresentationModel<ContentDeliveryConfigModel>(),
2732
Serializable,
2833
IExportParams {
@@ -40,5 +45,7 @@ class ContentDeliveryConfigModel(
4045
override var messageFormat: ExportMessageFormat? = null
4146
override var supportArrays: Boolean = false
4247
override var fileStructureTemplate: String? = null
43-
override var filterBranch: String? = null
48+
override var filterBranch: String?
49+
get() = branchName
50+
set(_) {} // no-op; branchName is the source of truth
4451
}

backend/api/src/main/kotlin/io/tolgee/hateoas/contentDelivery/ContentDeliveryConfigModelAssembler.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class ContentDeliveryConfigModelAssembler(
2828
lastPublished = entity.lastPublished?.time,
2929
lastPublishedFiles = entity.lastPublishedFiles ?: listOf(),
3030
escapeHtml = entity.escapeHtml,
31+
branchName = entity.branch?.name,
3132
).also {
3233
it.copyPropsFrom(entity)
3334
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.tolgee.development.testDataBuilder.data
2+
3+
import io.tolgee.model.automations.AutomationAction
4+
import io.tolgee.model.automations.AutomationTrigger
5+
import io.tolgee.model.automations.AutomationTriggerType
6+
import io.tolgee.model.branching.Branch
7+
8+
class ContentDeliveryConfigBranchingTestData : BaseTestData() {
9+
var mainBranch: Branch
10+
var featureBranch: Branch
11+
12+
val mainBranchCdnConfig =
13+
projectBuilder.addContentDeliveryConfig {
14+
name = "Main CDN"
15+
}
16+
17+
val featureBranchCdnConfig =
18+
projectBuilder.addContentDeliveryConfig {
19+
name = "Feature CDN"
20+
}
21+
22+
val defaultServerContentDeliveryConfig =
23+
projectBuilder.addContentDeliveryConfig {
24+
name = "Default server"
25+
}
26+
27+
val automation =
28+
projectBuilder.addAutomation {
29+
this.triggers.add(
30+
AutomationTrigger(this)
31+
.also { it.type = AutomationTriggerType.TRANSLATION_DATA_MODIFICATION },
32+
)
33+
this.actions.add(
34+
AutomationAction(this).also { it.contentDeliveryConfig = defaultServerContentDeliveryConfig.self },
35+
)
36+
}
37+
38+
val keyWithTranslation =
39+
this.projectBuilder.addKey("key") {
40+
addTranslation("en", "Hello")
41+
}
42+
43+
init {
44+
projectBuilder.apply {
45+
self.useBranching = true
46+
47+
mainBranch =
48+
addBranch {
49+
name = "main"
50+
project = projectBuilder.self
51+
isProtected = true
52+
isDefault = true
53+
}.self
54+
55+
featureBranch =
56+
addBranch {
57+
name = "feature"
58+
project = projectBuilder.self
59+
isProtected = false
60+
isDefault = false
61+
originBranch = mainBranch
62+
}.self
63+
64+
mainBranchCdnConfig.self.branch = mainBranch
65+
featureBranchCdnConfig.self.branch = featureBranch
66+
defaultServerContentDeliveryConfig.self.branch = mainBranch
67+
68+
keyWithTranslation.self.branch = mainBranch
69+
}
70+
}
71+
}

backend/data/src/main/kotlin/io/tolgee/model/contentDelivery/ContentDeliveryConfig.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import io.tolgee.formats.ExportMessageFormat
1111
import io.tolgee.model.Project
1212
import io.tolgee.model.StandardAuditModel
1313
import io.tolgee.model.automations.AutomationAction
14+
import io.tolgee.model.branching.Branch
15+
import io.tolgee.model.branching.EntityWithBranch
1416
import io.tolgee.model.enums.TranslationState
1517
import jakarta.persistence.Column
1618
import jakarta.persistence.Entity
@@ -21,6 +23,7 @@ import jakarta.persistence.Index
2123
import jakarta.persistence.ManyToOne
2224
import jakarta.persistence.OneToMany
2325
import jakarta.persistence.Table
26+
import jakarta.persistence.Transient
2427
import org.hibernate.annotations.ColumnDefault
2528
import org.hibernate.annotations.Type
2629
import java.util.Date
@@ -31,13 +34,15 @@ import java.util.Date
3134
indexes = [
3235
Index(columnList = "project_id"),
3336
Index(columnList = "content_storage_id"),
37+
Index(columnList = "branch_id"),
3438
],
3539
)
3640
class ContentDeliveryConfig(
3741
@ManyToOne(fetch = FetchType.LAZY)
3842
var project: Project,
3943
) : StandardAuditModel(),
40-
IExportParams {
44+
IExportParams,
45+
EntityWithBranch {
4146
@ActivityLoggedProp
4247
@ActivityDescribingProp
4348
lateinit var name: String
@@ -139,6 +144,15 @@ class ContentDeliveryConfig(
139144
@ActivityLoggedProp
140145
override var escapeHtml: Boolean? = false
141146

142-
@ActivityLoggedProp
143-
override var filterBranch: String? = null
147+
@get:Transient
148+
override var filterBranch: String?
149+
get() = branch?.name
150+
set(value) {} // no-op; branch FK is the source of truth
151+
152+
@ManyToOne(fetch = FetchType.LAZY, optional = true)
153+
var branch: Branch? = null
154+
155+
override fun resolveBranch(): Branch? = branch
156+
157+
override fun resolveProject(): Project? = project
144158
}

backend/data/src/main/kotlin/io/tolgee/repository/contentDelivery/ContentDeliveryConfigRepository.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface ContentDeliveryConfigRepository : JpaRepository<ContentDeliveryConfig?
2424
"""
2525
from ContentDeliveryConfig e
2626
left join fetch e.automationActions
27+
left join fetch e.branch
2728
where e.project.id in :projectId
2829
""",
2930
countQuery = """
@@ -50,4 +51,17 @@ interface ContentDeliveryConfigRepository : JpaRepository<ContentDeliveryConfig?
5051
): ContentDeliveryConfig
5152

5253
fun countByProject(project: Project): Int
54+
55+
@Query(
56+
"""
57+
from ContentDeliveryConfig e
58+
left join fetch e.automationActions
59+
left join fetch e.branch
60+
where e.project.id = :projectId and e.branch.id = :branchId
61+
""",
62+
)
63+
fun findAllByProjectIdAndBranchId(
64+
projectId: Long,
65+
branchId: Long,
66+
): List<ContentDeliveryConfig>
5367
}

backend/data/src/main/kotlin/io/tolgee/service/contentDelivery/ContentDeliveryConfigService.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@ import io.tolgee.model.contentDelivery.ContentDeliveryConfig
1313
import io.tolgee.model.contentDelivery.ContentStorage
1414
import io.tolgee.repository.contentDelivery.ContentDeliveryConfigRepository
1515
import io.tolgee.service.automations.AutomationService
16+
import io.tolgee.service.branching.BranchService
17+
import io.tolgee.service.project.ProjectFeatureGuard
1618
import io.tolgee.service.project.ProjectService
1719
import io.tolgee.util.SlugGenerator
1820
import jakarta.persistence.EntityManager
19-
import jakarta.transaction.Transactional
2021
import org.springframework.context.annotation.Lazy
2122
import org.springframework.data.domain.Page
2223
import org.springframework.data.domain.Pageable
2324
import org.springframework.stereotype.Service
25+
import org.springframework.transaction.annotation.Transactional
2426
import kotlin.random.Random
2527

2628
@Suppress("SpringJavaInjectionPointsAutowiringInspection")
@@ -35,17 +37,22 @@ class ContentDeliveryConfigService(
3537
@Lazy
3638
private val contentDeliveryUploader: ContentDeliveryUploader,
3739
private val enabledFeaturesProvider: EnabledFeaturesProvider,
40+
@Lazy
41+
private val branchService: BranchService,
42+
private val projectFeatureGuard: ProjectFeatureGuard,
3843
) {
3944
@Transactional
4045
fun create(
4146
projectId: Long,
4247
dto: ContentDeliveryConfigRequest,
4348
): ContentDeliveryConfig {
49+
projectFeatureGuard.checkIfUsed(Feature.BRANCHING, dto.filterBranch)
4450
val project = entityManager.getReference(Project::class.java, projectId)
4551
checkMultipleConfigsFeature(project)
4652
val config = ContentDeliveryConfig(project)
4753
config.name = dto.name
4854
config.contentStorage = getStorage(projectId, dto.contentStorageId)
55+
config.branch = branchService.getActiveOrDefault(projectId, dto.filterBranch)
4956
config.copyPropsFrom(dto)
5057
setSlugForCreation(config, dto)
5158
config.pruneBeforePublish = dto.pruneBeforePublish
@@ -130,13 +137,15 @@ class ContentDeliveryConfigService(
130137
id: Long,
131138
dto: ContentDeliveryConfigRequest,
132139
): ContentDeliveryConfig {
140+
projectFeatureGuard.checkIfUsed(Feature.BRANCHING, dto.filterBranch)
133141
checkMultipleConfigsFeature(projectService.get(projectId), maxCurrentAllowed = 1)
134142
val config = get(projectId, id)
135143
handleUpdateSlug(config, dto)
136144
config.contentStorage = getStorage(projectId, dto.contentStorageId)
137145
config.name = dto.name
138146
config.pruneBeforePublish = dto.pruneBeforePublish
139147
config.zip = dto.zip
148+
config.branch = branchService.getActiveOrDefault(projectId, dto.filterBranch)
140149
config.copyPropsFrom(dto)
141150
handleUpdateAutoPublish(dto, config)
142151
return save(config)
@@ -214,4 +223,18 @@ class ContentDeliveryConfigService(
214223
fun save(config: ContentDeliveryConfig): ContentDeliveryConfig {
215224
return contentDeliveryConfigRepository.save(config)
216225
}
226+
227+
@Transactional
228+
fun deleteAllByBranchId(
229+
projectId: Long,
230+
branchId: Long,
231+
) {
232+
val configs = contentDeliveryConfigRepository.findAllByProjectIdAndBranchId(projectId, branchId)
233+
configs.forEach { config ->
234+
config.automationActions.map { it.automation }.forEach {
235+
automationService.delete(it)
236+
}
237+
contentDeliveryConfigRepository.deleteById(config.id)
238+
}
239+
}
217240
}

backend/data/src/main/resources/db/changelog/schema.xml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5057,4 +5057,25 @@
50575057
</column>
50585058
</addColumn>
50595059
</changeSet>
5060+
<changeSet author="danielkrizan (generated)" id="1770751028716-1">
5061+
<addColumn tableName="content_delivery_config">
5062+
<column name="branch_id" type="BIGINT"/>
5063+
</addColumn>
5064+
</changeSet>
5065+
<changeSet author="danielkrizan (generated)" id="1770751028716-2">
5066+
<createIndex indexName="IDXmpg39yyj1lqn91onyyb64471v" tableName="content_delivery_config">
5067+
<column name="branch_id"/>
5068+
</createIndex>
5069+
</changeSet>
5070+
<changeSet author="danielkrizan (generated)" id="1770751028716-3">
5071+
<addForeignKeyConstraint baseColumnNames="branch_id" baseTableName="content_delivery_config" constraintName="FK5jtc226unswiodyqp8r4isds6" deferrable="false" initiallyDeferred="false" referencedColumnNames="id" referencedTableName="branch" validate="true"/>
5072+
</changeSet>
5073+
<changeSet author="danielkrizan (generated)" id="1770751028716-4">
5074+
<dropColumn columnName="filter_branch" tableName="content_delivery_config"/>
5075+
<rollback>
5076+
<addColumn tableName="content_delivery_config">
5077+
<column name="filter_branch" type="VARCHAR(255)"/>
5078+
</addColumn>
5079+
</rollback>
5080+
</changeSet>
50605081
</databaseChangeLog>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.tolgee.controllers.internal.e2eData
2+
3+
import io.tolgee.controllers.internal.InternalController
4+
import io.tolgee.development.testDataBuilder.builders.TestDataBuilder
5+
import io.tolgee.development.testDataBuilder.data.ContentDeliveryConfigBranchingTestData
6+
7+
@InternalController(["internal/e2e-data/content-delivery-branching"])
8+
class ContentDeliveryBranchingE2eDataController : AbstractE2eDataController() {
9+
override val testData: TestDataBuilder
10+
get() {
11+
val data = ContentDeliveryConfigBranchingTestData()
12+
return data.root
13+
}
14+
}

e2e/cypress/common/apiCalls/testData/testData.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ export const languagePermissionsData = generateTestDataObject(
6767
export const contentDeliveryTestData =
6868
generateTestDataObject('content-delivery');
6969

70+
export const contentDeliveryBranchingTestData = generateTestDataObject(
71+
'content-delivery-branching'
72+
);
73+
7074
export const generateExampleKeys = (
7175
projectId: number,
7276
numberOfExamples: number

0 commit comments

Comments
 (0)