feat(backend): implement step 9 — REST controllers, DTOs, EntityService, SecurityConfig (permit-all)
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
package com.condado.newsletter.config
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
|
||||
/**
|
||||
* Security configuration — Step 9: permits all requests.
|
||||
* Will be updated in Step 10 with JWT authentication.
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http
|
||||
.csrf { it.disable() }
|
||||
.authorizeHttpRequests { it.anyRequest().permitAll() }
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.condado.newsletter.controller
|
||||
|
||||
import com.condado.newsletter.dto.DispatchLogResponseDto
|
||||
import com.condado.newsletter.repository.DispatchLogRepository
|
||||
import com.condado.newsletter.repository.VirtualEntityRepository
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* REST controller for read-only access to [com.condado.newsletter.model.DispatchLog] resources.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/dispatch-logs")
|
||||
class DispatchLogController(
|
||||
private val dispatchLogRepository: DispatchLogRepository,
|
||||
private val virtualEntityRepository: VirtualEntityRepository
|
||||
) {
|
||||
|
||||
/** Lists all dispatch logs. */
|
||||
@GetMapping
|
||||
fun getAll(): ResponseEntity<List<DispatchLogResponseDto>> =
|
||||
ResponseEntity.ok(dispatchLogRepository.findAll().map { DispatchLogResponseDto.from(it) })
|
||||
|
||||
/** Lists dispatch logs for a specific entity. */
|
||||
@GetMapping("/entity/{entityId}")
|
||||
fun getByEntityId(@PathVariable entityId: UUID): ResponseEntity<List<DispatchLogResponseDto>> {
|
||||
val entity = virtualEntityRepository.findById(entityId).orElse(null)
|
||||
?: return ResponseEntity.notFound().build()
|
||||
return ResponseEntity.ok(
|
||||
dispatchLogRepository.findAllByVirtualEntity(entity).map { DispatchLogResponseDto.from(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.condado.newsletter.controller
|
||||
|
||||
import com.condado.newsletter.dto.VirtualEntityCreateDto
|
||||
import com.condado.newsletter.dto.VirtualEntityResponseDto
|
||||
import com.condado.newsletter.dto.VirtualEntityUpdateDto
|
||||
import com.condado.newsletter.scheduler.EntityScheduler
|
||||
import com.condado.newsletter.service.EntityService
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
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.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* REST controller for managing [com.condado.newsletter.model.VirtualEntity] resources.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/virtual-entities")
|
||||
class VirtualEntityController(
|
||||
private val entityService: EntityService,
|
||||
private val entityScheduler: EntityScheduler
|
||||
) {
|
||||
|
||||
/** Creates a new virtual entity. */
|
||||
@PostMapping
|
||||
fun create(@Valid @RequestBody dto: VirtualEntityCreateDto): ResponseEntity<VirtualEntityResponseDto> =
|
||||
ResponseEntity.status(HttpStatus.CREATED).body(entityService.create(dto))
|
||||
|
||||
/** Lists all virtual entities. */
|
||||
@GetMapping
|
||||
fun getAll(): ResponseEntity<List<VirtualEntityResponseDto>> =
|
||||
ResponseEntity.ok(entityService.findAll())
|
||||
|
||||
/** Returns one entity by ID. */
|
||||
@GetMapping("/{id}")
|
||||
fun getById(@PathVariable id: UUID): ResponseEntity<VirtualEntityResponseDto> {
|
||||
val entity = entityService.findById(id) ?: return ResponseEntity.notFound().build()
|
||||
return ResponseEntity.ok(entity)
|
||||
}
|
||||
|
||||
/** Updates an entity. */
|
||||
@PutMapping("/{id}")
|
||||
fun update(
|
||||
@PathVariable id: UUID,
|
||||
@RequestBody dto: VirtualEntityUpdateDto
|
||||
): ResponseEntity<VirtualEntityResponseDto> {
|
||||
val entity = entityService.update(id, dto) ?: return ResponseEntity.notFound().build()
|
||||
return ResponseEntity.ok(entity)
|
||||
}
|
||||
|
||||
/** Soft-deletes an entity (sets active = false). */
|
||||
@DeleteMapping("/{id}")
|
||||
fun deactivate(@PathVariable id: UUID): ResponseEntity<VirtualEntityResponseDto> {
|
||||
val entity = entityService.deactivate(id) ?: return ResponseEntity.notFound().build()
|
||||
return ResponseEntity.ok(entity)
|
||||
}
|
||||
|
||||
/** Manually triggers the email pipeline for a specific entity. */
|
||||
@PostMapping("/{id}/trigger")
|
||||
fun trigger(@PathVariable id: UUID): ResponseEntity<Void> {
|
||||
val entity = entityService.findRawById(id) ?: return ResponseEntity.notFound().build()
|
||||
entityScheduler.runPipeline(entity)
|
||||
return ResponseEntity.ok().build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.condado.newsletter.dto
|
||||
|
||||
import com.condado.newsletter.model.DispatchLog
|
||||
import com.condado.newsletter.model.DispatchStatus
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
/** DTO returned by the API for a [com.condado.newsletter.model.DispatchLog]. */
|
||||
data class DispatchLogResponseDto(
|
||||
val id: UUID?,
|
||||
val entityId: UUID?,
|
||||
val entityName: String,
|
||||
val promptSent: String?,
|
||||
val aiResponse: String?,
|
||||
val emailSubject: String?,
|
||||
val emailBody: String?,
|
||||
val status: DispatchStatus,
|
||||
val errorMessage: String?,
|
||||
val dispatchedAt: LocalDateTime
|
||||
) {
|
||||
companion object {
|
||||
fun from(log: DispatchLog) = DispatchLogResponseDto(
|
||||
id = log.id,
|
||||
entityId = log.virtualEntity.id,
|
||||
entityName = log.virtualEntity.name,
|
||||
promptSent = log.promptSent,
|
||||
aiResponse = log.aiResponse,
|
||||
emailSubject = log.emailSubject,
|
||||
emailBody = log.emailBody,
|
||||
status = log.status,
|
||||
errorMessage = log.errorMessage,
|
||||
dispatchedAt = log.dispatchedAt
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.condado.newsletter.dto
|
||||
|
||||
import jakarta.validation.constraints.Email
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
|
||||
/** DTO for creating a new [com.condado.newsletter.model.VirtualEntity]. */
|
||||
data class VirtualEntityCreateDto(
|
||||
@field:NotBlank val name: String,
|
||||
@field:NotBlank @field:Email val email: String,
|
||||
@field:NotBlank val jobTitle: String,
|
||||
val personality: String? = null,
|
||||
val scheduleCron: String? = null,
|
||||
val contextWindowDays: Int = 3
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.condado.newsletter.dto
|
||||
|
||||
import com.condado.newsletter.model.VirtualEntity
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
/** DTO returned by the API for a [com.condado.newsletter.model.VirtualEntity]. */
|
||||
data class VirtualEntityResponseDto(
|
||||
val id: UUID?,
|
||||
val name: String,
|
||||
val email: String,
|
||||
val jobTitle: String,
|
||||
val personality: String?,
|
||||
val scheduleCron: String?,
|
||||
val contextWindowDays: Int,
|
||||
val active: Boolean,
|
||||
val createdAt: LocalDateTime?
|
||||
) {
|
||||
companion object {
|
||||
fun from(entity: VirtualEntity) = VirtualEntityResponseDto(
|
||||
id = entity.id,
|
||||
name = entity.name,
|
||||
email = entity.email,
|
||||
jobTitle = entity.jobTitle,
|
||||
personality = entity.personality,
|
||||
scheduleCron = entity.scheduleCron,
|
||||
contextWindowDays = entity.contextWindowDays,
|
||||
active = entity.active,
|
||||
createdAt = entity.createdAt
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.condado.newsletter.dto
|
||||
|
||||
/** DTO for updating an existing [com.condado.newsletter.model.VirtualEntity]. All fields are optional. */
|
||||
data class VirtualEntityUpdateDto(
|
||||
val name: String? = null,
|
||||
val email: String? = null,
|
||||
val jobTitle: String? = null,
|
||||
val personality: String? = null,
|
||||
val scheduleCron: String? = null,
|
||||
val contextWindowDays: Int? = null,
|
||||
val active: Boolean? = null
|
||||
)
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.condado.newsletter.service
|
||||
|
||||
import com.condado.newsletter.dto.VirtualEntityCreateDto
|
||||
import com.condado.newsletter.dto.VirtualEntityResponseDto
|
||||
import com.condado.newsletter.dto.VirtualEntityUpdateDto
|
||||
import com.condado.newsletter.model.VirtualEntity
|
||||
import com.condado.newsletter.repository.DispatchLogRepository
|
||||
import com.condado.newsletter.repository.VirtualEntityRepository
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Service for managing [VirtualEntity] lifecycle: create, read, update, and soft-delete.
|
||||
*/
|
||||
@Service
|
||||
class EntityService(
|
||||
private val virtualEntityRepository: VirtualEntityRepository,
|
||||
private val dispatchLogRepository: DispatchLogRepository
|
||||
) {
|
||||
|
||||
/** Returns all virtual entities. */
|
||||
fun findAll(): List<VirtualEntityResponseDto> =
|
||||
virtualEntityRepository.findAll().map { VirtualEntityResponseDto.from(it) }
|
||||
|
||||
/** Returns one entity by ID, or null if not found. */
|
||||
fun findById(id: UUID): VirtualEntityResponseDto? =
|
||||
virtualEntityRepository.findById(id).map { VirtualEntityResponseDto.from(it) }.orElse(null)
|
||||
|
||||
/** Creates a new virtual entity. */
|
||||
@Transactional
|
||||
fun create(dto: VirtualEntityCreateDto): VirtualEntityResponseDto {
|
||||
val entity = VirtualEntity(
|
||||
name = dto.name,
|
||||
email = dto.email,
|
||||
jobTitle = dto.jobTitle,
|
||||
personality = dto.personality,
|
||||
scheduleCron = dto.scheduleCron,
|
||||
contextWindowDays = dto.contextWindowDays
|
||||
)
|
||||
return VirtualEntityResponseDto.from(virtualEntityRepository.save(entity))
|
||||
}
|
||||
|
||||
/** Applies partial updates to an existing entity. Returns null if not found. */
|
||||
@Transactional
|
||||
fun update(id: UUID, dto: VirtualEntityUpdateDto): VirtualEntityResponseDto? {
|
||||
val existing = virtualEntityRepository.findById(id).orElse(null) ?: return null
|
||||
val updated = VirtualEntity(
|
||||
name = dto.name ?: existing.name,
|
||||
email = dto.email ?: existing.email,
|
||||
jobTitle = dto.jobTitle ?: existing.jobTitle,
|
||||
personality = dto.personality ?: existing.personality,
|
||||
scheduleCron = dto.scheduleCron ?: existing.scheduleCron,
|
||||
contextWindowDays = dto.contextWindowDays ?: existing.contextWindowDays,
|
||||
active = dto.active ?: existing.active
|
||||
).apply { this.id = existing.id }
|
||||
return VirtualEntityResponseDto.from(virtualEntityRepository.save(updated))
|
||||
}
|
||||
|
||||
/** Soft-deletes an entity by setting active = false. Returns null if not found. */
|
||||
@Transactional
|
||||
fun deactivate(id: UUID): VirtualEntityResponseDto? {
|
||||
val existing = virtualEntityRepository.findById(id).orElse(null) ?: return null
|
||||
val deactivated = VirtualEntity(
|
||||
name = existing.name,
|
||||
email = existing.email,
|
||||
jobTitle = existing.jobTitle,
|
||||
personality = existing.personality,
|
||||
scheduleCron = existing.scheduleCron,
|
||||
contextWindowDays = existing.contextWindowDays,
|
||||
active = false
|
||||
).apply { this.id = existing.id }
|
||||
return VirtualEntityResponseDto.from(virtualEntityRepository.save(deactivated))
|
||||
}
|
||||
|
||||
/** Finds the raw entity for use in the scheduler pipeline. Returns null if not found. */
|
||||
fun findRawById(id: UUID): VirtualEntity? =
|
||||
virtualEntityRepository.findById(id).orElse(null)
|
||||
}
|
||||
Reference in New Issue
Block a user