Skip to content
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

feat: Single step import & export enhancements for CLI #2142

Merged
merged 35 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6bd4e40
feat: Single step import
JanCizmar Feb 25, 2024
e463a53
feat: Simple single step import & test
JanCizmar Apr 29, 2024
0096220
feat: Generate schema
JanCizmar Apr 29, 2024
ba19baf
feat: Import params are respected
JanCizmar May 2, 2024
f93c600
chore: Fix the detection
JanCizmar May 2, 2024
ad0787a
fix: Fix all
JanCizmar May 2, 2024
2871c05
docs: Add the docs for the schema
JanCizmar May 2, 2024
2a83198
docs: Fix the docs and param objects
JanCizmar May 2, 2024
c5e3b24
doc: Add additional tag export filters
JanCizmar May 3, 2024
5223b67
doc: Add docs
JanCizmar May 3, 2024
205c866
fix: Push languageMappings to upper level
JanCizmar May 3, 2024
bed2070
fix: Push languageMappings to upper level > docs
JanCizmar May 3, 2024
b1f3a54
fix: Export structure templates
JanCizmar May 3, 2024
77ec83d
fix: Export structure templates
JanCizmar May 6, 2024
e6bd907
fix: All exporters and tests
JanCizmar May 6, 2024
2877e1c
fix: Tests & Update schema
JanCizmar May 6, 2024
bcaa256
fix: Update docs
JanCizmar May 6, 2024
ad6192b
fix: Mapping with null namespace is used
JanCizmar May 6, 2024
99fa4f5
fix: Throw on conflict, prevent data from saving
JanCizmar May 7, 2024
37ee457
feat: Proper tag filtering and complex tagging endpoint & tests
JanCizmar May 14, 2024
d6afa8d
fix: Key complex edit activity
JanCizmar May 14, 2024
acbb143
fix: Tags controller key, project check & make untagging faster
JanCizmar May 15, 2024
eb6a568
fix: Wildcards support & fixes, tests
JanCizmar May 15, 2024
41b3d48
feat: correctly type error response
stepan662 May 23, 2024
8cf1675
feat: Add filter key not
JanCizmar May 23, 2024
6d36c0a
fix: Always use fresh entity from existingKeys
JanCizmar May 24, 2024
2806f7d
feat: Wildcard untagging
JanCizmar May 24, 2024
d167eda
feat: Single step import > tag new keys
JanCizmar May 24, 2024
6d0dada
fix: swagger for organizations endpoint
stepan662 May 24, 2024
e9e7e84
chore: remove unnecessary option from config
stepan662 May 29, 2024
046266d
fix: FE schema types
stepan662 May 29, 2024
4fa0193
feat: ability to remove other keys when importing (#2328)
stepan662 May 30, 2024
f2553d9
chore: Fix test
JanCizmar May 31, 2024
7831eb1
chore: Add docs
JanCizmar May 31, 2024
8b6fa42
fix: Self CR fixes
JanCizmar May 31, 2024
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
@@ -0,0 +1,28 @@
package io.tolgee.api.v2.controllers

import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.formats.ExportFormat
import io.tolgee.hateoas.exportInfo.ExportFormatModel
import io.tolgee.hateoas.exportInfo.ExportFormatModelAssembler
import org.springframework.hateoas.CollectionModel
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@CrossOrigin(origins = ["*"])
@RequestMapping(
value = [
"/v2/public/export-info",
],
)
@Tag(name = "Export info")
class ExportInfoController(
private val exportFormatModelAssembler: ExportFormatModelAssembler,
) {
@GetMapping(value = ["/formats"])
fun get(): CollectionModel<ExportFormatModel> {
return exportFormatModelAssembler.toCollectionModel(ExportFormat.entries)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,15 @@ import io.tolgee.activity.RequestActivity
import io.tolgee.activity.data.ActivityType
import io.tolgee.api.v2.hateoas.invitation.TagModel
import io.tolgee.api.v2.hateoas.invitation.TagModelAssembler
import io.tolgee.dtos.request.ComplexTagKeysRequest
import io.tolgee.dtos.request.key.TagKeyDto
import io.tolgee.exceptions.BadRequestException
import io.tolgee.exceptions.NotFoundException
import io.tolgee.model.enums.Scope
import io.tolgee.model.key.Key
import io.tolgee.model.key.Tag
import io.tolgee.openApiDocs.OpenApiOrderExtension
import io.tolgee.security.ProjectHolder
import io.tolgee.security.authentication.AllowApiAccess
import io.tolgee.security.authorization.RequiresProjectPermissions
import io.tolgee.security.authorization.UseDefaultPermissions
import io.tolgee.service.key.KeyService
import io.tolgee.service.key.TagService
import jakarta.validation.Valid
import org.springdoc.core.annotations.ParameterObject
Expand Down Expand Up @@ -47,7 +44,6 @@ import io.swagger.v3.oas.annotations.tags.Tag as OpenApiTag
@OpenApiTag(name = "Tags", description = "Manipulates key tags")
@OpenApiOrderExtension(6)
class TagsController(
private val keyService: KeyService,
private val projectHolder: ProjectHolder,
private val tagService: TagService,
private val tagModelAssembler: TagModelAssembler,
Expand All @@ -67,9 +63,7 @@ class TagsController(
@Valid @RequestBody
tagKeyDto: TagKeyDto,
): TagModel {
val key = keyService.findOptional(keyId).orElseThrow { NotFoundException() }
key.checkInProject()
return tagService.tagKey(key, tagKeyDto.name.trim()).model
return tagService.tagKey(projectHolder.project.id, keyId, tagKeyDto.name.trim()).model
}

@DeleteMapping(value = ["keys/{keyId:[0-9]+}/tags/{tagId:[0-9]+}"])
Expand All @@ -81,11 +75,7 @@ class TagsController(
@PathVariable keyId: Long,
@PathVariable tagId: Long,
) {
val key = keyService.findOptional(keyId).orElseThrow { NotFoundException() }
val tag = tagService.find(tagId) ?: throw NotFoundException()
tag.checkInProject()
key.checkInProject()
return tagService.remove(key, tag)
return tagService.removeTag(projectHolder.project.id, keyId, tagId)
}

@GetMapping(value = ["tags"])
Expand All @@ -100,14 +90,15 @@ class TagsController(
return pagedResourcesAssembler.toModel(data, tagModelAssembler)
}

private fun Key.checkInProject() {
keyService.checkInProject(this, projectHolder.project.id)
}

private fun Tag.checkInProject() {
if (this.project.id != projectHolder.project.id) {
throw BadRequestException(io.tolgee.constants.Message.TAG_NOT_FROM_PROJECT)
}
@PutMapping("tag-complex")
@Operation(summary = "Execute complex tag operation")
@AllowApiAccess
@RequiresProjectPermissions([Scope.KEYS_EDIT])
@RequestActivity(ActivityType.COMPLEX_TAG_OPERATION)
fun executeComplexTagOperation(
@RequestBody req: ComplexTagKeysRequest,
) {
tagService.complexTagOperation(projectHolder.project.id, req)
}

private val Tag.model: TagModel
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (c) 2020. Tolgee
*/

package io.tolgee.api.v2.controllers.dataImport

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Encoding
import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.tolgee.dtos.dataImport.ImportFileDto
import io.tolgee.dtos.request.SingleStepImportRequest
import io.tolgee.model.enums.Scope
import io.tolgee.openApiDocs.OpenApiOrderExtension
import io.tolgee.security.ProjectHolder
import io.tolgee.security.authentication.AllowApiAccess
import io.tolgee.security.authentication.AuthenticationFacade
import io.tolgee.security.authorization.RequiresProjectPermissions
import io.tolgee.service.dataImport.ImportService
import io.tolgee.util.Logging
import io.tolgee.util.filterFiles
import jakarta.validation.Valid
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile

@Suppress("MVCPathVariableInspection")
@RestController
@CrossOrigin(origins = ["*"])
@RequestMapping(value = ["/v2/projects/{projectId:\\d+}/single-step-import", "/v2/projects/single-step-import"])
@ImportDocsTag
class SingleStepImportController(
private val importService: ImportService,
private val authenticationFacade: AuthenticationFacade,
private val projectHolder: ProjectHolder,
) : Logging {
@PostMapping("", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
@Operation(
summary = "Single step import",
description =
"Unlike the /v2/projects/{projectId}/import endpoint, " +
"imports the data in single request by provided files and parameters. " +
"This is useful for automated importing via API or CLI.",
)
@RequiresProjectPermissions([Scope.TRANSLATIONS_VIEW])
@RequestBody(
content =
[
Content(
encoding = [
Encoding(name = "params", contentType = "application/json"),
],
),
],
)
@AllowApiAccess
@OpenApiOrderExtension(1)
fun doImport(
@RequestPart("files")
files: Array<MultipartFile>,
@RequestPart
@Valid params: SingleStepImportRequest,
) {
val filteredFiles = filterFiles(files.map { (it.originalFilename ?: "") to it })
val fileDtos =
filteredFiles.map {
ImportFileDto(it.originalFilename ?: "", it.inputStream.readAllBytes())
}

importService.singleStepImport(
files = fileDtos,
project = projectHolder.projectEntity,
userAccount = authenticationFacade.authenticatedUserEntity,
params = params,
) {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.tolgee.api.v2.controllers.dataImport

import io.sentry.Sentry
import io.tolgee.exceptions.BadRequestException
import io.tolgee.exceptions.ErrorException
import io.tolgee.exceptions.ErrorResponseBody
import io.tolgee.service.dataImport.status.ImportApplicationStatus
import io.tolgee.service.dataImport.status.ImportApplicationStatusItem
import io.tolgee.util.Logging
import io.tolgee.util.StreamingResponseBodyProvider
import io.tolgee.util.logger
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Component
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody

@Component
class StreamingImportProgressUtil(
private val streamingResponseBodyProvider: StreamingResponseBodyProvider,
) : Logging {
fun stream(
fn: (writeStatus: (status: ImportApplicationStatus) -> Unit) -> Unit,
): ResponseEntity<StreamingResponseBody> {
return streamingResponseBodyProvider.streamNdJson { write ->
val writeStatus = { status: ImportApplicationStatus ->
write(ImportApplicationStatusItem(status))
}
try {
fn(writeStatus)
} catch (e: Exception) {
if (e !is BadRequestException) {
Sentry.captureException(e)
logger.error("Unexpected error while importing", e)
}
when (e) {
is ErrorException ->
write(
ImportApplicationStatusItem(
ImportApplicationStatus.ERROR,
errorStatusCode = e.httpStatus.value(),
errorResponseBody = ErrorResponseBody(e.code, e.params),
),
)

else ->
write(
ImportApplicationStatusItem(
ImportApplicationStatus.ERROR,
errorStatusCode = 500,
),
)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@

package io.tolgee.api.v2.controllers.dataImport

import io.sentry.Sentry
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.tolgee.activity.RequestActivity
import io.tolgee.activity.data.ActivityType
import io.tolgee.dtos.dataImport.ImportAddFilesParams
import io.tolgee.dtos.dataImport.ImportFileDto
import io.tolgee.exceptions.BadRequestException
import io.tolgee.exceptions.ErrorException
import io.tolgee.exceptions.ErrorResponseBody
import io.tolgee.exceptions.NotFoundException
import io.tolgee.hateoas.dataImport.ImportAddFilesResultModel
Expand All @@ -29,13 +26,10 @@ import io.tolgee.security.authentication.AuthenticationFacade
import io.tolgee.security.authorization.RequiresProjectPermissions
import io.tolgee.service.dataImport.ForceMode
import io.tolgee.service.dataImport.ImportService
import io.tolgee.service.dataImport.status.ImportApplicationStatus
import io.tolgee.service.dataImport.status.ImportApplicationStatusItem
import io.tolgee.service.key.NamespaceService
import io.tolgee.util.Logging
import io.tolgee.util.StreamingResponseBodyProvider
import io.tolgee.util.filterFiles
import io.tolgee.util.logger
import org.springdoc.core.annotations.ParameterObject
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
Expand Down Expand Up @@ -72,6 +66,7 @@ class V2ImportController(
private val projectHolder: ProjectHolder,
private val namespaceService: NamespaceService,
private val streamingResponseBodyProvider: StreamingResponseBodyProvider,
private val streamingImportProgressUtil: StreamingImportProgressUtil,
) : Logging {
@PostMapping("", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
@Operation(description = "Prepares provided files to import.", summary = "Add files")
Expand Down Expand Up @@ -139,36 +134,8 @@ class V2ImportController(
): ResponseEntity<StreamingResponseBody> {
val projectId = projectHolder.project.id

return streamingResponseBodyProvider.streamNdJson { write ->
val writeStatus = { status: ImportApplicationStatus ->
write(ImportApplicationStatusItem(status))
}
try {
this.importService.import(projectId, authenticationFacade.authenticatedUser.id, forceMode, writeStatus)
} catch (e: Exception) {
if (e !is BadRequestException) {
Sentry.captureException(e)
logger.error("Unexpected error while importing", e)
}
when (e) {
is ErrorException ->
write(
ImportApplicationStatusItem(
ImportApplicationStatus.ERROR,
errorStatusCode = e.httpStatus.value(),
errorResponseBody = ErrorResponseBody(e.code, e.params),
),
)

else ->
write(
ImportApplicationStatusItem(
ImportApplicationStatus.ERROR,
errorStatusCode = 500,
),
)
}
}
return streamingImportProgressUtil.stream { writeStatus ->
this.importService.import(projectId, authenticationFacade.authenticatedUser.id, forceMode, writeStatus)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ class OrganizationController(
@ParameterObject
@SortDefault(sort = ["id"])
pageable: Pageable,
@ParameterObject
params: OrganizationRequestParamsDto,
): PagedModel<OrganizationModel>? {
val organizations = organizationService.findPermittedPaged(pageable, params)
Expand Down
Loading
Loading