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
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.linkedout.backend.controller

import com.linkedout.backend.model.Recommendation
import com.linkedout.backend.service.RecommendationService
import org.springframework.http.server.reactive.ServerHttpRequest
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

@RestController
@RequestMapping("/api/v1/recommendations")
open class RecommendationController(private val recommendationService: RecommendationService) {
@GetMapping
open fun getJobs(request: ServerHttpRequest): Flux<Recommendation> {
return Flux.fromIterable(recommendationService.findAll(request.id))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.linkedout.backend.model

data class Recommendation(
val id: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.linkedout.backend.service

import com.linkedout.backend.model.Recommendation
import com.linkedout.common.service.NatsService
import com.linkedout.common.utils.RequestResponseFactory
import com.linkedout.proto.services.Recommendations
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service

@Service
class RecommendationService(
private val natsService: NatsService,
@Value("\${app.services.recommendation.subjects.findAll}") private val findAllSubject: String
) {
fun findAll(requestId: String): List<Recommendation> {
// Request job offers from the job service
val request = RequestResponseFactory.newRequest(requestId)
.setGetRecommendationRequest(
Recommendations.GetRecommendationRequest.newBuilder()
)
.build()

val response = natsService.requestWithReply(findAllSubject, request)

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

val getRecommendationResponse = response.getRecommendationResponse
return getRecommendationResponse.recommendationsList.map { recommendation ->
Recommendation(
recommendation.id
)
}
}
}
3 changes: 3 additions & 0 deletions backend/api_gateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ nats:

app:
services:
recommendation:
subjects:
findAll: recommendations.findAll
jobs:
subjects:
findAll: jobs.findAll
Expand Down
6 changes: 6 additions & 0 deletions backend/protobuf/src/main/proto/models/recommendation.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
syntax = "proto3";
package com.linkedout.proto.models;

message Recommendation {
string id = 1;
}
2 changes: 2 additions & 0 deletions backend/protobuf/src/main/proto/request.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "services/messaging.proto";
import "services/employer.proto";
import "services/notification.proto";
import "services/profile.proto";
import "services/recommendations.proto";

// Message that contains a generic request
message Request {
Expand Down Expand Up @@ -67,5 +68,6 @@ message Request {
services.SetUserProfilePictureRequest set_user_profile_picture_request = 54;
services.GetUserCvRequest get_user_cv_request = 55;
services.SetUserCvRequest set_user_cv_request = 56;
services.GetRecommendationRequest get_recommendation_request = 57;
}
}
2 changes: 2 additions & 0 deletions backend/protobuf/src/main/proto/response.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "services/messaging.proto";
import "services/employer.proto";
import "services/notification.proto";
import "services/profile.proto";
import "services/recommendations.proto";

// Message that contains a generic response
message Error {
Expand Down Expand Up @@ -71,5 +72,6 @@ message Response {
services.SetUserProfilePictureResponse set_user_profile_picture_response = 53;
services.GetUserCvResponse get_user_cv_response = 54;
services.SetUserCvResponse set_user_cv_response = 55;
services.GetRecommendationResponse get_recommendation_response = 56;
}
}
14 changes: 14 additions & 0 deletions backend/protobuf/src/main/proto/services/recommendations.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
syntax = "proto3";
package com.linkedout.proto.services;

import "models/recommendation.proto";

// Get recommendations for a user
message GetRecommendationRequest {
// string id = 1;
}

message GetRecommendationResponse {
repeated models.Recommendation recommendations = 1;
}

48 changes: 48 additions & 0 deletions backend/recommendation/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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-data-neo4j")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-webflux")
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("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,25 @@
package com.linkedout.recommendation

import org.neo4j.driver.Driver
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider
import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager
import org.springframework.data.neo4j.repository.config.ReactiveNeo4jRepositoryConfigurationExtension
import org.springframework.transaction.ReactiveTransactionManager

@SpringBootApplication(scanBasePackages = ["com.linkedout"])
class RecommendationApplication {
@Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)
fun reactiveTransactionManager(
driver: Driver,
databaseNameProvider: ReactiveDatabaseSelectionProvider
): ReactiveTransactionManager {
return ReactiveNeo4jTransactionManager(driver, databaseNameProvider)
}
}

fun main(args: Array<String>) {
runApplication<RecommendationApplication>(*args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.linkedout.recommendation.converter.recommendations

import com.linkedout.proto.models.RecommendationOuterClass
import com.linkedout.recommendation.entity.JobOfferEntity
import org.springframework.core.convert.converter.Converter

class JobOfferToProto : Converter<JobOfferEntity, RecommendationOuterClass.Recommendation.Builder> {
override fun convert(source: JobOfferEntity): RecommendationOuterClass.Recommendation.Builder {
return RecommendationOuterClass.Recommendation.newBuilder()
.setId(source.id.toString())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.linkedout.recommendation.entity

import org.springframework.data.neo4j.core.schema.Id
import org.springframework.data.neo4j.core.schema.Node
import java.util.*

@Node("JobOffer")
data class JobOfferEntity(
@Id
val id: UUID
// val geographicArea: Date,
// val createdAt: Timestamp
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.linkedout.recommendation.function.jobOffers

import com.linkedout.common.utils.RequestResponseFactory
import com.linkedout.common.utils.handleRequestError
import com.linkedout.proto.RequestOuterClass.Request
import com.linkedout.proto.ResponseOuterClass.Response
import com.linkedout.proto.services.Recommendations
import com.linkedout.recommendation.converter.recommendations.JobOfferToProto
import com.linkedout.recommendation.service.RecommendationService
import org.springframework.stereotype.Component
import java.util.function.Function

@Component
class GetRecommendations(private val recommendationService: RecommendationService) : Function<Request, Response> {
override fun apply(t: Request): Response = handleRequestError {
// Extract the request
val request = t.getUserJobOffersRequest
// Get all the job offers from the database
// TODO: Pass the user ID to then fetch the user and get his experiences etc and use it in the recommendation query
val reactiveResponse = recommendationService.findAll()
.map { jobOffer ->
JobOfferToProto().convert(jobOffer)
}
.filter { it != null }
.reduce(Recommendations.GetRecommendationResponse.newBuilder()) { builder, jobOffer ->
builder.addRecommendations(jobOffer)
builder
}
.map { builder ->
builder.build()
}

// Block until the response is ready
val response = reactiveResponse.block()
?: return RequestResponseFactory.newSuccessfulResponse()
.setGetRecommendationResponse(Recommendations.GetRecommendationResponse.getDefaultInstance())
.build()
return RequestResponseFactory.newSuccessfulResponse()
.setGetRecommendationResponse(response)
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.linkedout.recommendation.repository

import com.linkedout.recommendation.entity.JobOfferEntity
import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository
import java.util.UUID

interface JobOfferRepository : ReactiveNeo4jRepository<JobOfferEntity, UUID>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.linkedout.recommendation.service

import com.linkedout.recommendation.entity.JobOfferEntity
import com.linkedout.recommendation.repository.JobOfferRepository
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import java.util.*

@Service
class RecommendationService(
private val jobOfferRepository: JobOfferRepository
) {
fun findAll(): Flux<JobOfferEntity> {
return jobOfferRepository.findAll()
}
}
16 changes: 16 additions & 0 deletions backend/recommendation/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
spring.cloud.function.definition=getRecommendations

spring.neo4j.uri=bolt://localhost:7687
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=bitnami1

nats.spring.server=nats://localhost:4222

spring.cloud.stream.bindings.getRecommendations-in-0.destination=recommendations.findAll
spring.cloud.stream.bindings.getRecommendations-in-0.group=recommendationsSvc
spring.cloud.stream.bindings.getRecommendations-in-0.binder=nats
spring.cloud.stream.bindings.getRecommendations-in-0.content-type=application/vnd.linkedout.proto-request
spring.cloud.stream.bindings.getRecommendations-out-0.destination=
spring.cloud.stream.bindings.getRecommendations-out-0.group=recommendationsSvc
spring.cloud.stream.bindings.getRecommendations-out-0.binder=nats
spring.cloud.stream.bindings.getRecommendations-out-0.content-type=application/vnd.linkedout.proto-response
2 changes: 2 additions & 0 deletions backend/recommendation/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
server:
port: 8085
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.linkedout.recommendation

import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class RecommendationApplicationTests {
@Test
fun contextLoads() {
}
}
1 change: 1 addition & 0 deletions backend/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ include("notification")
include("messaging")
include("profile")
include("protobuf")
include("recommendation")
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ services:
ports:
- 4222:4222
- 8222:8222
# neo4j
neo4j:
image: bitnami/neo4j:4.4.29
ports:
- 7474:7474
- 7473:7473
- 7687:7687
volumes:
- neo4j_data:/bitnami/neo4j

# # Keycloak
# keycloak_postgresql:
Expand Down Expand Up @@ -58,4 +67,5 @@ services:

volumes:
postgresql_data:
neo4j_data:
# keycloak_postgresql_data: