diff --git a/client/src/i18n.ts b/client/src/i18n.ts index 8b88dcb8..6350179e 100644 --- a/client/src/i18n.ts +++ b/client/src/i18n.ts @@ -31,7 +31,8 @@ export const I18N_INSTRUMENT_DATE_TO = "instrument.date.to"; export const I18N_INSTRUMENT_CARD_SHOW_BUTTON = "instrument.card.show.button"; export const I18N_INSTRUMENT_CARD_REMOVE_BUTTON = "instrument.remove.button"; export const I18N_INSTRUMENT_CARD_EDIT_BUTTON = "instrument.edit.button"; -export const I18N_INSTRUMENT_CARD_FAVORITE_BUTTON = "instrument.favorite.button"; +export const I18N_INSTRUMENT_CARD_FAVORITE_BUTTON = + "instrument.favorite.button"; export const I18N_LOGIN_INPUT = "login.login.input"; export const I18N_LOGIN_PASSWORD_INPUT = "login.login.password"; @@ -63,9 +64,12 @@ const resources = { [I18N_HOME_SEARCH_BAR_INPUT]: "What instrument?", [I18N_HOME_SEARCH_BAR_BUTTON]: "Search", [I18N_REASONS_H1]: "Why Choose Us for Your Musical Needs", - [I18N_REASONS_FIRST]: "We offer a wide range of high-quality instruments for all skill levels", - [I18N_REASONS_SECOND]: "Our expert staff provides personalized advice and service", - [I18N_REASONS_THIRD]: "Enjoy competitive prices and exclusive deals on top brands", + [I18N_REASONS_FIRST]: + "We offer a wide range of high-quality instruments for all skill levels", + [I18N_REASONS_SECOND]: + "Our expert staff provides personalized advice and service", + [I18N_REASONS_THIRD]: + "Enjoy competitive prices and exclusive deals on top brands", [I18N_TRENDS_H1]: "Trending Instruments", [I18N_INSTRUMENT_TYPE_FILTER]: "Type", [I18N_INSTRUMENT_CARD_MANUFACTURER]: "Manufacturer", @@ -98,7 +102,7 @@ const resources = { [I18N_NAVBAR_NEXT]: "Next", [I18N_FAVORITE_H1]: "Favorite", - } + }, }, ru: { translation: { @@ -111,9 +115,12 @@ const resources = { [I18N_HOME_SEARCH_BAR_INPUT]: "Какой инструмент?", [I18N_HOME_SEARCH_BAR_BUTTON]: "Поиск", [I18N_REASONS_H1]: "Почему вы выберете нас", - [I18N_REASONS_FIRST]: "Мы предлагаем широкий ассортимент высококачественных инструментов для всех уровней квалификации", - [I18N_REASONS_SECOND]: "Наш квалифицированный персонал предоставляет индивидуальные консультации и обслуживание", - [I18N_REASONS_THIRD]: "Наслаждайтесь конкурентоспособными ценами и эксклюзивными предложениями от ведущих брендов", + [I18N_REASONS_FIRST]: + "Мы предлагаем широкий ассортимент высококачественных инструментов для всех уровней квалификации", + [I18N_REASONS_SECOND]: + "Наш квалифицированный персонал предоставляет индивидуальные консультации и обслуживание", + [I18N_REASONS_THIRD]: + "Наслаждайтесь конкурентоспособными ценами и эксклюзивными предложениями от ведущих брендов", [I18N_TRENDS_H1]: "Тренды", [I18N_INSTRUMENT_TYPE_FILTER]: "Тип", [I18N_INSTRUMENT_CARD_MANUFACTURER]: "Производитель", @@ -144,8 +151,8 @@ const resources = { [I18N_LOGOUT_BUTTON]: "Выйти", [I18N_FAVORITE_H1]: "Любимое", - } - } + }, + }, }; i18n @@ -155,8 +162,8 @@ i18n lng: window.navigator.language, fallbackLng: "en", interpolation: { - escapeValue: false // react already safes from xss - } + escapeValue: false, // react already safes from xss + }, }); export default i18n; diff --git a/client/src/pages/home/ui/Home.page.tsx b/client/src/pages/home/ui/Home.page.tsx index 78b4cba4..8c3172ee 100644 --- a/client/src/pages/home/ui/Home.page.tsx +++ b/client/src/pages/home/ui/Home.page.tsx @@ -16,26 +16,29 @@ import { I18N_HOME_SEARCH_BAR_BUTTON, I18N_HOME_SEARCH_BAR_INPUT, I18N_REASONS_FIRST, - I18N_REASONS_H1, I18N_REASONS_SECOND, I18N_REASONS_THIRD, I18N_TRENDS_H1 + I18N_REASONS_H1, + I18N_REASONS_SECOND, + I18N_REASONS_THIRD, + I18N_TRENDS_H1, } from "../../../i18n"; const images = [ { image: saxophone, - caption: "Saxophone" + caption: "Saxophone", }, { image: guitar, - caption: "Guitar" + caption: "Guitar", }, { image: rock_guitar, - caption: "Rock Guitar" + caption: "Rock Guitar", }, { image: violin, - caption: "Violin" - } + caption: "Violin", + }, ]; const trendingInstrumentsResponsiveSettings = [ @@ -43,16 +46,16 @@ const trendingInstrumentsResponsiveSettings = [ breakpoint: 571, settings: { slidesToShow: 3, - slidesToScroll: 1 - } + slidesToScroll: 1, + }, }, { breakpoint: 570, settings: { slidesToShow: 1, - slidesToScroll: 1 - } - } + slidesToScroll: 1, + }, + }, ]; export function HomePage() { @@ -66,7 +69,7 @@ export function HomePage() {
{ const { t } = useTranslation(); - const jwt = useRef(getCookie(COOKIE_JWT_KEY)); const [favorite, setFavorite] = useState(); useEffect(() => { diff --git a/client/src/shared/instrument-card-actions/ui/RemoveInstrument.button.tsx b/client/src/shared/instrument-card-actions/ui/RemoveInstrument.button.tsx index 424b16e9..57d3dcb8 100644 --- a/client/src/shared/instrument-card-actions/ui/RemoveInstrument.button.tsx +++ b/client/src/shared/instrument-card-actions/ui/RemoveInstrument.button.tsx @@ -6,7 +6,7 @@ import { useDarkMode } from "shared/dark-mode/use-dark-mode"; import { apiConfig } from "shared/config/api"; import Jwt from "domain/model/jwt"; import { DeleteInstrumentByIdApi } from "generated/api/delete-instrument-by-id-api"; -import { I18N_INSTRUMENT_CARD_EDIT_BUTTON, I18N_INSTRUMENT_CARD_REMOVE_BUTTON } from "../../../i18n"; +import { I18N_INSTRUMENT_CARD_REMOVE_BUTTON } from "../../../i18n"; import { useTranslation } from "react-i18next"; interface Props { diff --git a/client/src/widgets/header/ui/Header.widget.tsx b/client/src/widgets/header/ui/Header.widget.tsx index f3085bae..36fba501 100644 --- a/client/src/widgets/header/ui/Header.widget.tsx +++ b/client/src/widgets/header/ui/Header.widget.tsx @@ -9,7 +9,8 @@ import { I18N_HEADER_CATALOGUE_BUTTON, I18N_HEADER_FAVORITE_BUTTON, I18N_HEADER_HOME_BUTTON, - I18N_HEADER_LOGIN_BUTTON, I18N_HEADER_PROFILE_BUTTON + I18N_HEADER_LOGIN_BUTTON, + I18N_HEADER_PROFILE_BUTTON, } from "../../../i18n"; import { useTranslation } from "react-i18next"; diff --git a/server/app/build.gradle.kts b/server/app/build.gradle.kts index ff15a6ca..dc4c3bc0 100644 --- a/server/app/build.gradle.kts +++ b/server/app/build.gradle.kts @@ -1,4 +1,5 @@ import io.gitlab.arturbosch.detekt.Detekt +import org.jooq.meta.jaxb.Property val rootProjectDir = "$projectDir/../.." @@ -13,8 +14,11 @@ plugins { id("org.sonarqube") version "5.0.0.4638" id("org.openapi.generator") version "7.8.0" id("info.solidsoft.pitest") version "1.15.0" + id("nu.studer.jooq") version "6.0.1" } +val schemaVersion by extra { "1" } + group = "mu.muse" version = "1.0.0-SNAPSHOT" @@ -38,6 +42,7 @@ springBoot { } repositories { + mavenLocal() mavenCentral() } @@ -61,6 +66,8 @@ dependencies { implementation("jakarta.validation:jakarta.validation-api:3.1.0") // `useSpringBoot3` param requires it implementation("org.postgresql:postgresql") implementation("org.springframework.boot:spring-boot-starter-data-jdbc") + implementation("org.jooq:jooq:3.19.11") + jooqGenerator("org.jooq:jooq-meta-extensions:3.19.11") } tasks.named("test") { @@ -68,7 +75,7 @@ tasks.named("test") { } configure { - debug.set(true) + debug = true verbose.set(true) ignoreFailures.set(false) @@ -145,3 +152,44 @@ pitest { targetClasses = setOf("mu.muse.*") timestampedReports = false } + +jooq { + version.set("3.19.11") + configurations { + create("main") { + jooqConfiguration.apply { + logging = org.jooq.meta.jaxb.Logging.WARN + generator.apply { + name = "org.jooq.codegen.KotlinGenerator" + database.apply { + name = "org.jooq.meta.extensions.ddl.DDLDatabase" + properties.apply { + add(Property().apply { + key = "scripts" + value = "./src/main/resources/db/schema.sql" + }) + add(Property().apply { + key = "defaultNameCase" + value = "lower" + }) + } + } + + generate.apply { + isPojos = true + isPojosAsKotlinDataClasses = true + isImmutablePojos = true + isImmutableInterfaces = true + isGeneratedAnnotation = true + isGeneratedAnnotationDate = true + isInterfaces = true + } + + target.apply { + packageName = "mu.muse.codegen.jooq" + } + } + } + } + } +} diff --git a/server/app/build/openapi/src/main/kotlin/mu/muse/rest/dto/ManufactureType.kt b/server/app/build/openapi/src/main/kotlin/mu/muse/rest/dto/ManufactureType.kt new file mode 100644 index 00000000..44a74f4e --- /dev/null +++ b/server/app/build/openapi/src/main/kotlin/mu/muse/rest/dto/ManufactureType.kt @@ -0,0 +1,28 @@ +package mu.muse.rest.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid + +/** + * + * @param i18nCode + * @param localizedMessage + */ +data class ManufactureType( + + @get:JsonProperty("i18n_code", required = true) val i18nCode: kotlin.String, + + @get:JsonProperty("localized_message") val localizedMessage: kotlin.String? = null + ) { + +} + diff --git a/server/app/src/main/kotlin/mu/muse/application/Application.kt b/server/app/src/main/kotlin/mu/muse/application/Application.kt index b5b3531f..da90db1c 100644 --- a/server/app/src/main/kotlin/mu/muse/application/Application.kt +++ b/server/app/src/main/kotlin/mu/muse/application/Application.kt @@ -18,6 +18,7 @@ import mu.muse.domain.user.UserId import mu.muse.domain.user.Username import mu.muse.usecase.access.instrument.InstrumentPersister import mu.muse.usecase.access.user.UserPersister +import org.jooq.DSLContext import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.CommandLineRunner import org.springframework.boot.autoconfigure.EnableAutoConfiguration diff --git a/server/app/src/main/kotlin/mu/muse/application/muse/PersistenceConfiguration.kt b/server/app/src/main/kotlin/mu/muse/application/muse/PersistenceConfiguration.kt index 8b0a79f5..847289f2 100644 --- a/server/app/src/main/kotlin/mu/muse/application/muse/PersistenceConfiguration.kt +++ b/server/app/src/main/kotlin/mu/muse/application/muse/PersistenceConfiguration.kt @@ -2,14 +2,16 @@ package mu.muse.application.muse import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource -import mu.muse.persistence.instrument.postgres.PostgresInstrumentIdGenerator -import mu.muse.persistence.instrument.postgres.PostgresInstrumentRepository -import mu.muse.persistence.user.postgres.PostgresUserIdGenerator -import mu.muse.persistence.user.postgres.PostgresUserRepository +import mu.muse.persistence.instrument.jooq.JooqPostgresInstrumentIdGenerator +import mu.muse.persistence.instrument.jooq.JooqPostgresInstrumentRepository +import mu.muse.persistence.user.jooq.JooqPostgresUserIdGenerator +import mu.muse.persistence.user.jooq.JooqPostgresUserRepository +import org.jooq.DSLContext +import org.jooq.SQLDialect +import org.jooq.impl.DSL import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate import javax.sql.DataSource @Configuration @@ -38,25 +40,25 @@ class PersistenceConfiguration { } @Bean - fun dataSource(hikariConfig: HikariConfig): DataSource = with(HikariDataSource(hikariConfig)) { - maximumPoolSize = MAXIMUM_POOL_SIZE - this - } + fun dataSource(hikariConfig: HikariConfig): DataSource = + with(HikariDataSource(hikariConfig)) { + maximumPoolSize = MAXIMUM_POOL_SIZE + this + } @Bean - fun namedTemplate(dataSource: DataSource) = NamedParameterJdbcTemplate(dataSource) + fun dslContext(dataSource: DataSource): DSLContext = DSL.using(dataSource, SQLDialect.POSTGRES) @Bean - fun userIdGenerator(namedTemplate: NamedParameterJdbcTemplate) = PostgresUserIdGenerator(namedTemplate) + fun userIdGenerator(dslContext: DSLContext) = JooqPostgresUserIdGenerator(dslContext) @Bean - fun userRepository(namedTemplate: NamedParameterJdbcTemplate) = PostgresUserRepository(namedTemplate) + fun userRepository(dslContext: DSLContext) = JooqPostgresUserRepository(dslContext) @Bean - fun instrumentIdGenerator(namedTemplate: NamedParameterJdbcTemplate) = PostgresInstrumentIdGenerator(namedTemplate) + fun instrumentIdGenerator(dslContext: DSLContext) = JooqPostgresInstrumentIdGenerator(dslContext) @Bean - fun instrumentRepository(namedTemplate: NamedParameterJdbcTemplate) = PostgresInstrumentRepository(namedTemplate) - + fun instrumentRepository(dslContext: DSLContext) = JooqPostgresInstrumentRepository(dslContext) } diff --git a/server/app/src/main/kotlin/mu/muse/domain/instrument/Country.kt b/server/app/src/main/kotlin/mu/muse/domain/instrument/Country.kt index 76effe17..3003992e 100644 --- a/server/app/src/main/kotlin/mu/muse/domain/instrument/Country.kt +++ b/server/app/src/main/kotlin/mu/muse/domain/instrument/Country.kt @@ -9,7 +9,10 @@ enum class Country(val i18nCode: String) { USA(i18nCode = "country.usa"); companion object { - fun fromI18nCode(i18nCodeRaw: String): Country { + fun fromI18nCode(i18nCodeRaw: String?): Country { + require(!i18nCodeRaw.isNullOrEmpty()) { + "Country i18n code cannot be null or empty: `${i18nCodeRaw}`" + } return entries.find { it.i18nCode == i18nCodeRaw } ?: throw UnknownCountryI18nCode(i18nCodeRaw) } diff --git a/server/app/src/main/kotlin/mu/muse/domain/instrument/Instrument.kt b/server/app/src/main/kotlin/mu/muse/domain/instrument/Instrument.kt index b81de6a1..e3aa2bd5 100644 --- a/server/app/src/main/kotlin/mu/muse/domain/instrument/Instrument.kt +++ b/server/app/src/main/kotlin/mu/muse/domain/instrument/Instrument.kt @@ -54,7 +54,10 @@ class Instrument internal constructor( WIND(i18nCode = "instrument.type.wind"); companion object { - fun fromI18nCode(i18nCodeRaw: String): Type { + fun fromI18nCode(i18nCodeRaw: String?): Type { + require(!i18nCodeRaw.isNullOrEmpty()) { + "Instrument i18n code cannot be null or empty: `${i18nCodeRaw}`" + } return entries.find { it.i18nCode == i18nCodeRaw } ?: throw UnknownInstrumentI18nCodeType(i18nCodeRaw) } } diff --git a/server/app/src/main/kotlin/mu/muse/domain/instrument/InstrumentBase64Photo.kt b/server/app/src/main/kotlin/mu/muse/domain/instrument/InstrumentBase64Photo.kt index 1fb7a89b..8498bee2 100644 --- a/server/app/src/main/kotlin/mu/muse/domain/instrument/InstrumentBase64Photo.kt +++ b/server/app/src/main/kotlin/mu/muse/domain/instrument/InstrumentBase64Photo.kt @@ -14,9 +14,9 @@ data class InstrumentBase64Photo internal constructor(private val value: String) return InstrumentBase64Photo(String(Base64.getEncoder().encode(value))) } - fun from(value: String): InstrumentBase64Photo { - require(value.isNotEmpty()) - return InstrumentBase64Photo(value) + fun from(valueRaw: String?): InstrumentBase64Photo { + require(!valueRaw.isNullOrEmpty()) + return InstrumentBase64Photo(valueRaw) } } } diff --git a/server/app/src/main/kotlin/mu/muse/domain/instrument/InstrumentId.kt b/server/app/src/main/kotlin/mu/muse/domain/instrument/InstrumentId.kt index efebeccb..5eb9a564 100644 --- a/server/app/src/main/kotlin/mu/muse/domain/instrument/InstrumentId.kt +++ b/server/app/src/main/kotlin/mu/muse/domain/instrument/InstrumentId.kt @@ -8,7 +8,8 @@ data class InstrumentId internal constructor(private val value: Long) { fun toLongValue() = value companion object { - fun from(valueLongRaw: Long): InstrumentId { + fun from(valueLongRaw: Long?): InstrumentId { + require(valueLongRaw != null) { "Instrument ID cannot be empty: `${valueLongRaw}`" } require(valueLongRaw >= 0 && valueLongRaw <= Long.MAX_VALUE) return InstrumentId(valueLongRaw) } diff --git a/server/app/src/main/kotlin/mu/muse/domain/instrument/InstrumentName.kt b/server/app/src/main/kotlin/mu/muse/domain/instrument/InstrumentName.kt index bedfa9a3..0858e06f 100644 --- a/server/app/src/main/kotlin/mu/muse/domain/instrument/InstrumentName.kt +++ b/server/app/src/main/kotlin/mu/muse/domain/instrument/InstrumentName.kt @@ -15,8 +15,9 @@ data class InstrumentName internal constructor(private val value: String) { } companion object { - fun from(value: String): InstrumentName { - return InstrumentName(value) + fun from(valueRaw: String?): InstrumentName { + require(!valueRaw.isNullOrEmpty()) { "Instrument name cannot be null or empty: `${valueRaw}`" } + return InstrumentName(valueRaw) } } } diff --git a/server/app/src/main/kotlin/mu/muse/domain/instrument/Manufacturer.kt b/server/app/src/main/kotlin/mu/muse/domain/instrument/Manufacturer.kt index f494100e..1b18cece 100644 --- a/server/app/src/main/kotlin/mu/muse/domain/instrument/Manufacturer.kt +++ b/server/app/src/main/kotlin/mu/muse/domain/instrument/Manufacturer.kt @@ -11,7 +11,10 @@ class Manufacturer { STEINWAY_AND_SONS(i18nCode = "manufacturer.type.steinway_and_sons"); companion object { - fun fromI18nCode(i18nCodeRaw: String): Type { + fun fromI18nCode(i18nCodeRaw: String?): Type { + require(!i18nCodeRaw.isNullOrEmpty()) { + "Manufacturer i18n code cannot be null or empty: `${i18nCodeRaw}`" + } return entries.find { it.i18nCode == i18nCodeRaw } ?: throw UnknownManufacturerType(i18nCodeRaw) } } diff --git a/server/app/src/main/kotlin/mu/muse/domain/instrument/Material.kt b/server/app/src/main/kotlin/mu/muse/domain/instrument/Material.kt index 92bcd99d..12e85185 100644 --- a/server/app/src/main/kotlin/mu/muse/domain/instrument/Material.kt +++ b/server/app/src/main/kotlin/mu/muse/domain/instrument/Material.kt @@ -12,9 +12,11 @@ class Material { EBONY(i18nCode = "material.type.ebony"); companion object { - fun fromI18nCode(i18nCodeRaw: String): Type { - return entries.find { it.i18nCode == i18nCodeRaw } ?: - throw UnknownMaterialTypeI18nCode(i18nCodeRaw) + fun fromI18nCode(i18nCodeRaw: String?): Type { + require(!i18nCodeRaw.isNullOrEmpty()) { + "Material i18n ocde cannot be null or empty: `${i18nCodeRaw}`" + } + return entries.find { it.i18nCode == i18nCodeRaw } ?: throw UnknownMaterialTypeI18nCode(i18nCodeRaw) } } diff --git a/server/app/src/main/kotlin/mu/muse/domain/user/FullName.kt b/server/app/src/main/kotlin/mu/muse/domain/user/FullName.kt index 5540609a..749319ba 100644 --- a/server/app/src/main/kotlin/mu/muse/domain/user/FullName.kt +++ b/server/app/src/main/kotlin/mu/muse/domain/user/FullName.kt @@ -8,9 +8,10 @@ data class FullName(private val value: String) { fun toStringValue() = value companion object { - fun from(fullName: String): FullName { - require(fullName.isNotEmpty()) - return FullName(fullName) + fun from(value: String?): FullName { + require(!value.isNullOrBlank()) { "Full name `${value}` cannot be null or empty" } + require(value.isNotEmpty()) + return FullName(value) } } } diff --git a/server/app/src/main/kotlin/mu/muse/domain/user/Role.kt b/server/app/src/main/kotlin/mu/muse/domain/user/Role.kt index d7860e69..ebdc036e 100644 --- a/server/app/src/main/kotlin/mu/muse/domain/user/Role.kt +++ b/server/app/src/main/kotlin/mu/muse/domain/user/Role.kt @@ -7,7 +7,8 @@ import mu.muse.common.annotations.ValueObject data class Role internal constructor(private val value: String) { companion object { - fun from(value: String): Role { + fun from(value: String?): Role { + require(!value.isNullOrBlank()) { "Role `${value}` cannot be null or empty" } return when (value) { USER -> Role(USER) EDITOR -> Role(EDITOR) diff --git a/server/app/src/main/kotlin/mu/muse/domain/user/UserId.kt b/server/app/src/main/kotlin/mu/muse/domain/user/UserId.kt index 5bccd94f..67cea30d 100644 --- a/server/app/src/main/kotlin/mu/muse/domain/user/UserId.kt +++ b/server/app/src/main/kotlin/mu/muse/domain/user/UserId.kt @@ -5,7 +5,8 @@ class UserId internal constructor(private val value: Long) { fun toLongValue() = value companion object { - fun from(value: Long): UserId { + fun from(value: Long?): UserId { + require(value != null) { "User ID cannot be null: `${value}`" } require(value >= 1L && value <= Long.MAX_VALUE) return UserId(value) } diff --git a/server/app/src/main/kotlin/mu/muse/persistence/instrument/postgres/PostgresInstrumentIdGenerator.kt b/server/app/src/main/kotlin/mu/muse/persistence/instrument/jdbc/JdbcPostgresInstrumentIdGenerator.kt similarity index 86% rename from server/app/src/main/kotlin/mu/muse/persistence/instrument/postgres/PostgresInstrumentIdGenerator.kt rename to server/app/src/main/kotlin/mu/muse/persistence/instrument/jdbc/JdbcPostgresInstrumentIdGenerator.kt index 155cc075..c57962a7 100644 --- a/server/app/src/main/kotlin/mu/muse/persistence/instrument/postgres/PostgresInstrumentIdGenerator.kt +++ b/server/app/src/main/kotlin/mu/muse/persistence/instrument/jdbc/JdbcPostgresInstrumentIdGenerator.kt @@ -1,10 +1,10 @@ -package mu.muse.persistence.instrument.postgres +package mu.muse.persistence.instrument.jdbc import mu.muse.domain.IdGenerator import mu.muse.domain.instrument.InstrumentId import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate -class PostgresInstrumentIdGenerator( +class JdbcPostgresInstrumentIdGenerator( private val namedParameter: NamedParameterJdbcTemplate, ) : IdGenerator { diff --git a/server/app/src/main/kotlin/mu/muse/persistence/instrument/postgres/PostgresInstrumentRepository.kt b/server/app/src/main/kotlin/mu/muse/persistence/instrument/jdbc/JdbcPostgresInstrumentRepository.kt similarity index 99% rename from server/app/src/main/kotlin/mu/muse/persistence/instrument/postgres/PostgresInstrumentRepository.kt rename to server/app/src/main/kotlin/mu/muse/persistence/instrument/jdbc/JdbcPostgresInstrumentRepository.kt index e18d6e70..5485cb4c 100644 --- a/server/app/src/main/kotlin/mu/muse/persistence/instrument/postgres/PostgresInstrumentRepository.kt +++ b/server/app/src/main/kotlin/mu/muse/persistence/instrument/jdbc/JdbcPostgresInstrumentRepository.kt @@ -1,4 +1,4 @@ -package mu.muse.persistence.instrument.postgres +package mu.muse.persistence.instrument.jdbc import mu.muse.common.types.Version import mu.muse.domain.instrument.Country diff --git a/server/app/src/main/kotlin/mu/muse/persistence/instrument/jooq/JooqPostgresInstrumentIdGenerator.kt b/server/app/src/main/kotlin/mu/muse/persistence/instrument/jooq/JooqPostgresInstrumentIdGenerator.kt new file mode 100644 index 00000000..174269a2 --- /dev/null +++ b/server/app/src/main/kotlin/mu/muse/persistence/instrument/jooq/JooqPostgresInstrumentIdGenerator.kt @@ -0,0 +1,16 @@ +package mu.muse.persistence.instrument.jooq + +import mu.muse.codegen.jooq.public.sequences.INSTRUMENT_ID_SEQ +import mu.muse.domain.IdGenerator +import mu.muse.domain.instrument.InstrumentId +import org.jooq.DSLContext + +class JooqPostgresInstrumentIdGenerator( + private val dslContext: DSLContext, +) : IdGenerator { + + override fun generate(): InstrumentId { + val instrumentIdRaw = dslContext.nextval(INSTRUMENT_ID_SEQ) + return InstrumentId.from(instrumentIdRaw) + } +} diff --git a/server/app/src/main/kotlin/mu/muse/persistence/instrument/jooq/JooqPostgresInstrumentRepository.kt b/server/app/src/main/kotlin/mu/muse/persistence/instrument/jooq/JooqPostgresInstrumentRepository.kt new file mode 100644 index 00000000..9c48bcd6 --- /dev/null +++ b/server/app/src/main/kotlin/mu/muse/persistence/instrument/jooq/JooqPostgresInstrumentRepository.kt @@ -0,0 +1,128 @@ +package mu.muse.persistence.instrument.jooq + +import mu.muse.codegen.jooq.public.tables.Instruments.Companion.INSTRUMENTS +import mu.muse.codegen.jooq.public.tables.pojos.Instruments +import mu.muse.common.types.Version +import mu.muse.domain.instrument.Country +import mu.muse.domain.instrument.Instrument +import mu.muse.domain.instrument.InstrumentBase64Photo +import mu.muse.domain.instrument.InstrumentId +import mu.muse.domain.instrument.InstrumentName +import mu.muse.domain.instrument.Manufacturer +import mu.muse.domain.instrument.ManufacturerDate +import mu.muse.domain.instrument.Material +import mu.muse.domain.instrument.ReleaseDate +import mu.muse.persistence.instrument.inmemory.matches +import mu.muse.usecase.access.instrument.InstrumentExtractor +import mu.muse.usecase.access.instrument.InstrumentPersister +import mu.muse.usecase.access.instrument.InstrumentRemover +import org.jooq.DSLContext +import org.jooq.impl.DSL.selectCount +import java.time.OffsetDateTime +import java.time.ZoneOffset + +class JooqPostgresInstrumentRepository( + private val dslContext: DSLContext, +) : InstrumentExtractor, InstrumentRemover, InstrumentPersister { + + override fun findAll(): List { + val instrumentRecords = dslContext + .selectFrom(INSTRUMENTS) + .fetchInto(Instruments::class.java) + return instrumentRecords.map { it.toInstrument() } + } + + override fun findById(id: InstrumentId): Instrument? { + val instrumentRecordResult = runCatching { + dslContext + .selectFrom(INSTRUMENTS) + .fetchSingleInto(Instruments::class.java) + } + + val instrumentRecordRaw = instrumentRecordResult.getOrNull() ?: return null + return instrumentRecordRaw.toInstrument() + } + + override fun findByIds(ids: List): List { + if (ids.isEmpty()) { + return emptyList() + } + val instrumentRecords = dslContext + .selectFrom(INSTRUMENTS) + .where(INSTRUMENTS.INSTRUMENT_ID.`in`(ids.map { it.toLongValue() })) + .fetchInto(Instruments::class.java) + return instrumentRecords.map { it.toInstrument() } + } + + + override fun findByCriteria(criteria: InstrumentExtractor.Criteria): List { + val instrumentRecords = dslContext + .selectFrom(INSTRUMENTS) + .fetchInto(Instruments::class.java) + val instruments = instrumentRecords.map { it.toInstrument() } + return instruments.filter { it matches criteria } + } + + override fun totalElements(): Long { + return dslContext.fetchValue(selectCount().from(INSTRUMENTS)).toLong() + } + + override fun removeInstrument(id: InstrumentId) { + dslContext + .deleteFrom(INSTRUMENTS) + .where(INSTRUMENTS.INSTRUMENT_ID.eq(id.toLongValue())) + .execute() + } + + override fun save(instrument: Instrument) { + dslContext.insertInto( + INSTRUMENTS, + INSTRUMENTS.INSTRUMENT_ID, + INSTRUMENTS.INSTRUMENT_NAME, + INSTRUMENTS.INSTRUMENT_I18N_CODE, + INSTRUMENTS.MANUFACTURER_I18N_CODE, + INSTRUMENTS.MANUFACTURER_DATE, + INSTRUMENTS.RELEASE_DATE, + INSTRUMENTS.COUNTRY_I18N_CODE, + INSTRUMENTS.MATERIALS, + INSTRUMENTS.IMAGE, + ) + .values( + instrument.id.toLongValue(), + instrument.name.toStringValue(), + instrument.type.i18nCode, + instrument.manufacturerType.i18nCode, + OffsetDateTime.ofInstant(instrument.manufactureDate.toInstantValue(), ZoneOffset.UTC), + OffsetDateTime.ofInstant(instrument.releaseDate.toInstantValue(), ZoneOffset.UTC), + instrument.country.i18nCode, + instrument.materialTypes.map { it.i18nCode }.toTypedArray(), + instrument.image.toStringValue(), + ) + .onConflict(INSTRUMENTS.INSTRUMENT_ID) + .doUpdate() + .set(INSTRUMENTS.INSTRUMENT_NAME, instrument.name.toStringValue()) + .set(INSTRUMENTS.INSTRUMENT_I18N_CODE, instrument.type.i18nCode) + .set(INSTRUMENTS.MANUFACTURER_I18N_CODE, instrument.manufacturerType.i18nCode) + .set(INSTRUMENTS.MANUFACTURER_DATE, OffsetDateTime.ofInstant(instrument.manufactureDate.toInstantValue(), ZoneOffset.UTC)) + .set(INSTRUMENTS.RELEASE_DATE, OffsetDateTime.ofInstant(instrument.releaseDate.toInstantValue(), ZoneOffset.UTC)) + .set(INSTRUMENTS.COUNTRY_I18N_CODE, instrument.country.i18nCode) + .set(INSTRUMENTS.MATERIALS, instrument.materialTypes.map { it.i18nCode }.toTypedArray()) + .set(INSTRUMENTS.IMAGE, instrument.image.toStringValue()) + .execute() + } +} + + +fun Array.toBasicMaterials() = this.toList().map { Material.Type.fromI18nCode(it) } +fun Instruments.toInstrument() = Instrument( + id = InstrumentId.from(this.instrumentId), + name = InstrumentName.from(this.instrumentName), + type = Instrument.Type.fromI18nCode(this.instrumentI18nCode), + manufacturerType = Manufacturer.Type.fromI18nCode(this.manufacturerI18nCode), + manufactureDate = ManufacturerDate.from(this.manufacturerDate!!.toInstant()), + releaseDate = ReleaseDate.from(this.releaseDate!!.toInstant()), + country = Country.fromI18nCode(this.countryI18nCode), + materialTypes = this.materials!!.toBasicMaterials(), + image = InstrumentBase64Photo.from(this.image), + version = Version.new(), +) diff --git a/server/app/src/main/kotlin/mu/muse/persistence/user/postgres/PostgresUserIdGenerator.kt b/server/app/src/main/kotlin/mu/muse/persistence/user/jdbc/JdbcPostgresUserIdGenerator.kt similarity index 86% rename from server/app/src/main/kotlin/mu/muse/persistence/user/postgres/PostgresUserIdGenerator.kt rename to server/app/src/main/kotlin/mu/muse/persistence/user/jdbc/JdbcPostgresUserIdGenerator.kt index de340eec..3d3b029c 100644 --- a/server/app/src/main/kotlin/mu/muse/persistence/user/postgres/PostgresUserIdGenerator.kt +++ b/server/app/src/main/kotlin/mu/muse/persistence/user/jdbc/JdbcPostgresUserIdGenerator.kt @@ -1,10 +1,10 @@ -package mu.muse.persistence.user.postgres +package mu.muse.persistence.user.jdbc import mu.muse.domain.IdGenerator import mu.muse.domain.user.UserId import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate -class PostgresUserIdGenerator( +class JdbcPostgresUserIdGenerator( private val namedTemplate: NamedParameterJdbcTemplate, ) : IdGenerator { diff --git a/server/app/src/main/kotlin/mu/muse/persistence/user/postgres/PostgresUserRepository.kt b/server/app/src/main/kotlin/mu/muse/persistence/user/jdbc/JdbcPostgresUserRepository.kt similarity index 95% rename from server/app/src/main/kotlin/mu/muse/persistence/user/postgres/PostgresUserRepository.kt rename to server/app/src/main/kotlin/mu/muse/persistence/user/jdbc/JdbcPostgresUserRepository.kt index bf4493cc..554fe3ad 100644 --- a/server/app/src/main/kotlin/mu/muse/persistence/user/postgres/PostgresUserRepository.kt +++ b/server/app/src/main/kotlin/mu/muse/persistence/user/jdbc/JdbcPostgresUserRepository.kt @@ -1,4 +1,4 @@ -package mu.muse.persistence.user.postgres +package mu.muse.persistence.user.jdbc import mu.muse.common.types.Version import mu.muse.domain.instrument.InstrumentId @@ -15,15 +15,16 @@ import org.springframework.dao.EmptyResultDataAccessException import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate import java.sql.ResultSet -class PostgresUserRepository( +class JdbcPostgresUserRepository( private val namedTemplate: NamedParameterJdbcTemplate, ) : UserExtractor, UserPersister { companion object { - val logger = LoggerFactory.getLogger(PostgresUserRepository::class.java) + val logger = LoggerFactory.getLogger(JdbcPostgresUserRepository::class.java) } @Suppress("SwallowedException") override fun findByUsername(username: Username): User? { + val sql = """ select user_id, username, password, role, full_name, favorite_ids diff --git a/server/app/src/main/kotlin/mu/muse/persistence/user/jooq/JooqPostgresUserIdGenerator.kt b/server/app/src/main/kotlin/mu/muse/persistence/user/jooq/JooqPostgresUserIdGenerator.kt new file mode 100644 index 00000000..d7e708ed --- /dev/null +++ b/server/app/src/main/kotlin/mu/muse/persistence/user/jooq/JooqPostgresUserIdGenerator.kt @@ -0,0 +1,17 @@ +package mu.muse.persistence.user.jooq + +import mu.muse.codegen.jooq.public.sequences.USER_ID_SEQ +import mu.muse.domain.IdGenerator +import mu.muse.domain.user.UserId +import org.jooq.DSLContext + +class JooqPostgresUserIdGenerator( + private val dslContext: DSLContext, +) : IdGenerator { + + override fun generate(): UserId { + val userIdRaw = dslContext.nextval(USER_ID_SEQ) + return UserId.from(userIdRaw) + } + +} diff --git a/server/app/src/main/kotlin/mu/muse/persistence/user/jooq/JooqPostgresUserRepository.kt b/server/app/src/main/kotlin/mu/muse/persistence/user/jooq/JooqPostgresUserRepository.kt new file mode 100644 index 00000000..85ccc357 --- /dev/null +++ b/server/app/src/main/kotlin/mu/muse/persistence/user/jooq/JooqPostgresUserRepository.kt @@ -0,0 +1,69 @@ +package mu.muse.persistence.user.jooq + +import mu.muse.codegen.jooq.public.tables.Users.Companion.USERS +import mu.muse.codegen.jooq.public.tables.pojos.Users +import mu.muse.domain.instrument.InstrumentId +import mu.muse.domain.user.FullName +import mu.muse.domain.user.Password +import mu.muse.domain.user.Role +import mu.muse.domain.user.User +import mu.muse.domain.user.UserId +import mu.muse.domain.user.Username +import mu.muse.usecase.access.user.UserExtractor +import mu.muse.usecase.access.user.UserPersister +import org.jooq.DSLContext + +class JooqPostgresUserRepository( + private val dslContext: DSLContext, +) : UserExtractor, UserPersister { + override fun findByUsername(username: Username): User? { + val userRecordResult = runCatching { + dslContext.selectFrom(USERS) + .where(USERS.USERNAME.eq(username.toStringValue())) + .fetchSingleInto(Users::class.java) + } + val userRecordRaw = userRecordResult.getOrNull() ?: return null + return userRecordRaw.toUser() + } + + override fun findAll(): Collection { + val users = dslContext.selectFrom(USERS).fetchInto(Users::class.java) + return users.map { it.toUser() } + } + + override fun save(user: User) { + dslContext.insertInto( + USERS, + USERS.USER_ID, + USERS.USERNAME, + USERS.PASSWORD, + USERS.ROLE, + USERS.FULL_NAME, + USERS.FAVORITE_IDS, + ) + .values( + user.id.toLongValue(), + user.username.toStringValue(), + user.password.toPlainStringValue(), + user.role.toStringValue(), + user.fullName.toStringValue(), + user.favoriteIds.map { it.toLongValue() }.toTypedArray(), + ) + .onConflictDoNothing() + .execute() + } +} + +fun Array?.toFavoriteIds(): MutableList { + require(this != null) { "Favorite IDS `${this}` cannot be null" } + return this.map { InstrumentId.from(it) }.toMutableList() +} + +fun Users.toUser(): User = User.create( + id = UserId.from(this.userId), + username = Username.from(this.username), + password = Password.from(this.password), + role = Role.from(this.role), + fullName = FullName.from(this.fullName), + favoriteIds = this.favoriteIds.toFavoriteIds(), +) diff --git a/server/app/src/main/resources/db/schema.sql b/server/app/src/main/resources/db/schema.sql index 1413aedf..e7f2c6ec 100644 --- a/server/app/src/main/resources/db/schema.sql +++ b/server/app/src/main/resources/db/schema.sql @@ -1,10 +1,11 @@ -drop table if exists users; -drop sequence if exists user_id_seq; +drop table if exists public.users; +drop sequence if exists public.user_id_seq; -drop table if exists instruments; -drop sequence if exists instrument_id_seq; +drop table if exists public.instruments; +drop sequence if exists public.instrument_id_seq; -create table users( +create sequence public.user_id_seq increment 1 start 1; +create table public.users( user_id bigint unique not null, username varchar(256) not null unique, password varchar(256) not null, @@ -13,9 +14,8 @@ create table users( favorite_ids bigint[] not null ); -create sequence user_id_seq increment 1 start 1; - -create table instruments( +create sequence public.instrument_id_seq increment 1 start 1; +create table public.instruments( instrument_id bigint unique not null, instrument_name varchar(256) not null, instrument_i18n_code varchar(256) not null, @@ -26,5 +26,3 @@ create table instruments( materials varchar(256)[] not null, image text not null ); - -create sequence instrument_id_seq increment 1 start 1; diff --git a/server/app/src/main/resources/logback.xml b/server/app/src/main/resources/logback.xml index f595f7b8..e4884c4a 100644 --- a/server/app/src/main/resources/logback.xml +++ b/server/app/src/main/resources/logback.xml @@ -13,4 +13,6 @@ + + diff --git a/tools/scripts/deploy.sh b/tools/scripts/deploy.sh index 3dbd8019..9fac85d0 100755 --- a/tools/scripts/deploy.sh +++ b/tools/scripts/deploy.sh @@ -44,6 +44,7 @@ fi # do not regenerate OpenAPI due to it is already committed to repo #(cd "$rootDir" && exec ./tools/scripts/openapi/regenerateOpenApi.sh) +(cd "$rootDir" && exec ./tools/scripts/server/generateJooq.sh) (cd "$rootDir" && exec ./tools/scripts/buildAndPush.sh "$stage" "$dockerRepository") (cd "$rootDir" && exec ./tools/scripts/stop.sh "$stage" "$dockerRepository") (cd "$rootDir" && exec ./tools/scripts/clean.sh "$stage" "$dockerRepository") diff --git a/tools/scripts/openapi/regenerateOpenApi.sh b/tools/scripts/openapi/regenerateOpenApi.sh index 7607bced..d6a378ba 100755 --- a/tools/scripts/openapi/regenerateOpenApi.sh +++ b/tools/scripts/openapi/regenerateOpenApi.sh @@ -22,11 +22,6 @@ echo -e "\033[0;37m[$stage] Regenerating OpenAPI specs...\033[0m" --additional-properties=apiPackage=api,modelPackage=model,supportsES6=true,withSeparateModelsAndApi=true ) -# mkdir local && -# mkdir -p local/client/src/generated/model && -# mkdir -p /local/client/src/generated/model && - -(cd "$rootDir/server" && ./gradlew clean) (cd "$rootDir/server" && ./gradlew openApiGenerate) (cd "$rootDir" && ./tools/scripts/client/runLinter.sh) diff --git a/tools/scripts/server/generateJooq.sh b/tools/scripts/server/generateJooq.sh new file mode 100755 index 00000000..fdc14e22 --- /dev/null +++ b/tools/scripts/server/generateJooq.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e +currentDir=$(cd -P -- "$(dirname -- "$0")" && pwd -P) +rootDir="$currentDir/../../../" + +(cd "$rootDir/server" && ./gradlew generateJooq)