add get and update for Type
- add UpdateType end-to-end - add updateType to TypeController - add updateType to TypeService - add sql 004-create_type_description_data.sql - update TypesEndpoint to use Types.Response - update type end-to-end test - update TypeEndpoint with CreateType - add reset test table - add test data - add getType to TypeController - add getType to TypeService - add findTypeWithDescription to TypeRepository - update type end-to-end test - update TypeController for changes for adding type - update type mapping for latest changes in Mapping.kt - update addType to use TypeDescriptionRepository and return TypeWithDescription in TypeService - add TypeWithDescription - add TypeDescriptionRepository - add TypeDescriptionEntity - add missing device mapping test - add type_descriptions sql script for database changes - update TypesEndpoint - update TypesController to use Types.Response - add TypeEntity.toTypesResponse to Mapping.kt
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
package ltd.hlaeja.controller
|
||||
|
||||
import java.util.UUID
|
||||
import ltd.hlaeja.library.deviceRegistry.Type
|
||||
import ltd.hlaeja.service.TypeService
|
||||
import ltd.hlaeja.util.toTypeEntity
|
||||
import ltd.hlaeja.util.toTypeResponse
|
||||
import org.springframework.http.HttpStatus.CREATED
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.PutMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@@ -14,7 +19,19 @@ class TypeController(
|
||||
) {
|
||||
|
||||
@PostMapping("/type")
|
||||
@ResponseStatus(CREATED)
|
||||
suspend fun addType(
|
||||
@RequestBody register: Type.Request,
|
||||
): Type.Response = service.addType(register.toTypeEntity()).toTypeResponse()
|
||||
@RequestBody request: Type.Request,
|
||||
): Type.Response = service.addType(request.name, request.description).toTypeResponse()
|
||||
|
||||
@GetMapping("/type-{type}")
|
||||
suspend fun getType(
|
||||
@PathVariable type: UUID,
|
||||
): Type.Response = service.getType(type).toTypeResponse()
|
||||
|
||||
@PutMapping("/type-{type}")
|
||||
suspend fun updateType(
|
||||
@PathVariable type: UUID,
|
||||
@RequestBody request: Type.Request,
|
||||
): Type.Response = service.updateType(type, request.name, request.description).toTypeResponse()
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package ltd.hlaeja.controller
|
||||
import jakarta.validation.constraints.Min
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import ltd.hlaeja.library.deviceRegistry.Type
|
||||
import ltd.hlaeja.library.deviceRegistry.Types
|
||||
import ltd.hlaeja.service.TypeService
|
||||
import ltd.hlaeja.util.toTypeResponse
|
||||
import ltd.hlaeja.util.toTypesResponse
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
@@ -31,6 +31,6 @@ class TypesController(
|
||||
@PathVariable(required = false) @Min(1) page: Int = DEFAULT_PAGE,
|
||||
@PathVariable(required = false) @Min(1) show: Int = DEFAULT_SIZE,
|
||||
@PathVariable(required = false) filter: String? = null,
|
||||
): Flow<Type.Response> = service.getTypes((page - 1) * show, show, filter)
|
||||
.map { it.toTypeResponse() }
|
||||
): Flow<Types.Response> = service.getTypes((page - 1) * show, show, filter)
|
||||
.map { it.toTypesResponse() }
|
||||
}
|
||||
|
||||
11
src/main/kotlin/ltd/hlaeja/dto/TypeWithDescription.kt
Normal file
11
src/main/kotlin/ltd/hlaeja/dto/TypeWithDescription.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package ltd.hlaeja.dto
|
||||
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.UUID
|
||||
|
||||
data class TypeWithDescription(
|
||||
val id: UUID,
|
||||
val timestamp: ZonedDateTime,
|
||||
val name: String,
|
||||
val description: String?,
|
||||
)
|
||||
12
src/main/kotlin/ltd/hlaeja/entity/TypeDescriptionEntity.kt
Normal file
12
src/main/kotlin/ltd/hlaeja/entity/TypeDescriptionEntity.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package ltd.hlaeja.entity
|
||||
|
||||
import java.util.UUID
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.relational.core.mapping.Table
|
||||
|
||||
@Table("type_descriptions")
|
||||
data class TypeDescriptionEntity(
|
||||
@Id
|
||||
val typeId: UUID,
|
||||
val description: String,
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
package ltd.hlaeja.repository
|
||||
|
||||
import java.util.UUID
|
||||
import ltd.hlaeja.entity.TypeDescriptionEntity
|
||||
import org.springframework.data.r2dbc.repository.Query
|
||||
import org.springframework.data.repository.kotlin.CoroutineCrudRepository
|
||||
import org.springframework.data.repository.query.Param
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface TypeDescriptionRepository : CoroutineCrudRepository<TypeDescriptionEntity, UUID> {
|
||||
@Query(
|
||||
"""
|
||||
INSERT INTO type_descriptions (type_id, description) VALUES (:type_id, :description)
|
||||
ON CONFLICT (type_id)
|
||||
DO UPDATE SET description = :description
|
||||
RETURNING *
|
||||
""",
|
||||
)
|
||||
suspend fun upsert(
|
||||
@Param("type_id") typeId: UUID,
|
||||
@Param("description") description: String,
|
||||
): TypeDescriptionEntity
|
||||
}
|
||||
@@ -2,8 +2,8 @@ package ltd.hlaeja.repository
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import ltd.hlaeja.dto.TypeWithDescription
|
||||
import ltd.hlaeja.entity.TypeEntity
|
||||
|
||||
import org.springframework.data.r2dbc.repository.Query
|
||||
import org.springframework.data.repository.kotlin.CoroutineCrudRepository
|
||||
import org.springframework.data.repository.query.Param
|
||||
@@ -24,4 +24,16 @@ interface TypeRepository : CoroutineCrudRepository<TypeEntity, UUID> {
|
||||
@Param("offset") offset: Int,
|
||||
@Param("limit") limit: Int,
|
||||
): Flow<TypeEntity>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT t.id, t.timestamp, t.name, td.description
|
||||
FROM types t
|
||||
LEFT JOIN type_descriptions td ON t.id = td.type_id
|
||||
WHERE t.id = :id
|
||||
""",
|
||||
)
|
||||
suspend fun findTypeWithDescription(
|
||||
@Param("id") id: UUID,
|
||||
): TypeWithDescription?
|
||||
}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
package ltd.hlaeja.service
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import ltd.hlaeja.dto.TypeWithDescription
|
||||
import ltd.hlaeja.entity.TypeDescriptionEntity
|
||||
import ltd.hlaeja.entity.TypeEntity
|
||||
import ltd.hlaeja.repository.TypeDescriptionRepository
|
||||
import ltd.hlaeja.repository.TypeRepository
|
||||
import org.springframework.dao.DuplicateKeyException
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.dao.DataIntegrityViolationException
|
||||
import org.springframework.http.HttpStatus.ACCEPTED
|
||||
import org.springframework.http.HttpStatus.CONFLICT
|
||||
import org.springframework.http.HttpStatus.EXPECTATION_FAILED
|
||||
import org.springframework.http.HttpStatus.NOT_FOUND
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
@@ -14,6 +23,7 @@ private val log = KotlinLogging.logger {}
|
||||
@Service
|
||||
class TypeService(
|
||||
private val typeRepository: TypeRepository,
|
||||
private val typeDescriptionRepository: TypeDescriptionRepository,
|
||||
) {
|
||||
|
||||
fun getTypes(
|
||||
@@ -25,13 +35,87 @@ class TypeService(
|
||||
else -> typeRepository.findAll(page, show)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
suspend fun addType(
|
||||
entity: TypeEntity,
|
||||
): TypeEntity = try {
|
||||
typeRepository.save(entity)
|
||||
.also { log.debug { "Added new type: $it.id" } }
|
||||
} catch (e: DuplicateKeyException) {
|
||||
log.warn { e.localizedMessage }
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT)
|
||||
name: String,
|
||||
description: String,
|
||||
): TypeWithDescription = try {
|
||||
val savedType = typeRepository.save(
|
||||
TypeEntity(timestamp = ZonedDateTime.now(), name = name),
|
||||
).also { log.debug { "Added new type: ${it.id}" } }
|
||||
val savedDescription = typeDescriptionRepository.upsert(
|
||||
savedType.id ?: throw ResponseStatusException(EXPECTATION_FAILED),
|
||||
description,
|
||||
).also { log.debug { "Added description for type: ${it.typeId}" } }
|
||||
TypeWithDescription(
|
||||
id = savedType.id,
|
||||
timestamp = savedType.timestamp,
|
||||
name = savedType.name,
|
||||
description = savedDescription.description,
|
||||
)
|
||||
} catch (e: DataIntegrityViolationException) {
|
||||
log.warn { "Failed to add type with name '$name': ${e.localizedMessage}" }
|
||||
throw ResponseStatusException(CONFLICT, "Type with name '$name' already exists")
|
||||
}
|
||||
|
||||
suspend fun getType(
|
||||
id: UUID,
|
||||
): TypeWithDescription = typeRepository.findTypeWithDescription(id)
|
||||
?.also { log.debug { "Retrieved type with description: ${it.id}" } }
|
||||
?: throw ResponseStatusException(NOT_FOUND, "Type with id '$id' not found")
|
||||
|
||||
@Transactional
|
||||
suspend fun updateType(
|
||||
id: UUID,
|
||||
name: String,
|
||||
description: String,
|
||||
): TypeWithDescription {
|
||||
var hasChanges = false
|
||||
val updatedType = updateType(id, name) { hasChanges = true }
|
||||
val updatedTypeDescription = updateTypeDescription(id, description) { hasChanges = true }
|
||||
if (!hasChanges) {
|
||||
throw ResponseStatusException(ACCEPTED, "No changes for type with id '$id'")
|
||||
}
|
||||
return TypeWithDescription(
|
||||
id = updatedType.id!!,
|
||||
timestamp = updatedType.timestamp,
|
||||
name = updatedType.name,
|
||||
description = updatedTypeDescription.description,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun updateTypeDescription(
|
||||
id: UUID,
|
||||
description: String,
|
||||
onChange: () -> Unit,
|
||||
): TypeDescriptionEntity {
|
||||
val existingDescription = typeDescriptionRepository.findById(id)
|
||||
?: throw ResponseStatusException(NOT_FOUND, "Type description with id '$id' not found")
|
||||
return if (existingDescription.description == description) {
|
||||
existingDescription
|
||||
} else {
|
||||
onChange()
|
||||
typeDescriptionRepository.save(existingDescription.copy(description = description))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateType(
|
||||
id: UUID,
|
||||
name: String,
|
||||
onChange: () -> Unit,
|
||||
): TypeEntity {
|
||||
val existingType = typeRepository.findById(id)
|
||||
?: throw ResponseStatusException(NOT_FOUND, "Type with id '$id' not found")
|
||||
return if (existingType.name == name) {
|
||||
existingType
|
||||
} else {
|
||||
onChange()
|
||||
try {
|
||||
typeRepository.save(existingType.copy(name = name))
|
||||
} catch (e: DataIntegrityViolationException) {
|
||||
log.warn { "Failed to update type with name '$name': ${e.localizedMessage}" }
|
||||
throw ResponseStatusException(CONFLICT, "Type with name '$name' already exists")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,43 @@
|
||||
package ltd.hlaeja.util
|
||||
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.UUID
|
||||
import ltd.hlaeja.entity.DeviceEntity
|
||||
import ltd.hlaeja.entity.NodeEntity
|
||||
import ltd.hlaeja.entity.TypeEntity
|
||||
import ltd.hlaeja.dto.TypeWithDescription
|
||||
import ltd.hlaeja.entity.TypeDescriptionEntity
|
||||
import ltd.hlaeja.jwt.service.PrivateJwtService
|
||||
import ltd.hlaeja.library.deviceRegistry.Device
|
||||
import ltd.hlaeja.library.deviceRegistry.Identity
|
||||
import ltd.hlaeja.library.deviceRegistry.Node
|
||||
import ltd.hlaeja.library.deviceRegistry.Type
|
||||
import ltd.hlaeja.library.deviceRegistry.Types
|
||||
import org.springframework.http.HttpStatus.EXPECTATION_FAILED
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
|
||||
fun Type.Request.toTypeEntity(): TypeEntity = TypeEntity(null, ZonedDateTime.now(), name)
|
||||
fun Type.Request.toTypeEntity(id: UUID): TypeEntity = TypeEntity(
|
||||
id = id,
|
||||
timestamp = ZonedDateTime.now(),
|
||||
name = name,
|
||||
)
|
||||
|
||||
fun TypeEntity.toTypeResponse(): Type.Response = Type.Response(
|
||||
id ?: throw ResponseStatusException(EXPECTATION_FAILED),
|
||||
name,
|
||||
fun Type.Request.toTypeDescriptionEntity(id: UUID): TypeDescriptionEntity = TypeDescriptionEntity(
|
||||
typeId = id,
|
||||
description = description,
|
||||
)
|
||||
|
||||
fun TypeWithDescription.toTypeResponse(): Type.Response = Type.Response(
|
||||
id = id,
|
||||
timestamp = timestamp,
|
||||
name = name,
|
||||
description = description ?: "",
|
||||
)
|
||||
|
||||
fun TypeEntity.toTypesResponse(): Types.Response = Types.Response(
|
||||
id = id!!,
|
||||
name = name,
|
||||
timestamp = timestamp,
|
||||
)
|
||||
|
||||
fun Node.Request.toEntity(): NodeEntity = NodeEntity(
|
||||
|
||||
Reference in New Issue
Block a user