Publish message into rabbitmq on create article
Create Redis and Rabbit in docker-compose
This commit is contained in:
11
.env
11
.env
@@ -4,6 +4,7 @@ DATABASE_URL=jdbc:postgresql:dc-project
|
|||||||
|
|
||||||
APP_PORT=8080
|
APP_PORT=8080
|
||||||
OPENAPI_PORT=8181
|
OPENAPI_PORT=8181
|
||||||
|
SONARQUBE_PORT=9000
|
||||||
|
|
||||||
ELASTIC_REST=9200
|
ELASTIC_REST=9200
|
||||||
ELASTIC_NODES=9300
|
ELASTIC_NODES=9300
|
||||||
@@ -13,4 +14,12 @@ DB_HOST=db
|
|||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
DB_NAME=dc-project
|
DB_NAME=dc-project
|
||||||
DB_USER=dc-project
|
DB_USER=dc-project
|
||||||
DB_PWD=dc-project
|
DB_PWD=dc-project
|
||||||
|
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_CONNECTION=redis://localhost:6379
|
||||||
|
REDIS_COMMANDER_PORT=8081
|
||||||
|
|
||||||
|
RABBITMQ_PORT=5672
|
||||||
|
RABBITMQ_CONNECTION=amqp://localhost:5672
|
||||||
|
RABBITMQ_MANAGEMENT_PORT=15672
|
||||||
@@ -3,6 +3,7 @@ import org.owasp.dependencycheck.reporting.ReportGenerator
|
|||||||
|
|
||||||
val ktor_version: String by project
|
val ktor_version: String by project
|
||||||
val kotlin_version: String by project
|
val kotlin_version: String by project
|
||||||
|
val coroutinesVersion: String by project
|
||||||
val logback_version: String by project
|
val logback_version: String by project
|
||||||
val koinVersion: String by project
|
val koinVersion: String by project
|
||||||
val postgresjson_version: String by project
|
val postgresjson_version: String by project
|
||||||
@@ -66,6 +67,8 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version")
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:${coroutinesVersion}")
|
||||||
implementation("io.ktor:ktor-server-jetty:$ktor_version")
|
implementation("io.ktor:ktor-server-jetty:$ktor_version")
|
||||||
implementation("ch.qos.logback:logback-classic:$logback_version")
|
implementation("ch.qos.logback:logback-classic:$logback_version")
|
||||||
implementation("io.ktor:ktor-server-core:$ktor_version")
|
implementation("io.ktor:ktor-server-core:$ktor_version")
|
||||||
@@ -83,6 +86,8 @@ dependencies {
|
|||||||
implementation("com.github.jasync-sql:jasync-postgresql:1.0.7")
|
implementation("com.github.jasync-sql:jasync-postgresql:1.0.7")
|
||||||
implementation("fr.postgresjson:postgresjson:$postgresjson_version")
|
implementation("fr.postgresjson:postgresjson:$postgresjson_version")
|
||||||
implementation("com.sendgrid:sendgrid-java:4.4.1")
|
implementation("com.sendgrid:sendgrid-java:4.4.1")
|
||||||
|
implementation("io.lettuce:lettuce-core:5.2.2.RELEASE")
|
||||||
|
implementation("com.rabbitmq:amqp-client:5.8.0")
|
||||||
|
|
||||||
testImplementation("io.ktor:ktor-server-tests:$ktor_version")
|
testImplementation("io.ktor:ktor-server-tests:$ktor_version")
|
||||||
testImplementation("io.ktor:ktor-client-mock:$ktor_version")
|
testImplementation("io.ktor:ktor-client-mock:$ktor_version")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ services:
|
|||||||
image: sonarqube
|
image: sonarqube
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 9000:9000
|
- ${SONARQUBE_PORT}:9000
|
||||||
|
|
||||||
openapi:
|
openapi:
|
||||||
container_name: openapi_${NAME}
|
container_name: openapi_${NAME}
|
||||||
@@ -18,6 +18,21 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
URL: "http://localhost:8080"
|
URL: "http://localhost:8080"
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
container_name: rabbitmq_${NAME}
|
||||||
|
image: rabbitmq:management-alpine
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- ${RABBITMQ_PORT}:5672
|
||||||
|
- ${RABBITMQ_MANAGEMENT_PORT}:15672
|
||||||
|
|
||||||
|
redis:
|
||||||
|
container_name: redis_${NAME}
|
||||||
|
image: redis:6.0-rc-alpine
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- ${REDIS_PORT}:6379
|
||||||
|
|
||||||
app:
|
app:
|
||||||
container_name: app_${NAME}
|
container_name: app_${NAME}
|
||||||
build:
|
build:
|
||||||
@@ -29,9 +44,13 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
DB_HOST: db
|
DB_HOST: db
|
||||||
SEND_GRID_KEY: ${SEND_GRID_KEY}
|
SEND_GRID_KEY: ${SEND_GRID_KEY}
|
||||||
|
REDIS_CONNECTION: redis://redis:6379
|
||||||
|
RABBITMQ_CONNECTION: amqp://rabbitmq:5671
|
||||||
depends_on:
|
depends_on:
|
||||||
- elasticsearch
|
- elasticsearch
|
||||||
- db
|
- db
|
||||||
|
- redis
|
||||||
|
- rabbitmq
|
||||||
|
|
||||||
elasticsearch:
|
elasticsearch:
|
||||||
container_name: elasticsearch_${NAME}
|
container_name: elasticsearch_${NAME}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
ktor_version=1.2.2
|
ktor_version=1.2.2
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
kotlin_version=1.3.40
|
kotlin_version=1.3.40
|
||||||
|
coroutinesVersion=1.3.3
|
||||||
logback_version=1.2.1
|
logback_version=1.2.1
|
||||||
postgresjson_version=0.1
|
postgresjson_version=0.1
|
||||||
koinVersion=2.0.1
|
koinVersion=2.0.1
|
||||||
|
|||||||
@@ -7,8 +7,12 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy
|
|||||||
import com.fasterxml.jackson.databind.SerializationFeature
|
import com.fasterxml.jackson.databind.SerializationFeature
|
||||||
import com.fasterxml.jackson.datatype.joda.JodaModule
|
import com.fasterxml.jackson.datatype.joda.JodaModule
|
||||||
import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException
|
import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException
|
||||||
|
import com.rabbitmq.client.ConnectionFactory
|
||||||
import fr.dcproject.Env.PROD
|
import fr.dcproject.Env.PROD
|
||||||
import fr.dcproject.entity.*
|
import fr.dcproject.entity.*
|
||||||
|
import fr.dcproject.event.EntityEvent
|
||||||
|
import fr.dcproject.event.EventNotification
|
||||||
|
import fr.dcproject.event.publisher.Publisher
|
||||||
import fr.dcproject.routes.*
|
import fr.dcproject.routes.*
|
||||||
import fr.dcproject.security.voter.*
|
import fr.dcproject.security.voter.*
|
||||||
import fr.postgresjson.migration.Migrations
|
import fr.postgresjson.migration.Migrations
|
||||||
@@ -27,7 +31,9 @@ import io.ktor.jackson.jackson
|
|||||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||||
import io.ktor.locations.Locations
|
import io.ktor.locations.Locations
|
||||||
import io.ktor.response.respond
|
import io.ktor.response.respond
|
||||||
|
import io.ktor.response.respondText
|
||||||
import io.ktor.routing.Routing
|
import io.ktor.routing.Routing
|
||||||
|
import io.ktor.routing.get
|
||||||
import io.ktor.util.KtorExperimentalAPI
|
import io.ktor.util.KtorExperimentalAPI
|
||||||
import org.eclipse.jetty.util.log.Slf4jLog
|
import org.eclipse.jetty.util.log.Slf4jLog
|
||||||
import org.koin.ktor.ext.Koin
|
import org.koin.ktor.ext.Koin
|
||||||
@@ -148,6 +154,25 @@ fun Application.module(env: Env = PROD) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install(EventNotification) {
|
||||||
|
/* Config Rabbit */
|
||||||
|
val exchangeName = config.exchangeNotificationName
|
||||||
|
get<ConnectionFactory>().newConnection().use { connection -> connection.createChannel().use { channel ->
|
||||||
|
channel.queueDeclare("sse", true, false, false, null)
|
||||||
|
channel.queueDeclare("email", true, false, false, null)
|
||||||
|
channel.exchangeDeclare(exchangeName, "direct")
|
||||||
|
channel.queueBind("sse", exchangeName, "")
|
||||||
|
channel.queueBind("email", exchangeName, "")
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Declare publisher on event */
|
||||||
|
val publisher = Publisher(get(), get())
|
||||||
|
subscribe(EntityEvent.Type.UPDATE_ARTICLE.event) {
|
||||||
|
println("Article is updated ${it.target.id}")
|
||||||
|
publisher.publish(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
install(Authentication) {
|
install(Authentication) {
|
||||||
/**
|
/**
|
||||||
* Setup the JWT authentication to be used in [Routing].
|
* Setup the JWT authentication to be used in [Routing].
|
||||||
@@ -200,6 +225,20 @@ fun Application.module(env: Env = PROD) {
|
|||||||
opinionArticle(get())
|
opinionArticle(get())
|
||||||
opinionChoice(get())
|
opinionChoice(get())
|
||||||
definition()
|
definition()
|
||||||
|
get("/sse") {
|
||||||
|
// environment.monitor.raise(EntityEvent.Type.UPDATE_ARTICLE.event, ArticleUpdate(ArticleRef()))
|
||||||
|
// val redis = this@authenticate.getKoin().get<RedisReactiveCommands<String, String>>()
|
||||||
|
// redis.set("key", "test").awaitSingle()
|
||||||
|
// redis.lpush("list", "test2").asFlow().map {
|
||||||
|
// println(it)
|
||||||
|
// }.collect()
|
||||||
|
// redis.get("key").asFlow().collect { println(it) }
|
||||||
|
// redis.rpop("list").asFlow().collect {
|
||||||
|
// println(it)
|
||||||
|
// call.respondText { it }
|
||||||
|
// }
|
||||||
|
call.respondText("OK")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ class Config {
|
|||||||
var username: String = config.getString("db.username")
|
var username: String = config.getString("db.username")
|
||||||
var password: String = config.getString("db.password")
|
var password: String = config.getString("db.password")
|
||||||
val port: Int = config.getInt("db.port")
|
val port: Int = config.getInt("db.port")
|
||||||
|
val redis: String = config.getString("redis.connection")
|
||||||
|
val rabbitmq: String = config.getString("rabbitmq.connection")
|
||||||
|
val exchangeNotificationName = "notification"
|
||||||
val sendGridKey: String = config.getString("mail.sendGrid.key")
|
val sendGridKey: String = config.getString("mail.sendGrid.key")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
package fr.dcproject
|
package fr.dcproject
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategy
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature
|
||||||
|
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||||
|
import com.fasterxml.jackson.datatype.joda.JodaModule
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.rabbitmq.client.ConnectionFactory
|
||||||
import fr.dcproject.messages.Mailer
|
import fr.dcproject.messages.Mailer
|
||||||
import fr.dcproject.messages.SsoManager
|
import fr.dcproject.messages.SsoManager
|
||||||
import fr.postgresjson.connexion.Connection
|
import fr.postgresjson.connexion.Connection
|
||||||
import fr.postgresjson.connexion.Requester
|
import fr.postgresjson.connexion.Requester
|
||||||
import fr.postgresjson.migration.Migrations
|
import fr.postgresjson.migration.Migrations
|
||||||
import io.ktor.util.KtorExperimentalAPI
|
import io.ktor.util.KtorExperimentalAPI
|
||||||
|
import io.lettuce.core.RedisClient
|
||||||
|
import io.lettuce.core.api.reactive.RedisReactiveCommands
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import fr.dcproject.repository.Article as ArticleRepository
|
import fr.dcproject.repository.Article as ArticleRepository
|
||||||
import fr.dcproject.repository.Citizen as CitizenRepository
|
import fr.dcproject.repository.Citizen as CitizenRepository
|
||||||
@@ -39,6 +49,25 @@ val Module = module {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
single<RedisReactiveCommands<String, String>> {
|
||||||
|
RedisClient.create(config.redis).connect()?.reactive() ?: error("Unable to connect to redis")
|
||||||
|
}
|
||||||
|
|
||||||
|
single<ConnectionFactory> {
|
||||||
|
ConnectionFactory().apply { setUri(config.rabbitmq) }
|
||||||
|
}
|
||||||
|
|
||||||
|
single<ObjectMapper> {
|
||||||
|
jacksonObjectMapper().apply {
|
||||||
|
registerModule(SimpleModule())
|
||||||
|
propertyNamingStrategy = PropertyNamingStrategy.SNAKE_CASE
|
||||||
|
|
||||||
|
registerModule(JodaModule())
|
||||||
|
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||||
|
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
single {
|
single {
|
||||||
Requester.RequesterFactory(
|
Requester.RequesterFactory(
|
||||||
connection = get(),
|
connection = get(),
|
||||||
|
|||||||
54
src/main/kotlin/fr/dcproject/event/EventNotification.kt
Normal file
54
src/main/kotlin/fr/dcproject/event/EventNotification.kt
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package fr.dcproject.event
|
||||||
|
|
||||||
|
import fr.dcproject.entity.Article
|
||||||
|
import fr.postgresjson.entity.immutable.UuidEntity
|
||||||
|
import io.ktor.application.*
|
||||||
|
import io.ktor.util.AttributeKey
|
||||||
|
import io.ktor.util.KtorExperimentalAPI
|
||||||
|
import kotlinx.coroutines.DisposableHandle
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
|
abstract class Notification(
|
||||||
|
val type: String,
|
||||||
|
val createdAt: DateTime = DateTime.now()
|
||||||
|
)
|
||||||
|
abstract class EntityEvent(
|
||||||
|
val target: UuidEntity,
|
||||||
|
type: String,
|
||||||
|
val action: String
|
||||||
|
) : Notification(type) {
|
||||||
|
enum class Type(val event: EventDefinition<ArticleUpdate>) {
|
||||||
|
UPDATE_ARTICLE(EventDefinition<ArticleUpdate>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArticleUpdate(
|
||||||
|
target: Article
|
||||||
|
) : EntityEvent(target, "article", "update")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installation Class
|
||||||
|
*/
|
||||||
|
class EventNotification {
|
||||||
|
class Configuration(private val monitor: ApplicationEvents) {
|
||||||
|
private val subscribers = mutableListOf<DisposableHandle>()
|
||||||
|
fun <T: Notification> subscribe(definition: EventDefinition<T>, handler: EventHandler<T>): DisposableHandle {
|
||||||
|
return monitor.subscribe(definition, handler).also {
|
||||||
|
subscribers.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object Feature : ApplicationFeature<Application, Configuration, EventNotification> {
|
||||||
|
override val key = AttributeKey<EventNotification>("EventNotification")
|
||||||
|
|
||||||
|
@KtorExperimentalAPI
|
||||||
|
override fun install(
|
||||||
|
pipeline: Application,
|
||||||
|
configure: Configuration.() -> Unit
|
||||||
|
): EventNotification {
|
||||||
|
Configuration(pipeline.environment.monitor).apply(configure)
|
||||||
|
return EventNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/main/kotlin/fr/dcproject/event/publisher/Publisher.kt
Normal file
30
src/main/kotlin/fr/dcproject/event/publisher/Publisher.kt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package fr.dcproject.event.publisher
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.rabbitmq.client.ConnectionFactory
|
||||||
|
import fr.dcproject.config
|
||||||
|
import fr.dcproject.event.EntityEvent
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
class Publisher(
|
||||||
|
private val mapper: ObjectMapper,
|
||||||
|
private val factory: ConnectionFactory,
|
||||||
|
private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName)
|
||||||
|
) {
|
||||||
|
fun <T: EntityEvent>publish(it: T): Job {
|
||||||
|
return GlobalScope.launch {
|
||||||
|
factory.newConnection().use { connection -> connection.createChannel().use { channel ->
|
||||||
|
channel.basicPublish(config.exchangeNotificationName, "", null, it.serialize().toByteArray())
|
||||||
|
logger.debug("Publish message ${it.target.id}")
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun EntityEvent.serialize(): String {
|
||||||
|
return mapper.writeValueAsString(this) ?: error("Unable tu serialize message")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
package fr.dcproject.routes
|
package fr.dcproject.routes
|
||||||
|
|
||||||
import fr.dcproject.citizen
|
import fr.dcproject.citizen
|
||||||
|
import fr.dcproject.event.ArticleUpdate
|
||||||
|
import fr.dcproject.event.EntityEvent
|
||||||
import fr.dcproject.repository.Article.Filter
|
import fr.dcproject.repository.Article.Filter
|
||||||
import fr.dcproject.security.voter.ArticleVoter.Action.CREATE
|
import fr.dcproject.security.voter.ArticleVoter.Action.CREATE
|
||||||
import fr.dcproject.security.voter.ArticleVoter.Action.VIEW
|
import fr.dcproject.security.voter.ArticleVoter.Action.VIEW
|
||||||
import fr.dcproject.security.voter.assertCan
|
import fr.dcproject.security.voter.assertCan
|
||||||
import fr.postgresjson.repository.RepositoryI
|
import fr.postgresjson.repository.RepositoryI
|
||||||
|
import io.ktor.application.application
|
||||||
import io.ktor.application.call
|
import io.ktor.application.call
|
||||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||||
import io.ktor.locations.Location
|
import io.ktor.locations.Location
|
||||||
@@ -83,6 +86,7 @@ fun Route.article(repo: ArticleRepository) {
|
|||||||
assertCan(CREATE, article)
|
assertCan(CREATE, article)
|
||||||
|
|
||||||
repo.upsert(article)
|
repo.upsert(article)
|
||||||
|
application.environment.monitor.raise(EntityEvent.Type.UPDATE_ARTICLE.event, ArticleUpdate(article))
|
||||||
|
|
||||||
call.respond(article)
|
call.respond(article)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,16 @@ db {
|
|||||||
port = 5432
|
port = 5432
|
||||||
}
|
}
|
||||||
|
|
||||||
|
redis {
|
||||||
|
connection = "redis://localhost:6379"
|
||||||
|
connection = ${?REDIS_CONNECTION}
|
||||||
|
}
|
||||||
|
|
||||||
|
rabbitmq {
|
||||||
|
connection = "amqp://localhost:5672"
|
||||||
|
connection = ${?RABBITMQ_CONNECTION}
|
||||||
|
}
|
||||||
|
|
||||||
mail {
|
mail {
|
||||||
sendGrid {
|
sendGrid {
|
||||||
key = ${?SEND_GRID_KEY}
|
key = ${?SEND_GRID_KEY}
|
||||||
|
|||||||
@@ -1132,7 +1132,7 @@ components:
|
|||||||
required: true
|
required: true
|
||||||
example:
|
example:
|
||||||
Lorem upsum...
|
Lorem upsum...
|
||||||
descritption:
|
description:
|
||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
example:
|
example:
|
||||||
|
|||||||
Reference in New Issue
Block a user