Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class SingleStepImportController(
fun singleStepResolvableImport(
@RequestBody @Valid params: SingleStepImportResolvableRequest,
): ImportResult {
projectFeatureGuard.checkIfUsed(Feature.BRANCHING, params.branch)
return singleStepImportService.singleStepImportResolvable(
project = projectHolder.projectEntity,
userAccount = authenticationFacade.authenticatedUserEntity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,11 @@ class KeyController(
fun importKeys(
@RequestBody @Valid
dto: ImportKeysResolvableDto,
@RequestParam branch: String? = null,
): KeyImportResolvableResultModel {
projectFeatureGuard.checkIfUsed(Feature.BRANCHING, branch)
val uploadedImageToScreenshotMap =
keyService.importKeysResolvable(dto.keys, projectHolder.projectEntity)
keyService.importKeysResolvable(dto.keys, projectHolder.projectEntity, branch)
val screenshots =
uploadedImageToScreenshotMap.screenshots
.map { (uploadedImageId, screenshot) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ data class SingleStepImportResolvableRequest(
"Unresolved conflicts are reported in the `params` of the error response",
)
val errorOnUnresolvedConflict: Boolean? = null,
@field:Schema(
description = "Branch to import keys into. If not specified, default branch is used.",
)
val branch: String? = null,
@get:Schema(
description = "List of keys to import",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class SingleStepImportService(
keysToFilesManager.processKeys(params.keys)

val request = SingleStepImportRequest()
request.branch = params.branch
request.overrideMode = params.overrideMode ?: OverrideMode.RECOMMENDED
request.errorOnUnresolvedConflict = params.errorOnUnresolvedConflict

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,12 +539,14 @@ class KeyService(
fun importKeysResolvable(
keys: List<ImportKeysResolvableItemDto>,
projectEntity: Project,
branch: String? = null,
): KeyImportResolvableResult {
val importer =
ResolvingKeyImporter(
applicationContext = applicationContext,
keysToImport = keys,
projectEntity = projectEntity,
branch = branch,
)
return importer()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import io.tolgee.model.Language
import io.tolgee.model.Project
import io.tolgee.model.Project_
import io.tolgee.model.Screenshot
import io.tolgee.model.branching.Branch
import io.tolgee.model.branching.Branch_
import io.tolgee.model.enums.Scope
import io.tolgee.model.key.Key
import io.tolgee.model.key.Key_
Expand All @@ -37,6 +39,7 @@ class ResolvingKeyImporter(
val applicationContext: ApplicationContext,
val keysToImport: List<ImportKeysResolvableItemDto>,
val projectEntity: Project,
val branch: String? = null,
) {
private val entityManager = applicationContext.getBean(EntityManager::class.java)
private val keyService = applicationContext.getBean(KeyService::class.java)
Expand Down Expand Up @@ -317,7 +320,7 @@ class ResolvingKeyImporter(
project = projectEntity,
name = keyToImport.name,
namespace = keyToImport.namespace,
branch = keyToImport.branch,
branch = branch ?: keyToImport.branch,
isPlural = false,
)
}
Expand All @@ -335,6 +338,22 @@ class ResolvingKeyImporter(
@Suppress("UNCHECKED_CAST")
val namespaceJoin: Join<Key, Namespace> = root.fetch(Key_.namespace, JoinType.LEFT) as Join<Key, Namespace>

@Suppress("UNCHECKED_CAST")
val branchJoin: Join<Key, Branch> = root.fetch(Key_.branch, JoinType.LEFT) as Join<Key, Branch>

val branchPredicate =
if (branch.isNullOrEmpty()) {
cb.or(
branchJoin.get(Branch_.id).isNull,
cb.isTrue(branchJoin.get(Branch_.isDefault)),
)
} else {
cb.and(
cb.equal(branchJoin.get(Branch_.name), cb.literal(branch)),
cb.isNull(branchJoin.get(Branch_.deletedAt)),
)
}

val predicates =
keys
.map { (namespace, name) ->
Expand All @@ -347,7 +366,12 @@ class ResolvingKeyImporter(
val projectIdPath = root.get(Key_.project).get(Project_.id)

query.where(
cb.and(cb.equal(projectIdPath, projectId), cb.isNull(root.get(Key_.deletedAt)), cb.or(*predicates)),
cb.and(
cb.equal(projectIdPath, projectId),
cb.isNull(root.get(Key_.deletedAt)),
branchPredicate,
cb.or(*predicates),
),
)

return this.entityManager.createQuery(query).resultList
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,68 @@ class KeyControllerBranchingTest : ProjectAuthControllerTest("/v2/projects/") {
),
).andPrettyPrint.andIsBadRequest.andHasErrorMessage(Message.FEATURE_NOT_ENABLED)
}

@ProjectJWTAuthTestMethod
@Test
fun `import-resolvable imports keys to branch`() {
enabledFeaturesProvider.forceEnabled = setOf(Feature.BRANCHING)
performProjectAuthPost(
"keys/import-resolvable?branch=dev",
mapOf(
"keys" to
listOf(
mapOf(
"name" to "new_resolvable_key",
"translations" to
mapOf(
"en" to
mapOf(
"text" to "hello resolvable",
"resolution" to "NEW",
),
),
),
),
),
).andIsOk

executeInNewTransaction {
val project = projectService.get(testData.project.id)
val key =
project.keys.find {
it.name == "new_resolvable_key" && it.branch?.name == "dev"
}
key.assert.isNotNull
key!!
.translations
.find { it.language.tag == "en" }!!
.text.assert
.isEqualTo("hello resolvable")
}
}

@ProjectJWTAuthTestMethod
@Test
fun `import-resolvable fails when branch specified but feature not enabled`() {
enabledFeaturesProvider.forceEnabled = emptySet()
performProjectAuthPost(
"keys/import-resolvable?branch=feature",
mapOf(
"keys" to
listOf(
mapOf(
"name" to "some_key",
"translations" to
mapOf(
"en" to
mapOf(
"text" to "hello",
"resolution" to "NEW",
),
),
),
),
),
).andIsBadRequest.andHasErrorMessage(Message.FEATURE_NOT_ENABLED)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package io.tolgee.ee.api.v2.controllers.branching

import io.tolgee.ProjectAuthControllerTest
import io.tolgee.constants.Feature
import io.tolgee.constants.Message
import io.tolgee.development.testDataBuilder.data.dataImport.SingleStepImportBranchTestData
import io.tolgee.ee.component.PublicEnabledFeaturesProvider
import io.tolgee.fixtures.andHasErrorMessage
import io.tolgee.fixtures.andIsBadRequest
import io.tolgee.fixtures.andIsOk
import io.tolgee.testing.annotations.ProjectJWTAuthTestMethod
import io.tolgee.testing.assert
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired

@Suppress("SpringJavaInjectionPointsAutowiringInspection")
class SingleStepImportResolvableBranchingTest : ProjectAuthControllerTest("/v2/projects/") {
lateinit var testData: SingleStepImportBranchTestData

@Autowired
lateinit var enabledFeaturesProvider: PublicEnabledFeaturesProvider

@BeforeEach
fun setup() {
testData = SingleStepImportBranchTestData()
testDataService.saveTestData(testData.root)
userAccount = testData.user
projectSupplier = { testData.project }
}

@Test
@ProjectJWTAuthTestMethod
fun `imports new keys to specified branch`() {
enabledFeaturesProvider.forceEnabled = setOf(Feature.BRANCHING)
performProjectAuthPost(
"single-step-import-resolvable",
mapOf(
"branch" to testData.featureBranch.name,
"keys" to
listOf(
mapOf(
"name" to "new_key",
"translations" to
mapOf(
"en" to
mapOf(
"text" to "hello",
"resolution" to "OVERRIDE",
),
),
),
),
),
).andIsOk

executeInNewTransaction {
val key = keyService.getAllByBranch(testData.project.id, "feature").find { it.name == "new_key" }
key.assert.isNotNull
key!!
.translations
.find { it.language.tag == "en" }!!
.text.assert
.isEqualTo("hello")
}
}

@Test
@ProjectJWTAuthTestMethod
fun `imports to default branch when no branch specified`() {
performProjectAuthPost(
"single-step-import-resolvable",
mapOf(
"keys" to
listOf(
mapOf(
"name" to "new_key",
"translations" to
mapOf(
"en" to
mapOf(
"text" to "hello",
"resolution" to "OVERRIDE",
),
),
),
),
),
).andIsOk

executeInNewTransaction {
val key = keyService.getAllByBranch(testData.project.id, "main").find { it.name == "new_key" }
key.assert.isNotNull
key!!
.translations
.find { it.language.tag == "en" }!!
.text.assert
.isEqualTo("hello")
}
}

@Test
@ProjectJWTAuthTestMethod
fun `keys imported to branch are not visible on default branch`() {
enabledFeaturesProvider.forceEnabled = setOf(Feature.BRANCHING)
performProjectAuthPost(
"single-step-import-resolvable",
mapOf(
"branch" to testData.featureBranch.name,
"keys" to
listOf(
mapOf(
"name" to "branch_only_key",
"translations" to
mapOf(
"en" to
mapOf(
"text" to "on branch",
"resolution" to "OVERRIDE",
),
),
),
),
),
).andIsOk

executeInNewTransaction {
val keysOnDefault = keyService.getAllByBranch(testData.project.id, "main")
keysOnDefault.find { it.name == "branch_only_key" }.assert.isNull()

val keysOnFeature = keyService.getAllByBranch(testData.project.id, "feature")
keysOnFeature.find { it.name == "branch_only_key" }.assert.isNotNull
}
}

@Test
@ProjectJWTAuthTestMethod
fun `fails when branch specified but feature not enabled`() {
enabledFeaturesProvider.forceEnabled = emptySet()
performProjectAuthPost(
"single-step-import-resolvable",
mapOf(
"branch" to testData.featureBranch.name,
"keys" to
listOf(
mapOf(
"name" to "some_key",
"translations" to
mapOf(
"en" to
mapOf(
"text" to "hello",
"resolution" to "OVERRIDE",
),
),
),
),
),
).andIsBadRequest.andHasErrorMessage(Message.FEATURE_NOT_ENABLED)
}

@Test
@ProjectJWTAuthTestMethod
fun `succeeds without branch when feature not enabled`() {
enabledFeaturesProvider.forceEnabled = emptySet()
performProjectAuthPost(
"single-step-import-resolvable",
mapOf(
"keys" to
listOf(
mapOf(
"name" to "some_key",
"translations" to
mapOf(
"en" to
mapOf(
"text" to "hello",
"resolution" to "OVERRIDE",
),
),
),
),
),
).andIsOk
}
}
5 changes: 5 additions & 0 deletions webapp/src/service/apiSchema.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5944,6 +5944,8 @@ export interface components {
};
};
SingleStepImportResolvableRequest: {
/** @description Branch to import keys into. If not specified, default branch is used. */
branch?: string;
/**
* @description If `false`, import will apply all `non-failed` overrides and reports `unresolvedConflict`
* .If `true`, import will fail completely on unresolved conflict and won't apply any changes. Unresolved conflicts are reported in the `params` of the error response
Expand Down Expand Up @@ -15361,6 +15363,9 @@ export interface operations {
*/
importKeys: {
parameters: {
query: {
branch?: string;
};
path: {
projectId: number;
};
Expand Down
Loading