diff --git a/backend/gateway/src/main/resources/application.yml b/backend/gateway/src/main/resources/application.yml index 11f8a8d4..602dfd55 100644 --- a/backend/gateway/src/main/resources/application.yml +++ b/backend/gateway/src/main/resources/application.yml @@ -97,6 +97,24 @@ spring: - Method=DELETE - Path=/api/people/*/photos/* + - id: put-people-person-id-interests-topic-id + uri: lb://people + predicates: + - Method=PUT + - Path=/api/people/*/interests/* + + - id: delete-people-person-id-interests-topic-id + uri: lb://people + predicates: + - Method=DELETE + - Path=/api/people/*/interests/* + + - id: get-people-topics + uri: lb://people + predicates: + - Method=GET + - Path=/api/topics + - id: get-people-faculties uri: lb://people predicates: diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/api/HttpPeopleApi.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/api/HttpPeopleApi.kt index 5147cccd..ef1b2f35 100644 --- a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/api/HttpPeopleApi.kt +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/api/HttpPeopleApi.kt @@ -12,19 +12,24 @@ import ru.ifmo.se.dating.pagging.Page import ru.ifmo.se.dating.people.api.generated.PeopleApiDelegate import ru.ifmo.se.dating.people.api.mapping.toMessage import ru.ifmo.se.dating.people.api.mapping.toModel +import ru.ifmo.se.dating.people.logic.InterestService import ru.ifmo.se.dating.people.logic.PersonService import ru.ifmo.se.dating.people.logic.PictureService import ru.ifmo.se.dating.people.model.Faculty +import ru.ifmo.se.dating.people.model.Person import ru.ifmo.se.dating.people.model.Picture +import ru.ifmo.se.dating.people.model.Topic import ru.ifmo.se.dating.people.model.generated.* import ru.ifmo.se.dating.security.auth.User import ru.ifmo.se.dating.spring.security.auth.SpringSecurityContext import java.time.LocalDate import java.time.OffsetDateTime +@Suppress("TooManyFunctions") @Controller class HttpPeopleApi( private val personService: PersonService, + private val interestService: InterestService, private val pictureService: PictureService, ) : PeopleApiDelegate { override fun peopleGet( @@ -109,6 +114,29 @@ class HttpPeopleApi( return ResponseEntity.ok(Unit) } + override suspend fun peoplePersonIdInterestsTopicIdDelete( + personId: Long, + topicId: Long, + ): ResponseEntity { + interestService.remove(User.Id(personId.toInt()), Topic.Id(topicId.toInt())) + return ResponseEntity.ok(Unit) + } + + override suspend fun peoplePersonIdInterestsTopicIdPut( + personId: Long, + topicId: Long, + interestPatchMessage: InterestPatchMessage, + ): ResponseEntity { + interestService.insert( + id = User.Id(personId.toInt()), + interest = Person.Interest( + topicId = Topic.Id(topicId.toInt()), + degree = interestPatchMessage.level.value.toInt(), + ) + ) + return ResponseEntity.ok(Unit) + } + override suspend fun peoplePersonIdPhotosPost( personId: Long, body: Resource?, diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/api/HttpTopicsApi.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/api/HttpTopicsApi.kt new file mode 100644 index 00000000..a3a47c1c --- /dev/null +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/api/HttpTopicsApi.kt @@ -0,0 +1,20 @@ +package ru.ifmo.se.dating.people.api + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import ru.ifmo.se.dating.people.api.generated.TopicsApiDelegate +import ru.ifmo.se.dating.people.api.mapping.toMessage +import ru.ifmo.se.dating.people.logic.InterestService +import ru.ifmo.se.dating.people.model.generated.TopicMessage + +@Controller +class HttpTopicsApi( + private val interestService: InterestService, +) : TopicsApiDelegate { + override fun topicsGet(): ResponseEntity> = + interestService.getAllTopics() + .map { it.toMessage() } + .let { ResponseEntity.ok(it) } +} diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/api/mapping/PersonMapping.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/api/mapping/PersonMapping.kt index 8af6325a..f6dbf045 100644 --- a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/api/mapping/PersonMapping.kt +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/api/mapping/PersonMapping.kt @@ -1,9 +1,6 @@ package ru.ifmo.se.dating.people.api.mapping -import ru.ifmo.se.dating.people.model.Faculty -import ru.ifmo.se.dating.people.model.Location -import ru.ifmo.se.dating.people.model.Person -import ru.ifmo.se.dating.people.model.PersonVariant +import ru.ifmo.se.dating.people.model.* import ru.ifmo.se.dating.people.model.generated.* import ru.ifmo.se.dating.security.auth.User @@ -13,6 +10,7 @@ fun PersonPatchMessage.toModel(id: Int) = Person.Draft( lastName = lastName?.let { Person.Name(it) }, height = height, birthday = birthday, + interests = emptySet(), facultyId = facultyId?.let { Faculty.Id(it.toInt()) }, locationId = locationId?.let { Location.Id(it.toInt()) }, ) @@ -31,7 +29,7 @@ fun Person.toMessage() = PersonMessage( birthday = birthday, facultyId = facultyId.number.toLong(), locationId = locationId.number.toLong(), - interests = emptySet(), + interests = interests.map { it.toMessage() }.toSet(), zodiac = ZodiacSignMessage.leo, pictures = pictureIds.map { PictureMessage(id = it.number.toLong()) }.toSet() ) @@ -45,6 +43,18 @@ fun Person.Draft.toMessage() = PersonDraftMessage( birthday = birthday, facultyId = facultyId?.number?.toLong(), locationId = locationId?.number?.toLong(), - interests = emptySet(), + interests = interests.map { it.toMessage() }.toSet(), pictures = pictureIds.map { PictureMessage(id = it.number.toLong()) }.toSet() ) + +fun InterestMessage.toModel(): Person.Interest = + Person.Interest( + topicId = Topic.Id(topicId.toInt()), + degree = level.value.toInt(), + ) + +fun Person.Interest.toMessage(): InterestMessage = + InterestMessage( + topicId = topicId.number.toLong(), + level = InterestLevelMessage.forValue(degree.toString()), + ) diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/api/mapping/TopicMapping.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/api/mapping/TopicMapping.kt new file mode 100644 index 00000000..050e96b4 --- /dev/null +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/api/mapping/TopicMapping.kt @@ -0,0 +1,11 @@ +package ru.ifmo.se.dating.people.api.mapping + +import ru.ifmo.se.dating.people.model.Topic +import ru.ifmo.se.dating.people.model.generated.TopicMessage + +fun Topic.toMessage(): TopicMessage = + TopicMessage( + id = id.number.toLong(), + name = name, + color = color.hex, + ) diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/InterestService.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/InterestService.kt new file mode 100644 index 00000000..45c7cdb2 --- /dev/null +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/InterestService.kt @@ -0,0 +1,12 @@ +package ru.ifmo.se.dating.people.logic + +import kotlinx.coroutines.flow.Flow +import ru.ifmo.se.dating.people.model.Person +import ru.ifmo.se.dating.people.model.Topic +import ru.ifmo.se.dating.security.auth.User + +interface InterestService { + suspend fun insert(id: User.Id, interest: Person.Interest) + suspend fun remove(id: User.Id, topicId: Topic.Id) + fun getAllTopics(): Flow +} diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/basic/BasicInterestService.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/basic/BasicInterestService.kt new file mode 100644 index 00000000..a3194a5f --- /dev/null +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/basic/BasicInterestService.kt @@ -0,0 +1,21 @@ +package ru.ifmo.se.dating.people.logic.basic + +import kotlinx.coroutines.flow.Flow +import ru.ifmo.se.dating.people.logic.InterestService +import ru.ifmo.se.dating.people.model.Person +import ru.ifmo.se.dating.people.model.Topic +import ru.ifmo.se.dating.people.storage.InterestStorage +import ru.ifmo.se.dating.security.auth.User + +class BasicInterestService( + private val storage: InterestStorage, +) : InterestService { + override suspend fun insert(id: User.Id, interest: Person.Interest) = + storage.upsert(id, interest) + + override suspend fun remove(id: User.Id, topicId: Topic.Id) = + storage.delete(id, topicId) + + override fun getAllTopics(): Flow = + storage.selectAllTopics() +} diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/basic/BasicPersonService.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/basic/BasicPersonService.kt index bd21453b..2e37cd80 100644 --- a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/basic/BasicPersonService.kt +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/basic/BasicPersonService.kt @@ -49,6 +49,7 @@ class BasicPersonService( if ( variant is Person.Draft && expected.copy( + interests = variant.interests, pictureIds = variant.pictureIds, version = variant.version, ) != variant diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/logging/LoggingInterestService.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/logging/LoggingInterestService.kt new file mode 100644 index 00000000..1ece6ff5 --- /dev/null +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/logging/LoggingInterestService.kt @@ -0,0 +1,42 @@ +package ru.ifmo.se.dating.people.logic.logging + +import kotlinx.coroutines.flow.Flow +import ru.ifmo.se.dating.logging.Log.Companion.autoLog +import ru.ifmo.se.dating.people.logic.InterestService +import ru.ifmo.se.dating.people.model.Person +import ru.ifmo.se.dating.people.model.Topic +import ru.ifmo.se.dating.security.auth.User + +class LoggingInterestService(private val origin: InterestService) : InterestService by origin { + private val log = autoLog() + + override suspend fun insert(id: User.Id, interest: Person.Interest) = + runCatching { origin.insert(id, interest) } + .onSuccess { + buildString { + append("Person with id $id is interested in ") + append("${interest.topicId} at degree ${interest.degree}") + }.let { log.info(it) } + } + .getOrThrow() + + override suspend fun remove(id: User.Id, topicId: Topic.Id) = + runCatching { origin.remove(id, topicId) } + .onSuccess { + log.info("Removed an interest in topic with id $topicId from user with id $id") + } + .onFailure { e -> + buildString { + append("Failed to remove an interest in ") + append("topic with id $topicId from ") + append("user with id $id: ${e.message}") + }.let { log.warn(it) } + } + .getOrThrow() + + override fun getAllTopics(): Flow = + runCatching { origin.getAllTopics() } + .onSuccess { log.debug("Got all topics") } + .onFailure { e -> log.warn("Failed to get all topics: ${e.message}") } + .getOrThrow() +} diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/spring/SpringInterestService.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/spring/SpringInterestService.kt new file mode 100644 index 00000000..f887ff31 --- /dev/null +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/logic/spring/SpringInterestService.kt @@ -0,0 +1,15 @@ +package ru.ifmo.se.dating.people.logic.spring + +import org.springframework.stereotype.Service +import ru.ifmo.se.dating.people.logic.InterestService +import ru.ifmo.se.dating.people.logic.basic.BasicInterestService +import ru.ifmo.se.dating.people.logic.logging.LoggingInterestService +import ru.ifmo.se.dating.people.storage.InterestStorage + +@Service +class SpringInterestService( + storage: InterestStorage, +) : InterestService by +LoggingInterestService( + BasicInterestService(storage) +) diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/model/Person.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/model/Person.kt index 63f94678..109ec3c4 100644 --- a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/model/Person.kt +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/model/Person.kt @@ -1,5 +1,6 @@ package ru.ifmo.se.dating.people.model +import ru.ifmo.se.dating.people.model.Person.Interest import ru.ifmo.se.dating.security.auth.User import ru.ifmo.se.dating.validation.expect import ru.ifmo.se.dating.validation.expectInRange @@ -8,6 +9,7 @@ import java.time.LocalDate sealed class PersonVariant { abstract val id: User.Id + abstract val interests: Set abstract val pictureIds: List abstract val version: Person.Version } @@ -20,6 +22,7 @@ data class Person( val birthday: LocalDate, val facultyId: Faculty.Id, val locationId: Location.Id, + override val interests: Set, override val pictureIds: List, override val version: Version, val isPublished: Boolean, @@ -35,6 +38,19 @@ data class Person( } } + data class Interest( + val topicId: Topic.Id, + val degree: Int, + ) { + init { + expectInRange("Level", degree, degreeRange) + } + + companion object { + private val degreeRange = 1..5 + } + } + @JvmInline value class Version(val number: Int) { init { @@ -48,6 +64,7 @@ data class Person( val lastName: Name? = null, val height: Int? = null, val birthday: LocalDate? = null, + override val interests: Set = emptySet(), val facultyId: Faculty.Id? = null, val locationId: Location.Id? = null, override val pictureIds: List = emptyList(), diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/model/Topic.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/model/Topic.kt new file mode 100644 index 00000000..225e5b38 --- /dev/null +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/model/Topic.kt @@ -0,0 +1,40 @@ +package ru.ifmo.se.dating.people.model + +import ru.ifmo.se.dating.validation.expectId +import ru.ifmo.se.dating.validation.expectMatches + +data class Topic( + val id: Id, + val name: String, + val color: Color, +) { + @JvmInline + value class Id(val number: Int) { + init { + expectId(number) + } + + override fun toString(): String = number.toString() + } + + @JvmInline + value class Color(val hex: String) { + init { + expectMatches("Hex", hex, regex) + } + + override fun toString(): String = hex + + companion object { + private val regex = Regex("#[A-F0-9]{6}") + } + } + + init { + expectMatches("Name", name, nameRegex) + } + + companion object { + private val nameRegex = Regex("[A-Z][a-z]{3,32}") + } +} diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/InterestStorage.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/InterestStorage.kt new file mode 100644 index 00000000..a449d3dd --- /dev/null +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/InterestStorage.kt @@ -0,0 +1,13 @@ +package ru.ifmo.se.dating.people.storage + +import kotlinx.coroutines.flow.Flow +import ru.ifmo.se.dating.people.model.Person +import ru.ifmo.se.dating.people.model.Topic +import ru.ifmo.se.dating.security.auth.User + +interface InterestStorage { + suspend fun upsert(id: User.Id, interest: Person.Interest) + suspend fun delete(id: User.Id, topicId: Topic.Id) + fun selectInterestsByPersonId(id: User.Id): Flow + fun selectAllTopics(): Flow +} diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/jooq/JooqInterestStorage.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/jooq/JooqInterestStorage.kt new file mode 100644 index 00000000..d025ccc8 --- /dev/null +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/jooq/JooqInterestStorage.kt @@ -0,0 +1,46 @@ +package ru.ifmo.se.dating.people.storage.jooq + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.jooq.generated.tables.references.PERSON_INTEREST +import org.jooq.generated.tables.references.TOPIC +import org.springframework.stereotype.Repository +import ru.ifmo.se.dating.people.model.Person +import ru.ifmo.se.dating.people.model.Topic +import ru.ifmo.se.dating.people.storage.InterestStorage +import ru.ifmo.se.dating.people.storage.jooq.mapping.toModel +import ru.ifmo.se.dating.security.auth.User +import ru.ifmo.se.dating.storage.jooq.JooqDatabase + +@Repository +class JooqInterestStorage( + private val database: JooqDatabase, +) : InterestStorage { + override suspend fun upsert(id: User.Id, interest: Person.Interest) = database.only { + insertInto(PERSON_INTEREST) + .set(PERSON_INTEREST.PERSON_ID, id.number) + .set(PERSON_INTEREST.TOPIC_ID, interest.topicId.number) + .set(PERSON_INTEREST.DEGREE, interest.degree) + .onConflict(PERSON_INTEREST.PERSON_ID, PERSON_INTEREST.TOPIC_ID) + .doUpdate() + .set(PERSON_INTEREST.DEGREE, interest.degree) + .returning() + }.let { } + + override suspend fun delete(id: User.Id, topicId: Topic.Id) = database.only { + delete(PERSON_INTEREST) + .where( + PERSON_INTEREST.PERSON_ID.eq(id.number) + .and(PERSON_INTEREST.TOPIC_ID.eq(topicId.number)) + ) + }.let {} + + override fun selectInterestsByPersonId(id: User.Id) = database.flow { + selectFrom(PERSON_INTEREST) + .where(PERSON_INTEREST.PERSON_ID.eq(id.number)) + }.map { it.toModel() } + + override fun selectAllTopics(): Flow = database.flow { + selectFrom(TOPIC) + }.map { it.toModel() } +} diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/jooq/JooqPersonStorage.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/jooq/JooqPersonStorage.kt index 96e4e2c7..702ae839 100644 --- a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/jooq/JooqPersonStorage.kt +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/jooq/JooqPersonStorage.kt @@ -1,8 +1,11 @@ package ru.ifmo.se.dating.people.storage.jooq +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.flow.toSet import org.jooq.generated.tables.records.PersonRecord import org.jooq.generated.tables.references.PERSON import org.jooq.impl.DSL.currentOffsetDateTime @@ -11,6 +14,7 @@ import org.springframework.stereotype.Repository import ru.ifmo.se.dating.exception.NotFoundException import ru.ifmo.se.dating.people.model.Person import ru.ifmo.se.dating.people.model.PersonVariant +import ru.ifmo.se.dating.people.storage.InterestStorage import ru.ifmo.se.dating.people.storage.PersonStorage import ru.ifmo.se.dating.people.storage.PictureRecordStorage import ru.ifmo.se.dating.people.storage.jooq.mapping.toModel @@ -26,6 +30,7 @@ class JooqPersonStorage( private val database: JooqDatabase, private val txEnv: TxEnv, private val pictureRecords: PictureRecordStorage, + private val interests: InterestStorage, ) : PersonStorage { @Suppress("CyclomaticComplexMethod") override suspend fun upsert(draft: Person.Draft): PersonVariant = txEnv.transactional { @@ -100,10 +105,29 @@ class JooqPersonStorage( .where(PERSON.READY_MOMENT.isNotNull) }.map { it.enrichToModel() as Person } - private suspend fun PersonRecord.enrichToModel() = - pictureRecords - .selectByOwner(User.Id(accountId)) - .toList(mutableListOf()) - .map { it.id } - .let { ids -> toModel(pictureIds = ids) } + private suspend fun PersonRecord.enrichToModel(): PersonVariant { + val userId = User.Id(accountId) + + val (interests, pictureIds) = coroutineScope { + val interests = async { + interests + .selectInterestsByPersonId(userId) + .toSet(mutableSetOf()) + } + + val pictureIds = async { + pictureRecords + .selectByOwner(userId) + .toList(mutableListOf()) + .map { it.id } + } + + Pair(interests.await(), pictureIds.await()) + } + + return toModel( + interests = interests, + pictureIds = pictureIds, + ) + } } diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/jooq/mapping/PersonMapping.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/jooq/mapping/PersonMapping.kt index 4f037d71..3c38ae75 100644 --- a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/jooq/mapping/PersonMapping.kt +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/jooq/mapping/PersonMapping.kt @@ -1,10 +1,14 @@ package ru.ifmo.se.dating.people.storage.jooq.mapping +import org.jooq.generated.tables.records.PersonInterestRecord import org.jooq.generated.tables.records.PersonRecord import ru.ifmo.se.dating.people.model.* import ru.ifmo.se.dating.security.auth.User -fun PersonRecord.toModel(pictureIds: List): PersonVariant = +fun PersonRecord.toModel( + interests: Set, + pictureIds: List, +): PersonVariant = if (readyMoment != null) { Person( id = User.Id(accountId), @@ -12,6 +16,7 @@ fun PersonRecord.toModel(pictureIds: List): PersonVariant = lastName = Person.Name(lastName!!), height = height!!, birthday = birthday!!, + interests = interests, facultyId = Faculty.Id(facultyId!!), locationId = Location.Id(locationId!!), pictureIds = pictureIds, @@ -25,9 +30,16 @@ fun PersonRecord.toModel(pictureIds: List): PersonVariant = lastName = lastName?.let { Person.Name(it) }, height = height, birthday = birthday, + interests = interests, facultyId = facultyId?.let { Faculty.Id(it) }, locationId = locationId?.let { Location.Id(it) }, pictureIds = pictureIds, version = Person.Version(version!!), ) } + +fun PersonInterestRecord.toModel(): Person.Interest = + Person.Interest( + topicId = Topic.Id(topicId), + degree = degree, + ) diff --git a/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/jooq/mapping/TopicMapping.kt b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/jooq/mapping/TopicMapping.kt new file mode 100644 index 00000000..910bcf7d --- /dev/null +++ b/backend/people/src/main/kotlin/ru/ifmo/se/dating/people/storage/jooq/mapping/TopicMapping.kt @@ -0,0 +1,11 @@ +package ru.ifmo.se.dating.people.storage.jooq.mapping + +import org.jooq.generated.tables.records.TopicRecord +import ru.ifmo.se.dating.people.model.Topic + +fun TopicRecord.toModel(): Topic = + Topic( + id = Topic.Id(id!!), + name = name, + color = Topic.Color(color), + ) diff --git a/backend/people/src/main/resources/database/changelog.sql b/backend/people/src/main/resources/database/changelog.sql index 1e775028..f93b3f25 100644 --- a/backend/people/src/main/resources/database/changelog.sql +++ b/backend/people/src/main/resources/database/changelog.sql @@ -63,3 +63,26 @@ CREATE TABLE people.picture ( is_referenced boolean NOT NULL, creation_moment timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP ); + +--changeset vityaman:topic +CREATE TABLE people.topic ( + id serial PRIMARY KEY, + name varchar(32) NOT NULL UNIQUE, + color varchar(8) NOT NULL UNIQUE +); + +INSERT INTO people.topic (name, color) +VALUES + ('Programming', '#9DE19A'), + ('Coding', '#A4C5EA'), + ('Debugging', '#BCA9E1'), + ('Compilers', '#E7ECA3'), + ('Databases', '#98A7F2'); + +--changeset vityaman:interest +CREATE TABLE people.person_interest ( + person_id integer NOT NULL REFERENCES people.person(account_id), + topic_id integer NOT NULL REFERENCES people.topic(id), + degree integer NOT NULL, + PRIMARY KEY (person_id, topic_id) +); diff --git a/backend/people/src/main/resources/static/openapi/api.yml b/backend/people/src/main/resources/static/openapi/api.yml index d750c4e7..cf82de07 100644 --- a/backend/people/src/main/resources/static/openapi/api.yml +++ b/backend/people/src/main/resources/static/openapi/api.yml @@ -15,6 +15,7 @@ tags: - name: Monitoring - name: People - name: Photos + - name: Interests - name: Faculties - name: Locations paths: @@ -299,6 +300,61 @@ paths: $ref: "#/components/responses/503" default: $ref: "#/components/responses/Unexpected" + /people/{person_id}/interests/{topic_id}: + put: + tags: [ Interests ] + security: + - bearerAuth: [ USER ] + parameters: + - $ref: "#/components/parameters/PersonIdPath" + - $ref: "#/components/parameters/TopicIdPath" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InterestPatch" + responses: + "204": + description: OK + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" + "503": + $ref: "#/components/responses/503" + default: + $ref: "#/components/responses/Unexpected" + delete: + tags: [ Interests ] + security: + - bearerAuth: [ USER ] + parameters: + - $ref: "#/components/parameters/PersonIdPath" + - $ref: "#/components/parameters/TopicIdPath" + responses: + "204": + description: Deleted + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" + "503": + $ref: "#/components/responses/503" + default: + $ref: "#/components/responses/Unexpected" /people/{person_id}/photos: post: tags: [ Photos ] @@ -392,6 +448,33 @@ paths: $ref: "#/components/responses/503" default: $ref: "#/components/responses/Unexpected" + /topics: + get: + tags: [ Interests ] + summary: Get all topics + description: Returns all topics list + security: + - bearerAuth: [ USER ] + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Topic" + maxItems: 512 + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "500": + $ref: "#/components/responses/500" + "503": + $ref: "#/components/responses/503" + default: + $ref: "#/components/responses/Unexpected" /faculties: get: tags: [ Faculties ] @@ -409,6 +492,16 @@ paths: items: $ref: "#/components/schemas/Faculty" maxItems: 512 + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "500": + $ref: "#/components/responses/500" + "503": + $ref: "#/components/responses/503" + default: + $ref: "#/components/responses/Unexpected" /locations: get: tags: [ Locations ] @@ -426,6 +519,16 @@ paths: items: $ref: "#/components/schemas/Location" maxItems: 512 + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "500": + $ref: "#/components/responses/500" + "503": + $ref: "#/components/responses/503" + default: + $ref: "#/components/responses/Unexpected" components: securitySchemes: bearerAuth: @@ -440,6 +543,12 @@ components: required: true schema: $ref: "#/components/schemas/PersonId" + TopicIdPath: + name: topic_id + in: path + required: true + schema: + $ref: "#/components/schemas/TopicId" PictureIdPath: name: picture_id in: path @@ -640,17 +749,30 @@ components: $ref: "#/components/schemas/PictureId" required: - id + InterestLevel: + type: string + description: A level of interest + enum: + - 1 + - 2 + - 3 + - 4 + - 5 + example: 3 + InterestPatch: + type: object + properties: + level: + $ref: "#/components/schemas/InterestLevel" + required: + - level Interest: type: object properties: topicId: $ref: "#/components/schemas/TopicId" level: - type: integer - description: A level of interest - format: int32 - minimum: 1 - maximum: 5 + $ref: "#/components/schemas/InterestLevel" required: - topicId - level @@ -672,8 +794,6 @@ components: pattern: ^[A-Z][a-z]{3,32}$ maxLength: 32 example: Programming - icon: - $ref: "#/components/schemas/Picture" color: type: string description: An RGB color in the hex format @@ -683,7 +803,6 @@ components: required: - id - name - - icon - color LocationId: type: integer