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
@@ -1,13 +1,16 @@
package com.lightswitch.application.service

import com.lightswitch.infrastructure.database.entity.Condition
import com.lightswitch.infrastructure.database.entity.FeatureFlag
import com.lightswitch.infrastructure.database.entity.User
import com.lightswitch.infrastructure.database.model.Type
import com.lightswitch.infrastructure.database.repository.ConditionRepository
import com.lightswitch.infrastructure.database.repository.FeatureFlagRepository
import com.lightswitch.presentation.exception.BusinessException
import com.lightswitch.presentation.model.flag.CreateFeatureFlagRequest
import com.lightswitch.presentation.model.flag.UpdateFeatureFlagRequest
import com.lightswitch.presentation.model.flag.defaultValueAsPair
import com.lightswitch.presentation.model.flag.variantPairs
import jakarta.persistence.EntityNotFoundException
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

Expand All @@ -21,15 +24,15 @@ class FeatureFlagService(
}

fun getFlagOrThrow(key: String): FeatureFlag {
return featureFlagRepository.findByName(key) ?: throw BusinessException("Feature flag $key does not exist")
return featureFlagRepository.findByName(key) ?: throw EntityNotFoundException("Feature flag $key does not exist")
}

@Transactional
fun create(
user: User,
request: CreateFeatureFlagRequest,
): FeatureFlag {
featureFlagRepository.findByName(request.key)?.let {
if (featureFlagRepository.existsByName(request.key)) {
throw BusinessException("FeatureFlag with key ${request.key} already exists")
}

Expand All @@ -46,17 +49,37 @@ class FeatureFlagService(
)

request.defaultValueAsPair()
.let { (key, value) -> Condition(flag = flag, key = key, value = value) }
.let { conditionRepository.save(it) }
.also {
flag.defaultCondition = it
flag.conditions.add(it)
}
.let { (key, value) -> flag.addDefaultCondition(key = key, value = value) }
request.variantPairs()
?.map { variant -> flag.addCondition(key = variant.first, value = variant.second) }

return featureFlagRepository.save(flag)
}

@Transactional
fun update(
user: User,
key: String,
request: UpdateFeatureFlagRequest,
): FeatureFlag {
if (request.key != key && featureFlagRepository.existsByName(request.key)) {
throw BusinessException("FeatureFlag with key ${request.key} already exists")
}

val flag = getFlagOrThrow(key)
flag.name = request.key
flag.type = Type.from(request.type)
flag.description = request.description
flag.updatedBy = user

flag.defaultCondition = null
flag.conditions.clear()
conditionRepository.deleteByFlag(flag)

request.defaultValueAsPair()
.let { (key, value) -> flag.addDefaultCondition(key = key, value = value) }
request.variantPairs()
?.map { variant -> Condition(flag = flag, key = variant.first, value = variant.second) }
?.let { conditionRepository.saveAll(it) }
?.also { flag.conditions.addAll(it) }
?.map { variant -> flag.addCondition(key = variant.first, value = variant.second) }

return featureFlagRepository.save(flag)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ class FeatureFlag(
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
@Column(nullable = false)
val name: String,
var name: String,
@Column(nullable = false)
val description: String,
var description: String,
@Column(nullable = false)
@Enumerated(value = EnumType.STRING)
val type: Type,
var type: Type,
@Column(nullable = false)
var enabled: Boolean,
@OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL], optional = true)
Expand All @@ -44,4 +44,21 @@ class FeatureFlag(
@LastModifiedBy
@ManyToOne(fetch = FetchType.LAZY)
var updatedBy: User,
) : BaseEntity()
) : BaseEntity() {
fun addDefaultCondition(
key: String,
value: Any,
) {
val condition = Condition(flag = this, key = key, value = value)
defaultCondition = condition
conditions.add(condition)
}

fun addCondition(
key: String,
value: Any,
) {
val condition = Condition(flag = this, key = key, value = value)
conditions.add(condition)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package com.lightswitch.infrastructure.database.repository

import com.lightswitch.infrastructure.database.entity.Condition
import com.lightswitch.infrastructure.database.entity.FeatureFlag
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query

interface ConditionRepository : JpaRepository<Condition, Long>
interface ConditionRepository : JpaRepository<Condition, Long> {
@Modifying(clearAutomatically = true)
@Query("DELETE FROM Condition c where c.flag = :flag")
fun deleteByFlag(flag: FeatureFlag)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import org.springframework.data.jpa.repository.EntityGraph
import org.springframework.data.jpa.repository.JpaRepository

interface FeatureFlagRepository : JpaRepository<FeatureFlag, Int> {
fun existsByName(name: String): Boolean

@EntityGraph(attributePaths = ["createdBy", "updatedBy", "defaultCondition", "conditions"])
fun findByName(name: String): FeatureFlag?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.security.SecurityScheme
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import io.swagger.v3.oas.models.security.SecurityRequirement
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

Expand All @@ -24,4 +25,7 @@ class SwaggerConfig {
.description("API for feature flag management.")
.version("1.0.0"),
)
.addSecurityItem(
SecurityRequirement().addList("bearerAuth"),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,16 @@ class FeatureFlagController(
@PathVariable key: String,
@RequestBody request: UpdateFeatureFlagRequest,
): PayloadResponse<FeatureFlagResponse> {
return PayloadResponse<FeatureFlagResponse>(
status = "status",
message = "message",
data = null,
// TODO: Improve the way finding the authenticated user.
val authentication = SecurityContextHolder.getContext().authentication
val userId = authentication.name
val user = userRepository.findByIdOrNull(userId.toLong()) ?: throw BusinessException("User not found")

val flag = featureFlagService.update(user, key, request)

return PayloadResponse.success(
message = "Updated flag successfully",
data = FeatureFlagResponse.from(flag),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,8 @@ data class CreateFeatureFlagRequest(
@field:Pattern(regexp = "(?i)^(number|boolean|string)$", message = "Type must be one of: number, boolean, string")
val type: String,
@field:NotEmpty(message = "Default value is required.")
val defaultValue: Map<String, Any>,
override val defaultValue: Map<String, Any>,
@field:NotBlank(message = "Description is required.")
val description: String,
val variants: List<Map<String, Any>>? = null,
) {
fun defaultValueAsPair(): Pair<String, Any> = defaultValue.entries.first().toPair()

fun variantPairs(): List<Pair<String, Any>>? = variants?.map { it.entries.first().toPair() }
}
override val variants: List<Map<String, Any>>? = null,
) : FeatureFlagRequest
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.lightswitch.presentation.model.flag

interface FeatureFlagRequest {
val defaultValue: Map<String, Any>
val variants: List<Map<String, Any>>?
}

fun FeatureFlagRequest.defaultValueAsPair(): Pair<String, Any> = defaultValue.entries.first().toPair()

fun FeatureFlagRequest.variantPairs(): List<Pair<String, Any>>? = variants?.map { it.entries.first().toPair() }
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@ package com.lightswitch.presentation.model.flag

import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotEmpty
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Pattern

data class UpdateFeatureFlagRequest(
@field:NotBlank(message = "Key is required.")
val key: String,
@field:NotNull(message = "Type is required.")
@field:NotBlank(message = "Type is required.")
@field:Pattern(regexp = "(?i)^(number|boolean|string)$", message = "Type must be one of: number, boolean, string")
val type: String,
@field:NotEmpty(message = "Default value is required.")
val defaultValue: Map<String, Any>,
override val defaultValue: Map<String, Any>,
@field:NotBlank(message = "Description is required.")
val description: String,
val variants: List<Map<String, Any>>? = null,
@field:NotBlank(message = "UpdatedBy is required.")
val updatedBy: String,
)
override val variants: List<Map<String, Any>>? = null,
) : FeatureFlagRequest
Loading
Loading