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