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

Copy modification #9

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ksidelta.libruch.modules.copy

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.client.WebClient

@Configuration
class BookApiConfig {

@Bean("book-client")
fun createBookApiClient(
webBuilder: WebClient.Builder,
@Value("\${api.search-book.base-url}") baseUrl: String,
): WebClient = webBuilder
.baseUrl(baseUrl)
.defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.ksidelta.libruch.modules.copy

import com.fasterxml.jackson.annotation.JsonProperty
import kotlinx.coroutines.reactive.awaitSingle
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient
import java.util.*

@Component
class BookApiService(
@Qualifier("book-client") private val webClient: WebClient,
@Value("\${api.search-book.key}") private val apiKey: String,
) {

suspend fun search(isbn: String): BookInfo? = webClient.get()
.uri {
it.path("/volumes")
.queryParam("q", "isbn:$isbn")
.queryParam("key", apiKey)
.build()
}
.retrieve()
.bodyToMono(BookVolumes::class.java)
.awaitSingle()
.items
.firstOrNull()
?.bookInfo

private data class BookVolumes(
@JsonProperty("items") val items: List<VolumeItem>
)

private data class VolumeItem(
@JsonProperty("volumeInfo") val bookInfo: BookInfo,
)

data class BookInfo(
@JsonProperty("title") val title: String,
@JsonProperty("subtitle") val subtitle: String = "",
@JsonProperty("authors") val authors: List<String> = emptyList(),
@JsonProperty("publishedDate") val publishedDate: String = "",
@JsonProperty("language") val language: String = "",
@JsonProperty("industryIdentifiers") val industryIdentifiers: List<IndustryIdentifier>,
)

data class IndustryIdentifier(
@JsonProperty("type") val type: String,
@JsonProperty("identifier") val identifier: String
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.ksidelta.libruch.modules.copy

import org.springframework.stereotype.Service


@Service
class BookSearchService(
private val bookApiService: BookApiService,
) {

private val cache: MutableMap<String, BookDetails> = mutableMapOf()

suspend fun get(isbn: String): BookDetails? =
cache.getOrElse(isbn) { search(isbn) }

private suspend fun search(isbn: String): BookDetails? =
bookApiService.search(isbn = isbn)?.let {
BookDetails(
isbn = it.industryIdentifiers.first { i -> i.type == "ISBN_13" }.identifier,
title = it.title,
subtitle = it.subtitle,
authors = it.authors.toString(),
publishedDate = it.publishedDate,
language = it.language,
)
}
}

data class BookDetails(
val isbn: String,
val title: String,
val subtitle: String,
val authors: String,
val publishedDate: String,
val language: String,
)

Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ package com.ksidelta.libruch.modules.copy
import com.ksidelta.libruch.modules.kernel.Party
import org.axonframework.commandhandling.CommandHandler
import org.axonframework.eventsourcing.EventSourcingHandler
import org.axonframework.extensions.kotlin.applyEvent
import org.axonframework.modelling.command.AggregateIdentifier
import org.axonframework.modelling.command.AggregateLifecycle.apply
import org.axonframework.modelling.command.TargetAggregateIdentifier
import org.axonframework.spring.stereotype.Aggregate
import java.util.*

Expand All @@ -18,13 +17,14 @@ class CopyAggregate() {

@CommandHandler
constructor(command: RegisterNewCopy) : this() {
apply(command.run {
applyEvent(
NewCopyRegistered(
copyId = UUID.randomUUID(),
isbn = isbn,
owner = owner,
copy = command.copy,
owner = command.owner,
organisation = command.organisation,
)
})
)
}

@EventSourcingHandler
Expand All @@ -34,7 +34,12 @@ class CopyAggregate() {
}


data class RegisterNewCopy(val isbn: String, val owner: Party)
data class RegisterNewCopy(val copy: BookDetails, val owner: Party.User, val organisation: Party.Organisation)


data class NewCopyRegistered(val copyId: UUID, val isbn: String, val owner: Party)
data class NewCopyRegistered(
val copyId: UUID,
val copy: BookDetails,
val owner: Party,
val organisation: Party.Organisation
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ class CopyEventProcessor(val copyReadModelRepository: CopyReadModelRepository) {
copyReadModelRepository.save(
CopyAvailabilityModel(
id = event.copyId,
isbn = event.isbn,
isbn = event.copy.isbn,
title = event.copy.title,
owner = event.owner.partyId,
organisation = event.organisation.partyId,
)
)
}
Expand All @@ -29,14 +31,18 @@ class CopyEventProcessor(val copyReadModelRepository: CopyReadModelRepository) {
interface CopyReadModelRepository : CrudRepository<CopyAvailabilityModel, UUID> {
fun findAllByOwner(owner: Party): Iterable<CopyAvailabilityModel>
fun findAllByOwnerIn(owner: Collection<Party>): Iterable<CopyAvailabilityModel>
fun findByIsbn(isbn: String): Optional<CopyAvailabilityModel>
fun findAllByOrganisationAndTitleContains(organisation: UUID, titleFragment: String): Iterable<CopyAvailabilityModel>
}

@Entity(name = "copy_availability_model")
data class CopyAvailabilityModel(
@Id
val id: UUID,
val isbn: String,
val title: String,
val owner: UUID,
val organisation: UUID,
)


Original file line number Diff line number Diff line change
@@ -1,46 +1,62 @@
package com.ksidelta.libruch.modules.copy

import com.ksidelta.libruch.modules.kernel.Party
import com.ksidelta.libruch.modules.user.UserService
import com.ksidelta.libruch.modules.user.withUser
import kotlinx.coroutines.future.await
import org.axonframework.commandhandling.gateway.CommandGateway
import org.axonframework.messaging.responsetypes.ResponseTypes
import org.axonframework.queryhandling.QueryGateway
import org.springframework.web.bind.annotation.*
import java.security.Principal
import java.util.*

@RestController
@RequestMapping(path = ["/api/copy"])
class CopyController(
val userService: UserService,
val bookSearchService: BookSearchService,
val commandGateway: CommandGateway,
val queryGateway: QueryGateway
) {

@PostMapping
suspend fun create(@RequestBody body: CreateCopyDTO, principal: Principal) =
suspend fun create(@RequestBody body: CreateCopyDTO, user: Party.User) =
body.run {
val user = userService.findUser(principal)
val aggregateId = commandGateway.send<UUID>(RegisterNewCopy(isbn, Party.User(user.id))).await()
val copy = bookSearchService.get(isbn = body.isbn) ?: throw TODO()

val aggregateId = commandGateway.send<UUID>(
RegisterNewCopy(
copy = copy,
owner = Party.User(user.id),
organisation = Party.Organisation(organisationId)
)
).await()
CreatedCopyDTO(aggregateId)
}

@GetMapping
suspend fun listAllByOrganisations(principal: Principal): CopyAvailabilityListDTO =
userService.withUser(principal) { it.organisations }.let { organisations ->
//TODO: change QueryByOwners to QueryByOrganisations
@GetMapping("/organisation")
suspend fun listAllByOrganisations(user: Party.User): CopyAvailabilityListDTO =
user.organisations.let { organisations ->
queryGateway.query(
QueryByOwners(owners = organisations),
ResponseTypes.multipleInstancesOf(CopyAvailabilityModel::class.java)
).await()
.map { it.run { CopyAvailabilityDTO(id = id, isbn = isbn) } }
.map { it.run { CopyAvailabilityDTO(id = id, isbn = isbn, title = title) } }
.let { CopyAvailabilityListDTO(it) }
}

@PostMapping("/by-organisation")
suspend fun listAllByOrganisationMatching(@RequestBody body: GetCopyDTO, user: Party.User): CopyAvailabilityListDTO =
body.run {
queryGateway.query(
QueryByOrganisationAndTitleFragment(organisationId, titleFragment),
ResponseTypes.multipleInstancesOf(CopyAvailabilityModel::class.java)
).await()
.map { it.run { CopyAvailabilityDTO(id = id, isbn = isbn, title = title) } }
.let { CopyAvailabilityListDTO(it) }
}
}

data class CreateCopyDTO(val isbn: String);
data class CreateCopyDTO(val isbn: String, val organisationId: UUID);
data class GetCopyDTO(val organisationId: UUID, val titleFragment: String);
data class CreatedCopyDTO(val id: UUID);

data class CopyAvailabilityListDTO(
Expand All @@ -49,5 +65,6 @@ data class CopyAvailabilityListDTO(

data class CopyAvailabilityDTO(
val id: UUID,
val title: String,
var isbn: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.ksidelta.libruch.modules.copy
import com.ksidelta.libruch.modules.kernel.Party
import org.axonframework.queryhandling.QueryHandler
import org.springframework.stereotype.Service
import java.util.UUID
import kotlin.jvm.optionals.getOrNull

@Service
class CopyQueryHandler(val copyReadModelRepository: CopyReadModelRepository) {
Expand All @@ -18,8 +20,21 @@ class CopyQueryHandler(val copyReadModelRepository: CopyReadModelRepository) {
fun query(queryByOwners: QueryByOwners) =
copyReadModelRepository.findAllByOwnerIn(owner = queryByOwners.owners).toList()

@QueryHandler
fun query(queryByOwners: QueryByOrganisationAndTitleFragment) =
copyReadModelRepository.findAllByOrganisationAndTitleContains(
organisation = queryByOwners.organisationId,
titleFragment = queryByOwners.titleFragment
).toList()

@OptIn(ExperimentalStdlibApi::class)
@QueryHandler
fun query(queryByIsbn: QueryByIsbn) =
copyReadModelRepository.findByIsbn(isbn = queryByIsbn.isbn).getOrNull()
}

class QueryAllCopies() {}
class QueryByOwner(val owner: Party) {}
class QueryByOwners(val owners: Collection<Party>) {}
class QueryByIsbn(val isbn: String) {}
class QueryByOrganisationAndTitleFragment(val organisationId: UUID, val titleFragment: String) {}
3 changes: 3 additions & 0 deletions backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ server.port=8080

# spring.security.oauth2.client.registration.google.client-id
# {baseUrl}/login/oauth2/code/{registrationId}

api.search-book.base-url=https://www.googleapis.com/books/v1
api.search-book.key=
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.ksidelta.libruch.modules.copy

import com.ksidelta.libruch.BaseTest
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import java.util.*

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CopyControllerTest : BaseTest() {

@Autowired
private lateinit var testRestTemplate: TestRestTemplate

companion object {
private const val URI = "/api/copy"
}

@Test
fun `should return UUID after create copy`() {
val isbn = "9788383223445"
val organisationId = UUID.randomUUID()


val result = testRestTemplate.postForEntity(URI, CreateCopyDTO(isbn, organisationId), CreatedCopyDTO::class.java)
println(result)


assertNotNull(result.body)
}

@Test
fun `should return list of books after given title fragment`() {
val organisationId = UUID.randomUUID()
val isbn1 = "9788383223445" // Czysty kod
val isbn2 = "9788328364622" // Czysty kod w Pythonie
val isbn3 = "9781617297571" // Spring in Action, Sixth Edition

testRestTemplate.postForEntity(URI, CreateCopyDTO(isbn1, organisationId), String::class.java)
testRestTemplate.postForEntity(URI, CreateCopyDTO(isbn2, organisationId), String::class.java)
testRestTemplate.postForEntity(URI, CreateCopyDTO(isbn3, organisationId), String::class.java)

val fragment = "Czysty"
val request = GetCopyDTO(organisationId =organisationId, titleFragment = fragment)


val result = testRestTemplate.postForEntity(URI.plus("/by-organisation"), request, CopyAvailabilityListDTO::class.java)


assertNotNull(result.body)
val copies = result.body!!.copies
assertEquals(2, copies.size)
assertTrue(copies.all { it.title.contains(fragment) })
}
}