Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/on_push_pr_main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
uses: ./.github/workflows/flow_backend_build_push.yml
strategy:
matrix:
service: [api_gateway, jobs, messaging, employer]
service: [api_gateway, jobs, messaging, employer, notification]
with:
service-name: ${{ matrix.service }}
push-image: ${{ github.event_name != 'pull_request' }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/on_semver_tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
uses: ./.github/workflows/flow_backend_build_push.yml
strategy:
matrix:
service: [api_gateway, jobs, messaging, employer]
service: [api_gateway, jobs, messaging, employer, notification]
with:
service-name: ${{ matrix.service }}
push-image: ${{ github.event_name != 'pull_request' }}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.linkedout.backend.controller

import com.linkedout.backend.model.Notification
import com.linkedout.backend.service.NotificationService
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Flux
import java.security.Principal

@RestController
@RequestMapping("/api/v1/notifications")
class NotificationController(
private val notificationService: NotificationService
) {
@GetMapping
open fun getNotificationsOfUser(request: ServerHttpRequest, principal: Principal): Flux<Notification> {
return Flux.fromIterable(notificationService.findAllOfUser(request.id, principal.name))
}

@DeleteMapping
open fun deleteAllNotificationsOfUser(request: ServerHttpRequest, principal: Principal) {
notificationService.deleteAllOfUser(request.id, principal.name)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.linkedout.backend.model

data class Notification(
val id: String,
val title: String,
val content: String,
val sentAt: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.linkedout.backend.service

import com.linkedout.common.service.NatsService
import com.linkedout.common.utils.RequestResponseFactory
import com.linkedout.proto.services.Notification
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import java.time.format.DateTimeFormatter
import java.util.*
import com.linkedout.backend.model.Notification as NotificationModel

@Service
class NotificationService(
private val natsService: NatsService,
@Value("\${app.services.notifications.subjects.sendTo}") private val sendToSubject: String,
@Value("\${app.services.notifications.subjects.findAllOfUser}") private val findAllOfUserSubject: String,
@Value("\${app.services.notifications.subjects.deleteAllOfUser}") private val deleteAllOfUserSubject: String
) {
fun sendTo(requestId: String, userId: String, title: String, content: String) {
// Send the notification to the notification service
val request = RequestResponseFactory.newRequest(requestId)
.setSendNotificationToRequest(
Notification.SendNotificationToRequest.newBuilder()
.setUserId(userId)
.setTitle(title)
.setContent(content)
)
.build()

natsService.request(sendToSubject, request)
}

fun findAllOfUser(requestId: String, userId: String): List<NotificationModel> {
// Send the request to the notification service
val request = RequestResponseFactory.newRequest(requestId)
.setGetUserNotificationsRequest(
Notification.GetUserNotificationsRequest.newBuilder()
.setUserId(userId)
)
.build()

val response = natsService.requestWithReply(findAllOfUserSubject, request)

// Handle the response
if (!response.hasGetUserNotificationsResponse()) {
throw Exception("Invalid response")
}

val getUserNotificationsResponse = response.getUserNotificationsResponse

return getUserNotificationsResponse.notificationsList.map { notification ->
val date = Date(notification.createdAt)

NotificationModel(
notification.id,
notification.title,
notification.content,
DateTimeFormatter.ISO_INSTANT.format(date.toInstant())
)
}
}

fun deleteAllOfUser(requestId: String, userId: String) {
// Send the request to the notification service
val request = RequestResponseFactory.newRequest(requestId)
.setDeleteUserNotificationsRequest(
Notification.DeleteUserNotificationsRequest.newBuilder()
.setUserId(userId)
)
.build()

natsService.requestWithReply(deleteAllOfUserSubject, request)
}
}
5 changes: 5 additions & 0 deletions backend/api_gateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ app:
subjects:
findOne: employer.findOne
findMultiple: employer.findMultiple
notifications:
subjects:
sendTo: notifications.sendTo
findAllOfUser: notifications.findAllOfUser
deleteAllOfUser: notifications.deleteAllOfUser
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ class NatsService(@Value("\${nats.spring.server}") private val natsUrl: String,

return response
}

fun request(subject: String, request: Request) {
nc.publish(subject, request.toByteArray())
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.linkedout.jobs
package com.linkedout.messaging

import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
Expand Down
51 changes: 51 additions & 0 deletions backend/notification/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id("org.springframework.boot")
id("io.spring.dependency-management")
id("org.jlleitschuh.gradle.ktlint")
kotlin("jvm")
kotlin("plugin.spring")
kotlin("plugin.jpa")
}

java {
sourceCompatibility = JavaVersion.VERSION_17
}

repositories {
mavenCentral()
}

dependencies {
implementation(project(":common"))
implementation(project(":protobuf"))
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework:spring-jdbc")
implementation("org.springframework.cloud:spring-cloud-stream:4.0.4")
implementation("io.nats:jnats:2.17.1")
implementation("io.nats:nats-spring:0.5.6")
implementation("io.nats:nats-spring-cloud-stream-binder:0.5.3")
implementation("org.flywaydb:flyway-core:9.22.3")
implementation("org.postgresql:r2dbc-postgresql:1.0.2.RELEASE")
implementation("jakarta.validation:jakarta.validation-api:3.0.2")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.20")
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.cloud:spring-cloud-stream-test-binder:4.0.4")
}

tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "17"
}
}

tasks.withType<Test> {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.linkedout.notification

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication(scanBasePackages = ["com.linkedout"])
class NotificationApplication

fun main(args: Array<String>) {
runApplication<NotificationApplication>(*args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.linkedout.notification.function.notifications

import com.linkedout.common.utils.RequestResponseFactory
import com.linkedout.common.utils.handleRequestError
import com.linkedout.notification.service.NotificationService
import com.linkedout.proto.RequestOuterClass.Request
import com.linkedout.proto.ResponseOuterClass.Response
import com.linkedout.proto.services.Notification
import org.springframework.stereotype.Component
import java.util.UUID
import java.util.function.Function

@Component
class DeleteNotificationsOfUser(private val notificationService: NotificationService) : Function<Request, Response> {
override fun apply(t: Request): Response = handleRequestError {
// Extract the request
val request = t.deleteUserNotificationsRequest
val userId = UUID.fromString(request.userId)

// Delete the notifications from the database
notificationService.deleteBySeasonWorkerId(userId).block()

return RequestResponseFactory.newSuccessfulResponse()
.setDeleteUserNotificationsResponse(Notification.DeleteUserNotificationsResponse.getDefaultInstance())
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.linkedout.notification.function.notifications

import com.linkedout.common.utils.RequestResponseFactory
import com.linkedout.common.utils.handleRequestError
import com.linkedout.notification.service.NotificationService
import com.linkedout.proto.RequestOuterClass.Request
import com.linkedout.proto.ResponseOuterClass.Response
import com.linkedout.proto.models.NotificationOuterClass
import com.linkedout.proto.services.Notification.GetUserNotificationsResponse
import org.springframework.stereotype.Component
import java.time.ZoneOffset
import java.util.*
import java.util.function.Function

@Component
class GetNotificationsOfUser(private val notificationService: NotificationService) : Function<Request, Response> {
override fun apply(t: Request): Response = handleRequestError {
// Extract the request
val request = t.getUserNotificationsRequest
val userId = UUID.fromString(request.userId)

// Get the notifications from the database
val reactiveResponse = notificationService.findBySeasonWorkerId(userId)
.map { notification ->
NotificationOuterClass.Notification.newBuilder()
.setId(notification.id.toString())
.setCreatedAt(notification.created.toEpochSecond(ZoneOffset.UTC) * 1000)
.setTitle(notification.title)
.setContent(notification.content)
.build()
}
.reduce(GetUserNotificationsResponse.newBuilder()) { builder, notification ->
builder.addNotifications(notification)
builder
}
.map { builder ->
builder.build()
}

// Block until the response is ready
val response = reactiveResponse.block()
?: return RequestResponseFactory.newSuccessfulResponse()
.setGetUserNotificationsResponse(GetUserNotificationsResponse.getDefaultInstance())
.build()

return RequestResponseFactory.newSuccessfulResponse()
.setGetUserNotificationsResponse(response)
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.linkedout.notification.function.notifications

import com.linkedout.notification.service.NotificationService
import com.linkedout.proto.RequestOuterClass.Request
import org.springframework.stereotype.Component
import java.util.UUID
import java.util.function.Function

@Component
class SendNotification(private val notificationService: NotificationService) : Function<Request, Unit> {
override fun apply(t: Request) {
// Extract the request
val request = t.sendNotificationToRequest
val userId = UUID.fromString(request.userId)

// Insert the notification into the database
val reactiveResponse = notificationService.saveNotification(userId, request.title, request.content)
reactiveResponse.block()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.linkedout.notification.model

import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.LocalDateTime
import java.util.*

@Table("notification")
data class Notification(
@Id
val id: UUID,
@Column("seasonworkerid")
val seasonworkerId: UUID,
val title: String,
val content: String,
val created: LocalDateTime
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.linkedout.notification.repository

import com.linkedout.notification.model.Notification
import org.springframework.data.r2dbc.repository.Query
import org.springframework.data.repository.reactive.ReactiveCrudRepository
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.util.UUID

interface NotificationRepository : ReactiveCrudRepository<Notification, UUID> {
@Query(
"""
INSERT INTO notification (seasonworkerid, title, content)
VALUES (:seasonworkerId, :title, :content)
RETURNING *
"""
)
fun saveNotification(seasonworkerId: UUID, title: String, content: String): Mono<Notification>

@Query(
"""
SELECT * FROM notification
WHERE seasonworkerid = :seasonworkerId
ORDER BY created DESC
"""
)
fun findBySeasonWorkerId(seasonworkerId: UUID): Flux<Notification>

@Query(
"""
DELETE FROM notification
WHERE seasonworkerid = :seasonworkerId
"""
)
fun deleteBySeasonWorkerId(seasonworkerId: UUID): Mono<Void>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.linkedout.notification.service

import com.linkedout.notification.model.Notification
import com.linkedout.notification.repository.NotificationRepository
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.util.UUID

@Service
class NotificationService(
private val notificationRepository: NotificationRepository
) {
fun saveNotification(seasonworkerId: UUID, title: String, content: String): Mono<Notification> {
return notificationRepository.saveNotification(seasonworkerId, title, content)
}

fun findBySeasonWorkerId(seasonworkerId: UUID): Flux<Notification> {
return notificationRepository.findBySeasonWorkerId(seasonworkerId)
}

fun deleteBySeasonWorkerId(seasonworkerId: UUID): Mono<Void> {
return notificationRepository.deleteBySeasonWorkerId(seasonworkerId)
}
}
Loading