45 Commits

Author SHA1 Message Date
03350db56f change ac.assert { can*() } to as.can*().assert() 2022-03-03 19:46:49 +01:00
f380231e1e Multiple minors fix and improve
use data class for entity
Add sealed on entity interfaces
Replace listOf() by setOf() instead of deduplicate
use interface instead of EntityRef
replace .toLowerCase() to .lowercase()
fix test.sh
2022-03-03 19:42:18 +01:00
1c013e3e15 lint 2022-02-25 23:48:33 +01:00
003aa9ff05 update gradle plugin 2022-02-25 23:48:33 +01:00
44c5f1b15b Merge pull request #104 from flecomte/sibling
Rename findVersionsByVersionId to findSiblingVersions
2022-02-25 23:45:45 +01:00
2f3a2f9b8e Rename findVersionsByVersionId to findSiblingVersions 2022-02-25 23:17:03 +01:00
1a62c5ec9a Merge pull request #103 from flecomte/update_gradle2
gradle dependencies fix
2022-02-25 22:55:12 +01:00
55674cbef1 upgrade jacoco 2022-02-25 22:44:38 +01:00
ac5dbbc384 update gradle to 7.4 2022-02-25 22:44:26 +01:00
9460992f37 update gradle to 7.2 2022-02-25 22:43:49 +01:00
8013cfb266 gradle dependencies fix 2022-02-25 22:42:32 +01:00
f577ea70b3 Merge pull request #96 from flecomte/60
#60 Can follow citizen
2021-04-27 21:48:16 +02:00
2673cf527a Clean useless error 2021-04-27 21:30:06 +02:00
371483ccde Improve query findFollowsByTarget & add tests 2021-04-27 18:52:36 +02:00
76e4033a22 deduplicate follow output 2021-04-18 02:36:14 +02:00
fee5e5784b Refactoring of Notification system 2021-04-18 02:16:36 +02:00
1c33c026f0 #60 Can follow citizen 2021-04-17 01:37:32 +02:00
4871e7d780 Merge pull request #95 from flecomte/61
#61 Fix version date returned for the article.createdAt
2021-04-16 21:44:03 +02:00
359450ad8f #61 Fix version date returned for the article.createdAt 2021-04-16 21:23:43 +02:00
11903a4cda Merge pull request #94 from flecomte/62
#62 if not connected, you not must view the articles draft
2021-04-16 18:30:15 +02:00
1a8b544cdb #62 if not connected, you not must view the articles draft 2021-04-16 18:15:22 +02:00
e2c1f15ab8 Merge pull request #93 from flecomte/dependencies
Use dynamic dependencies
2021-04-16 17:25:52 +02:00
dc87c95bb4 Enable zip64 for jar 2021-04-16 16:59:13 +02:00
d4cc3f21da fix CI 2021-04-16 16:19:04 +02:00
59c050d14d Update dependencies
Fix for koin 3.0
2021-04-16 16:09:59 +02:00
cb9bd0c14b Use gradle Lock file 2021-04-16 16:09:58 +02:00
1ce3a18d8e Update gradle plugins versions 2021-04-16 16:09:58 +02:00
be6fb8dc12 Update gradle to 7.0 2021-04-16 16:09:58 +02:00
cd9fd569d7 launch CI on master 2021-04-16 03:40:39 +02:00
2920186352 Merge pull request #91 from flecomte/21-valid-input
Valider les resource entrente
2021-04-16 03:27:10 +02:00
cccabb2cc9 improve sonarqube action 2021-04-16 03:17:38 +02:00
620cd73fec Change intellij tasks 2021-04-16 03:09:56 +02:00
e474a40068 Change tests task and CI 2021-04-16 03:09:55 +02:00
242bf9c9b3 Fix commentSqlTest 2021-04-16 02:53:05 +02:00
543f3fb9bb Add retry for viewsTest 2021-04-16 02:53:04 +02:00
994e266b52 Add validation on route CreateWorkgroup 2021-04-16 00:08:57 +02:00
3f392eece6 Add validation on route EditWorkgroup 2021-04-15 23:45:47 +02:00
518b59e9aa Add validation on route GetWorkgroups 2021-04-15 22:20:16 +02:00
596b7ff0c9 Extract Workgroup Member test into external file 2021-04-15 20:52:34 +02:00
87175eb8ea clean route comment 2021-04-15 02:53:32 +02:00
1118866856 Add validation on route PutVoteOnConstitution 2021-04-15 02:42:29 +02:00
367f59ee18 Add validation on route PutVoteOnComment 2021-04-15 02:33:46 +02:00
0588f88f9a Add validation on route PutVoteOnArticle 2021-04-15 02:20:57 +02:00
496cf50d88 Add validation on route GetCitizenVotesOnArticle 2021-04-15 01:27:48 +02:00
13253e4af1 Add validation on route GetMyOpinionsArticle 2021-04-15 01:06:46 +02:00
159 changed files with 2660 additions and 974 deletions

View File

@@ -4,6 +4,9 @@
name: Tests
on:
push:
branches:
- 'master'
pull_request:
branches:
- 'master'
@@ -18,6 +21,10 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 11
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
- name: Cache Gradle packages
uses: actions/cache@v2
@@ -29,26 +36,17 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Build
uses: eskatos/gradle-command-action@v1
uses: gradle/gradle-build-action@v2
with:
gradle-version: 6.8
gradle-version: '7.4'
arguments: build -x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck -x detekt
- name: Cleanup Gradle Cache
# Remove some files from the Gradle cache, so they aren't cached by GitHub Actions.
# Restoring these files from a GitHub Actions cache might cause problems for future builds.
run: |
rm -f ~/.gradle/caches/modules-2/modules-2.lock
rm -f ~/.gradle/caches/modules-2/gc.properties
- name: processResources
uses: eskatos/gradle-command-action@v1
with:
gradle-version: 6.8
arguments: processResources
run: gradle processResources
- name: processTestResources
uses: eskatos/gradle-command-action@v1
uses: gradle/gradle-build-action@v2
with:
gradle-version: 6.8
gradle-version: '7.4'
arguments: processResources
- uses: actions/upload-artifact@v2
with:
@@ -69,10 +67,17 @@ jobs:
with:
name: Build
path: build
- name: TestSql
uses: eskatos/gradle-command-action@v1
- name: Composer Up
uses: gradle/gradle-build-action@v2
with:
gradle-version: 6.8
gradle-version: '7.4'
arguments: testSqlComposeUp
- name: TestSql
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: testSql
test:
@@ -81,37 +86,58 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/download-artifact@v2
with:
name: Build
path: build
- name: Composer Up
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: testComposeUp
- name: Test
uses: eskatos/gradle-command-action@v1
uses: gradle/gradle-build-action@v2
with:
gradle-version: 6.8
arguments: test -x testSql
gradle-version: '7.4'
arguments: test
- name: Coverage
uses: eskatos/gradle-command-action@v1
uses: gradle/gradle-build-action@v2
with:
gradle-version: 6.8
gradle-version: '7.4'
arguments: coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
- name: Cache SonarCloud packages
uses: actions/cache@v1
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Test
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: test
- name: Build and analyze
uses: gradle/gradle-build-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: ./gradlew build sonarqube --info
with:
gradle-version: '7.4'
arguments: sonarqube --info
lint:
needs: build
@@ -128,7 +154,7 @@ jobs:
name: Build
path: build
- name: Lint
uses: eskatos/gradle-command-action@v1
uses: gradle/gradle-build-action@v2
with:
gradle-version: 6.8
gradle-version: '7.4'
arguments: ktlintCheck

View File

@@ -8,7 +8,7 @@
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<SqlCodeStyleSettings version="5">
<SqlCodeStyleSettings version="6">
<option name="KEYWORD_CASE" value="1" />
<option name="IDENTIFIER_CASE" value="1" />
<option name="TYPE_CASE" value="4" />
@@ -56,21 +56,13 @@
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="5" />
<option name="CALL_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="CALL_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_WRAP" value="5" />
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>

View File

@@ -25,12 +25,9 @@
<option value="openapi" />
<option value="rabbitmq" />
<option value="redis" />
<option value="sonarqube" />
</list>
</option>
<option name="sourceFilePath" value="docker-compose.yml" />
<option name="upExitCodeFromService" value="" />
<option name="upTimeout" value="" />
</settings>
</deployment>
<method v="2" />

View File

@@ -4,7 +4,7 @@
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="-x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck" />
<option name="scriptParameters" value="-x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck -x detekt" />
<option name="taskDescriptions">
<list />
</option>

View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Create lock dependencies" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--write-locks" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="dependencies" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -24,13 +24,9 @@
<option value="rabbitmq" />
<option value="redis" />
<option value="openapi" />
<option value="sonarqube" />
<option value="sonarqube_db" />
</list>
</option>
<option name="sourceFilePath" value="docker-compose.yml" />
<option name="upExitCodeFromService" value="" />
<option name="upTimeout" value="" />
</settings>
</deployment>
<method v="2" />

View File

@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Sonarqube without test" type="GradleRunConfiguration" factoryName="Gradle">
<configuration default="false" name="Sonarqube (Send without run test)" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />

View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test With Dependencies" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="testWithDependencies" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Update Dependency" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--update-locks *:*" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="classes" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -9,12 +9,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.owasp.dependencycheck.reporting.ReportGenerator
import org.slf4j.LoggerFactory
val ktorVersion = "1.5.0"
val kotlinVersion = "1.4.30"
val coroutinesVersion = "1.4.3"
val ktorVersion = "1.5.4"
val kotlinVersion = "1.5.31"
val coroutinesVersion = "1.5.2"
val logbackVersion = "1.2.3"
val koinVersion = "2.0.1"
val jacksonVersion = "2.12.1"
val koinVersion = "3.1.5"
val jacksonVersion = "2.13.1"
group = "com.github.flecomte"
version = versioning.info.run {
@@ -28,20 +28,24 @@ version = versioning.info.run {
plugins {
jacoco
application
maven
`maven-publish`
id("maven-publish")
kotlin("jvm") version "1.4.30"
kotlin("plugin.serialization") version "1.4.30"
kotlin("jvm") version "1.5.31"
kotlin("plugin.serialization") version "1.5.31"
id("com.github.johnrengelman.shadow") version "5.2.0"
id("org.jlleitschuh.gradle.ktlint") version "9.4.1"
id("org.owasp.dependencycheck") version "6.1.1"
id("org.sonarqube") version "3.1.1"
id("net.nemerosa.versioning") version "2.14.0"
id("io.gitlab.arturbosch.detekt") version "1.16.0-RC1"
id("com.avast.gradle.docker-compose") version "0.14.0"
id("com.github.kt3k.coveralls") version "2.8.4"
id("com.github.johnrengelman.shadow") version "7.1.2"
id("org.jlleitschuh.gradle.ktlint") version "10.2.1"
id("org.owasp.dependencycheck") version "6.1.5"
id("org.sonarqube") version "3.3"
id("net.nemerosa.versioning") version "2.15.1"
id("io.gitlab.arturbosch.detekt") version "1.19.0"
id("com.avast.gradle.docker-compose") version "0.15.1"
id("com.github.kt3k.coveralls") version "2.12.0"
}
dependencyLocking {
lockAllConfigurations()
// lockMode.set(LockMode.STRICT)
}
application {
@@ -56,7 +60,7 @@ buildscript {
maven { url = uri("https://jitpack.io") }
}
dependencies {
classpath("com.typesafe:config:1.4.1")
classpath("com.typesafe:config:1.4.2")
classpath("com.github.flecomte:postgres-json:2.1.2")
}
}
@@ -94,7 +98,7 @@ val migration by tasks.registering {
}
val migrationTest by tasks.registering {
group = "verification"
group = "tests"
dependsOn(tasks.named("testComposeUp"))
finalizedBy(tasks.named("testComposeDown"))
doLast {
@@ -118,11 +122,9 @@ val migrationTest by tasks.registering {
}
val testSql by tasks.registering {
group = "verification"
group = "tests"
dependsOn(tasks.named("processResources"))
dependsOn(tasks.named("processTestResources"))
dependsOn(tasks.named("testSqlComposeUp"))
finalizedBy(tasks.named("testSqlComposeDown"))
doLast {
val config = ConfigFactory.parseFile(file("$buildDir/resources/test/application-test.conf")).resolve()
@@ -167,6 +169,7 @@ tasks.withType<Jar> {
)
)
}
isZip64 = true
}
tasks.withType<KotlinCompile> {
@@ -180,11 +183,10 @@ tasks.withType<KotlinCompile> {
tasks.named<ShadowJar>("shadowJar") {
mergeServiceFiles("META-INF/services")
archiveFileName.set("${archiveBaseName.get()}-latest-all.${archiveExtension.get()}")
isZip64 = true
}
tasks.sonarqube.configure {
dependsOn(tasks.test)
dependsOn(tasks.detekt)
dependsOn(tasks.jacocoTestReport)
}
@@ -197,8 +199,7 @@ val sourcesJar by tasks.registering(Jar::class) {
tasks.test {
useJUnit()
useJUnitPlatform()
// systemProperty("junit.jupiter.execution.parallel.enabled", true)
dependsOn(testSql)
systemProperty("junit.jupiter.execution.parallel.enabled", true)
finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run
}
@@ -206,13 +207,6 @@ coveralls {
sourceDirs.add("src/main/kotlin")
}
tasks.register("testAll") {
group = "verification"
dependsOn(testSql)
dependsOn(tasks.test)
dependsOn(tasks.ktlintCheck)
}
apply(plugin = "docker-compose")
dockerCompose {
projectName = "dc-project"
@@ -228,14 +222,12 @@ dockerCompose {
useComposeFiles = listOf("docker-compose-test.yml")
startedServices = listOf("db", "elasticsearch")
stopContainers = false
isRequiredBy(project.tasks.named("testSql"))
}
createNested("test").apply {
projectName = "dc-project_test"
useComposeFiles = listOf("docker-compose-test.yml")
stopContainers = false
isRequiredBy(project.tasks.test)
}
}
@@ -266,7 +258,7 @@ publishing {
}
jacoco {
toolVersion = "0.8.6"
toolVersion = "0.8.7"
applyTo(tasks.run.get())
}
@@ -320,6 +312,14 @@ tasks.named("testComposeUp").configure {
}
}
tasks.register("testWithDependencies", Test::class) {
group = "tests"
dependsOn(tasks.named("testComposeUp"))
dependsOn(tasks.ktlintCheck)
dependsOn(testSql)
dependsOn(tasks.jacocoTestReport)
finalizedBy(tasks.sonarqube) // report is always generated after tests run
}
tasks.register("testArticles", Test::class) {
group = "tests"
useJUnitPlatform {
@@ -356,6 +356,30 @@ tasks.register("testNotifications", Test::class) {
includeTags("notification")
}
}
tasks.register("testOpinions", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("opinion")
}
}
tasks.register("testVotes", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("vote")
}
}
tasks.register("testWorkgroups", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("workgroup")
}
}
tasks.register("testViews", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("view")
}
}
dependencyCheck {
formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML)
@@ -364,9 +388,7 @@ dependencyCheck {
repositories {
mavenLocal()
jcenter()
maven("https://kotlin.bintray.com/ktor")
maven("https://jitpack.io")
maven("https://dl.bintray.com/konform-kt/konform")
maven { url = uri("https://jitpack.io") }
}
dependencies {
@@ -383,7 +405,7 @@ dependencies {
implementation("io.ktor:ktor-auth:$ktorVersion")
implementation("io.ktor:ktor-auth-jwt:$ktorVersion")
implementation("io.ktor:ktor-websockets:$ktorVersion")
implementation("org.koin:koin-ktor:$koinVersion")
implementation("io.insert-koin:koin-ktor:$koinVersion")
implementation("io.ktor:ktor-jackson:$ktorVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:$jacksonVersion")
@@ -397,18 +419,18 @@ dependencies {
implementation("org.elasticsearch.client:elasticsearch-rest-client:6.7.1")
implementation("com.jayway.jsonpath:json-path:2.5.0")
implementation("com.avast.gradle:gradle-docker-compose-plugin:0.14.0")
implementation("io.konform:konform-jvm:0.2.0")
implementation("io.konform:konform:0.3.0")
testImplementation("io.ktor:ktor-server-tests:$ktorVersion")
testImplementation("io.ktor:ktor-client-mock:$ktorVersion")
testImplementation("io.ktor:ktor-client-mock-jvm:$ktorVersion")
testImplementation("org.koin:koin-test:$koinVersion")
testImplementation("io.insert-koin:koin-test:$koinVersion")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
testImplementation("io.mockk:mockk:1.10.6")
testImplementation("org.junit.jupiter:junit-jupiter:5.7.0")
testImplementation("org.amshove.kluent:kluent:1.61")
testImplementation("io.mockk:mockk-agent-api:1.10.6")
testImplementation("io.mockk:mockk-agent-jvm:1.10.6")
testImplementation("org.junit.jupiter:junit-jupiter:5.8.2")
testImplementation("org.amshove.kluent:kluent:1.68")
testImplementation("io.mockk:mockk:1.12.2")
testImplementation("io.mockk:mockk-agent-api:1.12.2")
testImplementation("io.mockk:mockk-agent-jvm:1.12.2")
testImplementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
testImplementation("com.thedeanda:lorem:2.1")
testImplementation("org.openapi4j:openapi-operation-validator:1.0.6")

View File

@@ -1,4 +1,4 @@
version: '3.8'
version: '3.3'
services:
rabbitmq:
container_name: ${APP_NAME}_rabbitmq_test

275
gradle.lockfile Normal file
View File

@@ -0,0 +1,275 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
ch.qos.logback:logback-classic:1.2.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
ch.qos.logback:logback-core:1.2.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.auth0:java-jwt:3.12.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.auth0:jwks-rsa:0.9.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.avast.gradle:gradle-docker-compose-plugin:0.14.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.beust:jcommander:1.81=detekt
com.fasterxml.jackson.core:jackson-annotations:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-core:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-databind:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson.datatype:jackson-datatype-joda:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson:jackson-bom:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.github.flecomte:postgres-json:2.1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.github.jasync-sql:jasync-common:1.1.6=compileClasspath,implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata
com.github.jasync-sql:jasync-common:1.1.7=runtimeClasspath,testRuntimeClasspath
com.github.jasync-sql:jasync-pool:1.1.6=compileClasspath,implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata
com.github.jasync-sql:jasync-pool:1.1.7=runtimeClasspath,testRuntimeClasspath
com.github.jasync-sql:jasync-postgresql:1.1.6=compileClasspath,implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata
com.github.jasync-sql:jasync-postgresql:1.1.7=runtimeClasspath,testRuntimeClasspath
com.github.shyiko.klob:klob:0.2.1=ktlint
com.google.code.findbugs:jsr305:3.0.2=runtimeClasspath,testRuntimeClasspath
com.google.errorprone:error_prone_annotations:2.2.0=runtimeClasspath,testRuntimeClasspath
com.google.guava:failureaccess:1.0.1=runtimeClasspath,testRuntimeClasspath
com.google.guava:guava:27.1-jre=runtimeClasspath,testRuntimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=runtimeClasspath,testRuntimeClasspath
com.google.j2objc:j2objc-annotations:1.1=runtimeClasspath,testRuntimeClasspath
com.googlecode.json-simple:json-simple:1.1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.jayway.jsonpath:json-path:2.5.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.ongres.scram:client:2.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.ongres.scram:common:2.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.ongres.stringprep:saslprep:1.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.ongres.stringprep:stringprep:1.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.pinterest.ktlint:ktlint-core:0.42.1=ktlint,ktlintBaselineReporter
com.pinterest.ktlint:ktlint-reporter-baseline:0.42.1=ktlint,ktlintBaselineReporter
com.pinterest.ktlint:ktlint-reporter-checkstyle:0.42.1=ktlint
com.pinterest.ktlint:ktlint-reporter-html:0.42.1=ktlint
com.pinterest.ktlint:ktlint-reporter-json:0.42.1=ktlint
com.pinterest.ktlint:ktlint-reporter-plain:0.42.1=ktlint
com.pinterest.ktlint:ktlint-reporter-sarif:0.42.1=ktlint
com.pinterest.ktlint:ktlint-ruleset-experimental:0.42.1=ktlint
com.pinterest.ktlint:ktlint-ruleset-standard:0.42.1=ktlint
com.pinterest.ktlint:ktlint-ruleset-test:0.42.1=ktlint
com.pinterest:ktlint:0.42.1=ktlint
com.rabbitmq:amqp-client:5.10.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.sendgrid:java-http-client:4.3.6=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.sendgrid:sendgrid-java:4.7.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.thedeanda:lorem:2.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.typesafe:config:1.3.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
commons-codec:commons-codec:1.11=compileClasspath,implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata
commons-codec:commons-codec:1.14=runtimeClasspath,testRuntimeClasspath
commons-io:commons-io:2.6=runtimeClasspath,testRuntimeClasspath
commons-logging:commons-logging:1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
info.picocli:picocli:3.9.6=ktlint
io.github.detekt.sarif4k:sarif4k:0.0.1=detekt,ktlint
io.github.microutils:kotlin-logging:1.7.6=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.gitlab.arturbosch.detekt:detekt-api:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-cli:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-core:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-metrics:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-parser:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-psi-utils:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-report-html:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-report-sarif:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-report-txt:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-report-xml:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-complexity:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-coroutines:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-documentation:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-empty:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-errorprone:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-exceptions:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-naming:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-performance:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-style:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-tooling:1.19.0=detekt
io.insert-koin:koin-core-jvm:3.1.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.insert-koin:koin-core:3.1.5=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.insert-koin:koin-ktor:3.1.5=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.insert-koin:koin-test-jvm:3.1.5=testCompileClasspath,testRuntimeClasspath
io.insert-koin:koin-test:3.1.5=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.konform:konform-jvm:0.3.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.konform:konform:0.3.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-auth-jwt-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testImplementationDependenciesMetadata
io.ktor:ktor-auth-jwt:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-auth-kotlinMultiplatform:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-auth:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-client-cio-jvm:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-client-cio:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-client-core-jvm:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-client-core:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-client-jetty-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-client-jetty:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-client-mock-jvm:1.5.4=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-client-mock:1.5.4=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-http-cio-jvm:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-http-cio:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-http-jvm:1.6.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-http:1.6.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-io-jvm:1.6.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-io:1.6.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-jackson-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testImplementationDependenciesMetadata
io.ktor:ktor-jackson:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-locations-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testImplementationDependenciesMetadata
io.ktor:ktor-locations:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-network-jvm:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-network-tls-certificates-kotlinMultiplatform:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-network-tls-certificates:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-network-tls-jvm:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-network-tls:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-network:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-core-kotlinMultiplatform:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-server-core:1.6.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-server-host-common-kotlinMultiplatform:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-host-common:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-jetty-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testImplementationDependenciesMetadata
io.ktor:ktor-server-jetty:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-server-servlet-kotlinMultiplatform:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-servlet:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-test-host-kotlinMultiplatform:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-test-host:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-tests-kotlinMultiplatform:1.5.4=testImplementationDependenciesMetadata
io.ktor:ktor-server-tests:1.5.4=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-utils-jvm:1.6.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-utils:1.6.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-websockets-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-websockets:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.lettuce:lettuce-core:5.3.6.RELEASE=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-agent-api:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-agent-common:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-agent-jvm:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-common:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-dsl-jvm:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-dsl:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-buffer:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-codec:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-common:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-handler:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-resolver:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-transport:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.projectreactor:reactor-core:3.4.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
javax.servlet:javax.servlet-api:3.1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
joda-time:joda-time:2.10.8=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
junit:junit:4.12=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.12.5=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.12.5=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.java.dev.jna:jna-platform:5.5.0=testRuntimeClasspath
net.java.dev.jna:jna:5.5.0=testRuntimeClasspath
net.minidev:accessors-smart:1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.minidev:json-smart:2.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.pearx.kasechange:kasechange-jvm:1.3.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.pearx.kasechange:kasechange-metadata:1.3.0=implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.amshove.kluent:kluent-common:1.68=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.amshove.kluent:kluent:1.68=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apache.httpcomponents:httpasyncclient:4.1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apache.httpcomponents:httpclient:4.5.12=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apache.httpcomponents:httpcore-nio:4.4.5=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apache.httpcomponents:httpcore:4.4.13=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata
org.bouncycastle:bcprov-jdk15on:1.67=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.checkerframework:checker-qual:2.5.2=runtimeClasspath,testRuntimeClasspath
org.codehaus.mojo:animal-sniffer-annotations:1.17=runtimeClasspath,testRuntimeClasspath
org.ec4j.core:ec4j-core:0.3.0=ktlint,ktlintBaselineReporter
org.eclipse.jetty.http2:http2-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http2:http2-common:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http2:http2-hpack:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http2:http2-http-client-transport:9.4.31.v20200723=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http2:http2-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-java-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-java-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-openjdk8-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-openjdk8-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-client:9.4.31.v20200723=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-continuation:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-http:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-io:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-servlets:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-util:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.elasticsearch.client:elasticsearch-rest-client:6.7.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.fusesource.jansi:jansi:2.3.4=runtimeClasspath,testRuntimeClasspath
org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath
org.jacoco:org.jacoco.agent:0.8.7=jacocoAgent,jacocoAnt
org.jacoco:org.jacoco.ant:0.8.7=jacocoAnt
org.jacoco:org.jacoco.core:0.8.7=jacocoAnt
org.jacoco:org.jacoco.report:0.8.7=jacocoAnt
org.jetbrains.intellij.deps:trove4j:1.0.20181211=detekt,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.5.31=detekt,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.5.31=detekt,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.5.31=kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-native-utils:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-project-model:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-reflect:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-reflect:1.5.30=compileClasspath,implementationDependenciesMetadata,runtimeClasspath
org.jetbrains.kotlin:kotlin-reflect:1.5.31=detekt,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-script-runtime:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-script-runtime:1.5.31=detekt,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-scripting-common:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-jvm:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-serialization:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-stdlib-common:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31=compileClasspath,detekt,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-stdlib:1.5.31=compileClasspath,detekt,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-test-annotations-common:1.5.31=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-test-common:1.5.31=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-test-junit:1.5.31=testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-test:1.5.31=testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-util-io:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.2=implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core-metadata:1.4.2=implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.5.2=testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.5.1-native-mt=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.5.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.5.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.3=detekt
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.1.0=detekt,ktlint
org.jetbrains.kotlinx:kotlinx-serialization-core-metadata:1.0.1=implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-core:1.1.0=detekt,ktlint
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.1.0=detekt,ktlint
org.jetbrains.kotlinx:kotlinx-serialization-json-metadata:1.0.1=implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0=detekt,ktlint
org.jetbrains:annotations:13.0=compileClasspath,detekt,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,ktlint,ktlintBaselineReporter,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.joda:joda-convert:1.8.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:5.8.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.8.2=testRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:5.8.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.jupiter:junit-jupiter:5.8.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.8.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.8.2=testRuntimeClasspath
org.junit:junit-bom:5.8.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.objenesis:objenesis:3.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.openapi4j:openapi-core:1.0.6=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.openapi4j:openapi-operation-validator:1.0.6=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.openapi4j:openapi-parser:1.0.6=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.openapi4j:openapi-schema-validator:1.0.6=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.ow2.asm:asm-analysis:9.1=jacocoAnt
org.ow2.asm:asm-commons:9.1=jacocoAnt
org.ow2.asm:asm-tree:9.1=jacocoAnt
org.ow2.asm:asm:5.0.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.ow2.asm:asm:9.1=jacocoAnt
org.reactivestreams:reactive-streams:1.0.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.slf4j:slf4j-api:1.7.30=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.yaml:snakeyaml:1.27=runtimeClasspath
org.yaml:snakeyaml:1.28=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.yaml:snakeyaml:1.29=detekt
empty=annotationProcessor,apiDependenciesMetadata,compileOnly,compileOnlyDependenciesMetadata,detektPlugins,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,ktlintReporter,ktlintRuleset,runtimeOnlyDependenciesMetadata,shadow,testAnnotationProcessor,testApiDependenciesMetadata,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntimeOnlyDependenciesMetadata

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -9,6 +9,7 @@ import com.fasterxml.jackson.datatype.joda.JodaModule
import fr.dcproject.application.Env.PROD
import fr.dcproject.application.Env.TEST
import fr.dcproject.application.http.statusPagesInstallation
import fr.dcproject.common.utils.onApplicationStopped
import fr.dcproject.component.article.articleKoinModule
import fr.dcproject.component.article.routes.installArticleRoutes
import fr.dcproject.component.auth.authKoinModule
@@ -25,8 +26,10 @@ import fr.dcproject.component.constitution.routes.installConstitutionRoutes
import fr.dcproject.component.doc.routes.installDocRoutes
import fr.dcproject.component.follow.followKoinModule
import fr.dcproject.component.follow.routes.article.installFollowArticleRoutes
import fr.dcproject.component.follow.routes.citizen.installFollowCitizenRoutes
import fr.dcproject.component.follow.routes.constitution.installFollowConstitutionRoutes
import fr.dcproject.component.notification.NotificationConsumer
import fr.dcproject.component.notification.email.NotificationEmailConsumer
import fr.dcproject.component.notification.push.NotificationPushConsumer
import fr.dcproject.component.notification.routes.installNotificationsRoutes
import fr.dcproject.component.opinion.opinionKoinModule
import fr.dcproject.component.opinion.routes.installOpinionRoutes
@@ -37,7 +40,6 @@ import fr.dcproject.component.workgroup.routes.installWorkgroupRoutes
import fr.dcproject.component.workgroup.workgroupKoinModule
import fr.postgresjson.migration.Migrations
import io.ktor.application.Application
import io.ktor.application.ApplicationStopped
import io.ktor.application.install
import io.ktor.auth.Authentication
import io.ktor.client.HttpClient
@@ -57,7 +59,6 @@ import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Locations
import io.ktor.routing.Routing
import io.ktor.server.jetty.EngineMain
import io.ktor.util.KtorExperimentalAPI
import io.ktor.websocket.WebSockets
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.eclipse.jetty.util.log.Slf4jLog
@@ -72,7 +73,6 @@ fun main(args: Array<String>): Unit = EngineMain.main(args)
enum class Env { PROD, TEST }
@ExperimentalCoroutinesApi
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI
@Suppress("unused") // Referenced in application.conf
fun Application.module(env: Env = PROD) {
@@ -117,11 +117,14 @@ fun Application.module(env: Env = PROD) {
masking = false
}
get<NotificationConsumer>().run {
get<NotificationEmailConsumer>().run {
start()
environment.monitor.subscribe(ApplicationStopped) {
close()
}
onApplicationStopped { close() }
}
get<NotificationPushConsumer>().run {
start()
onApplicationStopped { close() }
}
install(Authentication, jwtInstallation(get(), get()))
@@ -154,6 +157,7 @@ fun Application.module(env: Env = PROD) {
installCommentRoutes()
installFollowArticleRoutes()
installFollowConstitutionRoutes()
installFollowCitizenRoutes()
installWorkgroupRoutes()
installOpinionRoutes()
installVoteRoutes()

View File

@@ -5,20 +5,10 @@ import fr.dcproject.application.http.HttpErrorBadRequest
import fr.dcproject.application.http.HttpErrorBadRequest.InvalidParam
import io.ktor.features.DataConversion
import io.ktor.http.HttpStatusCode
import io.ktor.util.KtorExperimentalAPI
import org.koin.core.context.GlobalContext
import org.koin.core.parameter.ParametersDefinition
import org.koin.core.qualifier.Qualifier
import java.util.UUID
private typealias ConverterDeclaration = DataConversion.Configuration.() -> Unit
private inline fun <reified T> DataConversion.Configuration.get(
qualifier: Qualifier? = null,
noinline parameters: ParametersDefinition? = null
): T = GlobalContext.get().koin.rootScope.get(qualifier, parameters)
@KtorExperimentalAPI
val converters: ConverterDeclaration = {
convert<UUID> {
decode { values, _ ->

View File

@@ -10,21 +10,20 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.common.email.Mailer
import fr.dcproject.component.auth.jwt.JwtConfig
import fr.dcproject.component.notification.NotificationConsumer
import fr.dcproject.component.notification.NotificationEmailSender
import fr.dcproject.component.notification.NotificationsPush
import fr.dcproject.component.notification.Publisher
import fr.dcproject.component.notification.NotificationPublisherAsync
import fr.dcproject.component.notification.email.NotificationEmailConsumer
import fr.dcproject.component.notification.email.NotificationEmailSender
import fr.dcproject.component.notification.push.NotificationPushConsumer
import fr.dcproject.component.notification.push.NotificationPushListener
import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations
import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.WebSockets
import io.ktor.util.KtorExperimentalAPI
import io.lettuce.core.RedisClient
import org.koin.core.qualifier.named
import org.koin.dsl.module
@KtorExperimentalAPI
val KoinModule = module {
// JWT
single {
@@ -65,11 +64,15 @@ val KoinModule = module {
}
}
single { NotificationsPush.Builder(get()) }
single { NotificationPushListener.Builder(get()) }
single {
val config: Configuration = get()
NotificationConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
NotificationEmailConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
}
single {
val config: Configuration = get()
NotificationPushConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
}
// RabbitMQ
@@ -114,7 +117,7 @@ val KoinModule = module {
single {
val config: Configuration = get()
Publisher(factory = get(), exchangeName = config.exchangeNotificationName)
NotificationPublisherAsync(factory = get(), exchangeName = config.exchangeNotificationName)
}
single {

View File

@@ -9,6 +9,9 @@ import java.io.IOException
class Mailer(
private val key: String
) {
/**
* Send email via Sendgrid
*/
fun sendEmail(action: () -> Mail): Boolean {
val mail = action()

View File

@@ -2,6 +2,7 @@ package fr.dcproject.common.entity
import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.dcproject.component.opinion.database.OpinionRef
@@ -34,7 +35,8 @@ interface TargetI : EntityI {
Article("article"),
Constitution("constitution"),
Comment("comment"),
Opinion("opinion")
Opinion("opinion"),
Citizen("citizen"),
}
companion object {
@@ -44,6 +46,7 @@ interface TargetI : EntityI {
t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference
t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference
t.isSubclassOf(OpinionRef::class) -> TargetName.Opinion.targetReference
t.isSubclassOf(CitizenRef::class) -> TargetName.Citizen.targetReference
else -> throw error("target not implemented: ${t.qualifiedName} \nImplement it or return 'reference' from SQL")
}
}

View File

@@ -0,0 +1,10 @@
package fr.dcproject.common.utils
import io.ktor.application.Application
import io.ktor.application.ApplicationStopped
fun Application.onApplicationStopped(callback: Application.() -> Unit) {
environment.monitor.subscribe(ApplicationStopped) {
callback()
}
}

View File

@@ -0,0 +1,30 @@
package fr.dcproject.common.utils
import com.rabbitmq.client.AMQP
import com.rabbitmq.client.Channel
import com.rabbitmq.client.Consumer
import com.rabbitmq.client.DefaultConsumer
import com.rabbitmq.client.Envelope
import kotlinx.coroutines.runBlocking
import java.io.IOException
fun Channel.consumeQueue(queueName: String, callback: DefaultConsumer.(ByteArray) -> Unit) {
val consumer: Consumer = object : DefaultConsumer(this) {
@Throws(IOException::class)
override fun handleDelivery(
consumerTag: String,
envelope: Envelope,
properties: AMQP.BasicProperties,
body: ByteArray
) = runBlocking {
try {
callback(body)
basicAck(envelope.deliveryTag, false)
} catch (e: Throwable) {
basicNack(envelope.deliveryTag, false, true)
}
}
}
/* Launch Consumer */
basicConsume(queueName, false, consumer)
}

View File

@@ -0,0 +1,41 @@
package fr.dcproject.common.utils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
@ExperimentalTime
fun <T> retry(numOfRetries: Int, duration: Duration = Duration.ZERO, block: (RetryContext) -> T): T {
val logger: Logger = LoggerFactory.getLogger("fr.dcproject.utils.retry")
var throwable: Throwable? = null
for (attempt in 1..numOfRetries) {
val context = RetryContext()
try {
val output = block(context)
if (context.hasStop()) {
break
}
return output
} catch (e: Throwable) {
throwable = e
logger.debug("Failed attempt $attempt / $numOfRetries. Wait ${duration.inSeconds} seconds")
Thread.sleep(duration.inMilliseconds.toLong())
} finally {
if (context.hasStop()) {
break
}
}
}
throw throwable!!
}
class RetryContext() {
var stoped = false
fun stop() {
stoped = true
}
fun hasStop(): Boolean = stoped
}

View File

@@ -0,0 +1,15 @@
package fr.dcproject.common.validation
import io.konform.validation.ValidationBuilder
import java.net.MalformedURLException
import java.net.URL
fun ValidationBuilder<String>.isUrl() =
addConstraint("is not url") {
try {
URL(it)
true
} catch (e: MalformedURLException) {
false
}
}

View File

@@ -37,7 +37,7 @@ class ArticleAccessControl(private val articleRepo: ArticleRepository) : AccessC
if (subject.createdBy.id == citizen.id) {
/* The creator must be the same of the creator of preview version of article */
val lastVersionId = articleRepo
.findVersionsByVersionId(1, 1, subject.versionId)
.findSiblingVersions(1, 1, subject)
.result
.firstOrNull()?.createdBy?.id

View File

@@ -48,7 +48,7 @@ data class ArticleForView(
val lastVersion: Boolean = false
}
interface ArticleForUpdateI<C : CitizenRef> : ArticleI, ArticleWithTitleI, VersionableId, TargetI, CreatedBy<C> {
sealed interface ArticleForUpdateI<C : CitizenRef> : ArticleI, ArticleWithTitleI, VersionableId, TargetI, CreatedBy<C> {
val anonymous: Boolean
val content: String
val description: String
@@ -56,13 +56,13 @@ interface ArticleForUpdateI<C : CitizenRef> : ArticleI, ArticleWithTitleI, Versi
val workgroup: WorkgroupRef?
}
class ArticleForUpdate(
data class ArticleForUpdate(
override val id: UUID = UUID.randomUUID(),
override val title: String,
override val anonymous: Boolean = true,
override val content: String,
override val description: String,
tags: List<String> = emptyList(),
val tags: Set<String> = emptySet(),
override val draft: Boolean = false,
override val createdBy: CitizenRef,
override val workgroup: WorkgroupRef? = null,
@@ -71,12 +71,10 @@ class ArticleForUpdate(
) : ArticleRef(id),
ArticleForUpdateI<CitizenRef>,
ArticleAuthI<CitizenRef>,
VersionableId {
val tags: List<String> = tags.distinct()
}
VersionableId
class ArticleForListing(
id: UUID? = null,
data class ArticleForListing(
override val id: UUID = UUID.randomUUID(),
override val title: String,
override val createdBy: CitizenCreator,
override val workgroup: WorkgroupCart? = null,
@@ -87,9 +85,10 @@ class ArticleForListing(
ArticleRef(id),
ArticleAuthI<CitizenCartI>,
Votable by VotableImp(),
CreatedAt by CreatedAt.Imp(),
CreatedBy<CitizenCartI>
interface ArticleForListingI : ArticleWithTitleI, CreatedBy<CitizenCartI> {
sealed interface ArticleForListingI : ArticleWithTitleI, CreatedBy<CitizenCartI> {
val workgroup: WorkgroupCartI?
}
@@ -97,13 +96,13 @@ open class ArticleRef(
id: UUID? = null
) : ArticleI, TargetRef(id)
interface ArticleI : EntityI, TargetI
sealed interface ArticleI : EntityI, TargetI
interface ArticleWithTitleI : ArticleI {
sealed interface ArticleWithTitleI : ArticleI {
val title: String
}
interface ArticleAuthI<U : CitizenI> :
sealed interface ArticleAuthI<U : CitizenI> :
ArticleI,
CreatedBy<U>,
DeletedAt {

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.article.database
import fr.dcproject.common.entity.VersionableId
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.Parameter
@@ -19,10 +20,10 @@ class ArticleRepository(override var requester: Requester) : RepositoryI {
.select(page, limit, "id" to id)
}
fun findVersionsByVersionId(page: Int = 1, limit: Int = 50, versionId: UUID): Paginated<ArticleForListing> {
fun <A> findSiblingVersions(page: Int = 1, limit: Int = 50, article: A): Paginated<ArticleForListing> where A : VersionableId, A : ArticleI {
return requester
.getFunction("find_articles_versions_by_version_id")
.select(page, limit, "version_id" to versionId)
.select(page, limit, "version_id" to article.versionId)
}
fun find(

View File

@@ -65,7 +65,7 @@ object FindArticleVersions {
it.validate().badRequestIfNotValid()
repo.findVersions(it)
.apply { ac.assert { canView(result, citizenOrNull) } }
.apply { ac.canView(result, citizenOrNull).assert() }
.run {
call.respond(
toOutput { a: ArticleForListing ->

View File

@@ -76,13 +76,14 @@ object FindArticles {
it.validate().badRequestIfNotValid()
repo.findArticles(it)
.apply { ac.assert { canView(result, citizenOrNull) } }
.apply { ac.canView(result, citizenOrNull).assert() }
.let {
call.respond(
it.toOutput {
object {
val id = it.id
val title = it.title
val createdAt = it.createdAt
val createdBy: Any = it.createdBy.toOutput()
val workgroup = it.workgroup?.let {
object {

View File

@@ -27,7 +27,7 @@ object GetOneArticle {
fun Route.getOneArticle(viewRepository: ArticleViewRepository<ArticleForView>, ac: ArticleAccessControl, repo: ArticleRepository) {
get<ArticleRequest> {
val article: ArticleForView = repo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found")
ac.assert { canView(article, citizenOrNull) }
ac.canView(article, citizenOrNull).assert()
call.respond(
article.let { a ->

View File

@@ -10,8 +10,8 @@ import fr.dcproject.component.article.routes.UpsertArticle.UpsertArticleRequest.
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.notification.ArticleUpdateNotification
import fr.dcproject.component.notification.Publisher
import fr.dcproject.component.notification.ArticleUpdateNotificationMessage
import fr.dcproject.component.notification.NotificationPublisherAsync
import fr.dcproject.component.workgroup.database.WorkgroupRef
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxItems
@@ -37,7 +37,7 @@ object UpsertArticle {
val anonymous: Boolean = true,
val content: String,
val description: String,
val tags: List<String> = emptyList(),
val tags: Set<String> = emptySet(),
val draft: Boolean = false,
val versionId: UUID,
val workgroup: WorkgroupRef? = null,
@@ -63,7 +63,7 @@ object UpsertArticle {
}
}
fun Route.upsertArticle(repo: ArticleRepository, publisher: Publisher, ac: ArticleAccessControl) {
fun Route.upsertArticle(repo: ArticleRepository, notificationPublisher: NotificationPublisherAsync, ac: ArticleAccessControl) {
suspend fun ApplicationCall.convertRequestToEntity(): ArticleForUpdate = receiveOrBadRequest<Input>().run {
validate().badRequestIfNotValid()
ArticleForUpdate(
@@ -83,7 +83,7 @@ object UpsertArticle {
post<UpsertArticleRequest> {
mustBeAuth()
val article = call.convertRequestToEntity()
ac.assert { canUpsert(article, citizenOrNull) }
ac.canUpsert(article, citizenOrNull).assert()
repo.upsert(article)?.let { a ->
call.respond(
object {
@@ -92,7 +92,7 @@ object UpsertArticle {
val versionNumber = a.versionNumber
}
)
publisher.publish(ArticleUpdateNotification(a))
notificationPublisher.publishAsync(ArticleUpdateNotificationMessage(a))
} ?: error("Article not updated")
}
}

View File

@@ -17,13 +17,13 @@ private val citizenAttributeKey = AttributeKey<CitizenEntity>("CitizenContext")
val ApplicationCall.citizen: CitizenEntity
get() = attributes.computeIfAbsent(citizenAttributeKey) {
val user = authentication.principal<UserI>() ?: throw ForbiddenException("No User Connected")
GlobalContext.get().koin.get<CitizenRepository>().findByUser(user)
GlobalContext.get().get<CitizenRepository>().findByUser(user)
?: throw ForbiddenException("Citizen not found for this user id \"${user.id}\"")
}
val ApplicationCall.citizenOrNull: CitizenEntity?
get() = authentication.principal<UserI>()?.let {
GlobalContext.get().koin.get<CitizenRepository>().findByUser(it)
GlobalContext.get().get<CitizenRepository>().findByUser(it)
}
val ApplicationCall.isAuth: Boolean

View File

@@ -9,35 +9,45 @@ import io.ktor.auth.Principal
import org.joda.time.DateTime
import java.util.UUID
class UserForCreate(
id: UUID = UUID.randomUUID(),
username: String,
data class UserForCreate(
override val id: UUID = UUID.randomUUID(),
override val username: String,
override val password: String,
blockedAt: DateTime? = null,
roles: List<Roles> = emptyList()
) : User(id, username, blockedAt, roles),
UserWithPasswordI
override val blockedAt: DateTime? = null,
override val roles: Set<Roles> = emptySet()
) : UserForViewI,
UserWithPasswordI,
CreatedAt by CreatedAt.Imp(),
UpdatedAt by UpdatedAt.Imp()
open class User(
id: UUID = UUID.randomUUID(),
override var username: String,
var blockedAt: DateTime? = null,
var roles: List<Roles> = emptyList()
data class User(
override val id: UUID = UUID.randomUUID(),
override val username: String,
override val blockedAt: DateTime? = null,
override val roles: Set<Roles> = emptySet()
) : UserRef(id),
UserForViewI,
UserWithUsername,
CreatedAt by CreatedAt.Imp(),
UpdatedAt by UpdatedAt.Imp()
sealed interface UserForViewI :
UserI,
UserWithUsername,
UserForAuthI,
CreatedAt,
UpdatedAt
class UserCreator(
id: UUID = UUID.randomUUID(),
override val username: String,
) : UserRef(id), UserWithUsername
interface UserWithUsername : UserI {
sealed interface UserWithUsername : UserI {
val username: String
}
interface UserWithPasswordI : UserI {
sealed interface UserWithPasswordI : UserI {
val password: String
}
@@ -51,11 +61,11 @@ open class UserRef(
id: UUID = UUID.randomUUID()
) : UserI, Entity(id)
interface UserI : EntityI, Principal {
sealed interface UserI : EntityI, Principal {
enum class Roles { ROLE_USER, ROLE_ADMIN }
}
interface UserForAuthI : UserI {
var roles: List<Roles>
var blockedAt: DateTime?
sealed interface UserForAuthI : UserI {
val roles: Set<Roles>
val blockedAt: DateTime?
}

View File

@@ -7,7 +7,7 @@ import org.koin.core.context.GlobalContext
/**
* Produce a token for this combination of User and Account
*/
fun UserI.makeToken(): String = GlobalContext.get().koin.get<JwtConfig>().run {
fun UserI.makeToken(): String = GlobalContext.get().get<JwtConfig>().run {
JWT.create()
.withSubject("Authentication")
.withIssuer(issuer)

View File

@@ -91,7 +91,7 @@ object Register {
user = UserForCreate(
username = user.username,
password = user.password,
roles = listOf(UserI.Roles.ROLE_USER)
roles = setOf(UserI.Roles.ROLE_USER)
)
)

View File

@@ -2,8 +2,9 @@ package fr.dcproject.component.citizen.database
import fr.dcproject.common.entity.CreatedAt
import fr.dcproject.common.entity.DeletedAt
import fr.dcproject.common.entity.Entity
import fr.dcproject.common.entity.EntityI
import fr.dcproject.common.entity.TargetI
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.auth.database.User
import fr.dcproject.component.auth.database.UserCreator
import fr.dcproject.component.auth.database.UserForCreate
@@ -16,19 +17,19 @@ import fr.postgresjson.entity.Serializable
import org.joda.time.DateTime
import java.util.UUID
class CitizenForCreate(
data class CitizenForCreate(
val name: Name,
val email: String,
val birthday: DateTime,
val voteAnonymous: Boolean = true,
val followAnonymous: Boolean = true,
override val user: UserForCreate,
id: UUID = UUID.randomUUID(),
override val id: UUID = UUID.randomUUID(),
) : CitizenI,
CitizenRefWithUser(id, user),
CitizenWithUserI by CitizenRefWithUser(id, user),
CreatedAt by CreatedAt.Imp()
class Citizen(
data class Citizen(
override val id: UUID = UUID.randomUUID(),
override val name: Name,
override val email: String,
@@ -36,7 +37,7 @@ class Citizen(
override val voteAnonymous: Boolean = true,
override val followAnonymous: Boolean = true,
override val user: User,
deletedAt: DateTime? = null
override val deletedAt: DateTime? = null
) : CitizenWithEmail,
CitizenCreatorI,
CitizenWithUserI,
@@ -61,10 +62,11 @@ data class CitizenCreator(
override val user: UserCreator,
override val deletedAt: DateTime? = null
) : CitizenCreatorI,
CitizenRefWithUser(id, user),
CitizenI,
CitizenWithUserI by CitizenRefWithUser(id, user),
DeletedAt by DeletedAt.Imp(deletedAt)
interface CitizenCreatorI : CitizenWithUserI, CitizenWithEmail, CitizenCartI, DeletedAt {
sealed interface CitizenCreatorI : CitizenWithUserI, CitizenWithEmail, CitizenCartI, DeletedAt {
override val id: UUID
override val name: Name
override val email: String
@@ -74,8 +76,8 @@ interface CitizenCreatorI : CitizenWithUserI, CitizenWithEmail, CitizenCartI, De
override val deletedAt: DateTime?
}
class CitizenCart(
id: UUID = UUID.randomUUID(),
data class CitizenCart(
override val id: UUID = UUID.randomUUID(),
override val name: Name,
override val user: UserRef,
override val deletedAt: DateTime? = null,
@@ -83,22 +85,22 @@ class CitizenCart(
CitizenCartI,
DeletedAt by DeletedAt.Imp(deletedAt)
interface CitizenCartI : CitizenI, CitizenWithUserI {
sealed interface CitizenCartI : CitizenI, CitizenWithUserI {
val name: Name
}
open class CitizenRefWithUser(
id: UUID = UUID.randomUUID(),
override val user: UserRef
data class CitizenRefWithUser(
override val id: UUID = UUID.randomUUID(),
override val user: UserI
) : CitizenWithUserI,
CitizenRef(id)
open class CitizenRef(
id: UUID = UUID.randomUUID()
) : Entity(id),
) : TargetRef(id),
CitizenI
interface CitizenI : EntityI {
sealed interface CitizenI : EntityI, TargetI {
data class Name(
override val firstName: String,
override val lastName: String,
@@ -113,10 +115,10 @@ interface CitizenI : EntityI {
}
}
interface CitizenWithUserI : CitizenI {
sealed interface CitizenWithUserI : CitizenI {
val user: UserI
}
interface CitizenWithEmail : CitizenI {
sealed interface CitizenWithEmail : CitizenI {
val email: String
}

View File

@@ -42,7 +42,7 @@ object ChangeMyPassword {
mustBeAuth()
val content = call.receiveOrBadRequest<ChangePasswordCitizenRequest.Input>()
.apply { validate().badRequestIfNotValid() }
ac.assert { canChangePassword(it.citizen, citizenOrNull) }
ac.canChangePassword(it.citizen, citizenOrNull).assert()
userRepository.findByCredentials(UserPasswordCredential(citizen.user.username, content.oldPassword)) ?: throw BadRequestException("Bad Password")
userRepository.changePassword(
UserWithPassword(

View File

@@ -55,7 +55,7 @@ object FindCitizens {
mustBeAuth()
it.validate().badRequestIfNotValid()
val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
ac.assert { canView(citizens.result, citizenOrNull) }
ac.canView(citizens.result, citizenOrNull).assert()
call.respond(
citizens.toOutput { c: CitizenCreator ->
object {

View File

@@ -28,7 +28,7 @@ object GetCurrentCitizen {
if (currentUser === null) {
call.respond(HttpStatusCode.Unauthorized)
} else {
ac.assert { canView(currentUser, citizenOrNull) }
ac.canView(currentUser, citizenOrNull).assert()
call.respond(
object {
val id: UUID = citizen.id

View File

@@ -29,7 +29,7 @@ object GetOneCitizen {
get<CitizenRequest> {
mustBeAuth()
val citizen = citizenRepository.findById(it.citizen.id) ?: throw NotFoundException("Citizen not found ${it.citizen.id}")
ac.assert { canView(citizen, citizenOrNull) }
ac.canView(citizen, citizenOrNull).assert()
call.respond(
object {

View File

@@ -51,7 +51,7 @@ object CreateCommentArticle {
content = content
)
}.let { comment ->
ac.assert { canCreate(comment, citizenOrNull) }
ac.canCreate(comment, citizenOrNull).assert()
repo.comment(comment)
call.respond(

View File

@@ -58,7 +58,7 @@ object GetArticleComments {
val comments = repo.findByTarget(it.article, it.page, it.limit, it.sort)
if (comments.result.isNotEmpty()) {
ac.assert { canView(comments.result, citizenOrNull) }
ac.canView(comments.result, citizenOrNull).assert()
}
call.respond(
HttpStatusCode.OK,

View File

@@ -28,7 +28,7 @@ object GetCitizenArticleComments {
get<CitizenCommentArticleRequest> {
mustBeAuth()
repo.findByCitizen(it.citizen).let { comments ->
ac.assert { canView(comments.result, citizenOrNull) }
ac.canView(comments.result, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
comments.toOutput { comment ->

View File

@@ -53,7 +53,7 @@ object CreateConstitutionComment {
content = content
)
}.let { comment ->
ac.assert { canCreate(comment, citizenOrNull) }
ac.canCreate(comment, citizenOrNull).assert()
repo.comment(comment)
call.respond(

View File

@@ -28,7 +28,7 @@ object GetCitizenCommentConstitution {
get<GetCitizenCommentConstitutionRequest> {
mustBeAuth()
val comments = repo.findByCitizen(it.citizen)
ac.assert { canView(comments.result, citizenOrNull) }
ac.canView(comments.result, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
comments.toOutput { comment ->

View File

@@ -57,7 +57,7 @@ object GetConstitutionComment {
it.validate().badRequestIfNotValid()
val comments = repo.findByTarget(it.constitution)
ac.assert { canView(comments.result, citizenOrNull) }
ac.canView(comments.result, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
comments.toOutput { comment ->

View File

@@ -16,8 +16,8 @@ import fr.dcproject.component.vote.entity.VotableImp
import org.joda.time.DateTime
import java.util.UUID
class CommentForView<T : TargetI, C : CitizenCreatorI>(
id: UUID = UUID.randomUUID(),
data class CommentForView<T : TargetI, C : CitizenCreatorI>(
override val id: UUID = UUID.randomUUID(),
override val createdBy: C,
override val target: T,
override var content: String,
@@ -30,7 +30,7 @@ class CommentForView<T : TargetI, C : CitizenCreatorI>(
CommentWithTargetI<T>,
CreatedBy<C> by CreatedBy.Imp(createdBy),
UpdatedAt by UpdatedAt.Imp(),
DeletedAt by DeletedAt.Imp(),
DeletedAt,
Votable by VotableImp(),
TargetI {
constructor(
@@ -50,9 +50,9 @@ open class CommentForUpdate<T : TargetI, C : CitizenI>(
override val createdBy: C,
override val target: T,
open var content: String,
override val parent: CommentParent<T>? = null,
override val parent: CommentParentI<T>? = null,
override val deletedAt: DateTime? = null
) : CommentParent<T>(id, deletedAt, target),
) : CommentParentI<T> by CommentParent(id, deletedAt, target),
CommentWithParentI<T>,
ExtraI<T, C>,
CommentWithTargetI<T>,
@@ -62,7 +62,7 @@ open class CommentForUpdate<T : TargetI, C : CitizenI>(
TargetI {
constructor(
createdBy: C,
parent: CommentParent<T>,
parent: CommentParentI<T>,
content: String,
id: UUID? = null,
) : this(
@@ -74,21 +74,21 @@ open class CommentForUpdate<T : TargetI, C : CitizenI>(
)
}
open class CommentParent<T : TargetI>(
data class CommentParent<T : TargetI>(
override val id: UUID,
override val deletedAt: DateTime?,
override val target: T
) : CommentRef(id),
CommentParentI<T>
interface CommentParentI<T : TargetI> : CommentI, DeletedAt, CommentWithTargetI<T>
sealed interface CommentParentI<T : TargetI> : CommentI, DeletedAt, CommentWithTargetI<T>
interface CommentWithTargetI<T : TargetI> : CommentI, TargetI, HasTarget<T>
interface CommentWithParentI<T : TargetI> {
val parent: CommentParent<T>?
val parent: CommentParentI<T>?
}
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentI, TargetRef(id)
interface CommentI : EntityI
sealed interface CommentI : EntityI

View File

@@ -21,7 +21,7 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
): Paginated<CommentForView<T, CitizenCreatorI>>
open fun findByParent(
parent: CommentForView<T, CitizenCreatorI>,
parent: CommentI,
page: Int = 1,
limit: Int = 50
): Paginated<CommentForView<T, CitizenCreatorI>> {
@@ -32,26 +32,21 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
parentId: UUID,
page: Int = 1,
limit: Int = 50
): Paginated<CommentForView<T, CitizenCreatorI>> {
return requester.run {
getFunction("find_comments_by_parent")
.select<CommentForView<T, CitizenCreator>>(
page,
limit,
"parent_id" to parentId
)
as Paginated<CommentForView<T, CitizenCreatorI>>
}
}
): Paginated<CommentForView<T, CitizenCreatorI>> = requester
.getFunction("find_comments_by_parent")
.select<CommentForView<T, CitizenCreator>>(
page,
limit,
"parent_id" to parentId
)
as Paginated<CommentForView<T, CitizenCreatorI>>
open fun findByTarget(
target: EntityI,
page: Int = 1,
limit: Int = 50,
sort: String = "createdAt"
): Paginated<CommentForView<T, CitizenCreatorI>> {
return findByTarget(target.id, page, limit, sort)
}
): Paginated<CommentForView<T, CitizenCreatorI>> = findByTarget(target.id, page, limit, sort)
open fun findByTarget(
targetId: UUID,
@@ -85,41 +80,33 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
}
class CommentRepository(requester: Requester) : CommentRepositoryAbs<TargetRef>(requester) {
override fun findById(id: UUID): CommentForView<TargetRef, CitizenCreatorI>? {
return requester
.getFunction("find_comment_by_id")
.selectOne<CommentForView<TargetRef, CitizenCreator>>(mapOf("id" to id))
as CommentForView<TargetRef, CitizenCreatorI>?
}
override fun findById(id: UUID): CommentForView<TargetRef, CitizenCreatorI>? = requester
.getFunction("find_comment_by_id")
.selectOne<CommentForView<TargetRef, CitizenCreator>>(mapOf("id" to id))
as CommentForView<TargetRef, CitizenCreatorI>?
override fun findByCitizen(
citizen: CitizenI,
page: Int,
limit: Int
): Paginated<CommentForView<TargetRef, CitizenCreatorI>> {
return requester.run {
getFunction("find_comments_by_citizen")
.select<CommentForView<TargetRef, CitizenCreator>>(
page,
limit,
"created_by_id" to citizen.id
) as Paginated<CommentForView<TargetRef, CitizenCreatorI>>
}
}
): Paginated<CommentForView<TargetRef, CitizenCreatorI>> = requester
.getFunction("find_comments_by_citizen")
.select<CommentForView<TargetRef, CitizenCreator>>(
page,
limit,
"created_by_id" to citizen.id
) as Paginated<CommentForView<TargetRef, CitizenCreatorI>>
override fun findByParent(
parentId: UUID,
page: Int,
limit: Int
): Paginated<CommentForView<TargetRef, CitizenCreatorI>> {
return requester.run {
getFunction("find_comments_by_parent")
.select<CommentForView<TargetRef, CitizenCreator>>(
page,
limit,
"parent_id" to parentId
)
as Paginated<CommentForView<TargetRef, CitizenCreatorI>>
}
}
): Paginated<CommentForView<TargetRef, CitizenCreatorI>> = requester
.getFunction("find_comments_by_parent")
.select<CommentForView<TargetRef, CitizenCreator>>(
page,
limit,
"parent_id" to parentId
)
as Paginated<CommentForView<TargetRef, CitizenCreatorI>>
}

View File

@@ -54,7 +54,7 @@ object CreateComment {
parent = parent,
)
}.let { newComment ->
ac.assert { canCreate(newComment, citizenOrNull) }
ac.canCreate(newComment, citizenOrNull).assert()
repo.comment(newComment)
call.respond(HttpStatusCode.Created, newComment.toOutput())
}

View File

@@ -42,7 +42,7 @@ object EditComment {
put<EditCommentRequest> {
mustBeAuth()
val commentOld = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
ac.assert { canUpdate(commentOld, citizenOrNull) }
ac.canUpdate(commentOld, citizenOrNull).assert()
call.receiveOrBadRequest<EditCommentRequest.Input>()
.apply { validate().badRequestIfNotValid() }

View File

@@ -4,6 +4,7 @@ import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.comment.toOutput
import fr.dcproject.routes.PaginatedRequest
@@ -21,11 +22,13 @@ import java.util.UUID
object GetCommentChildren {
@Location("/comments/{comment}/children")
class CommentChildrenRequest(
val comment: UUID,
comment: UUID,
page: Int = 1,
limit: Int = 50,
val search: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit)
) : PaginatedRequestI by PaginatedRequest(page, limit) {
val comment = CommentRef(comment)
}
fun Route.getChildrenComments(repo: CommentRepository, ac: CommentAccessControl) {
get<CommentChildrenRequest> {
@@ -36,7 +39,7 @@ object GetCommentChildren {
it.limit
)
ac.assert { canView(comments.result, citizenOrNull) }
ac.canView(comments.result, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,

View File

@@ -27,7 +27,7 @@ object GetOneComment {
fun Route.getOneComment(repo: CommentRepository, ac: CommentAccessControl) {
get<CommentRequest> {
val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment ${it.comment.id} not found")
ac.assert { canView(comment, citizenOrNull) }
ac.canView(comment, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,

View File

@@ -89,7 +89,7 @@ object CreateConstitution {
post<PostConstitutionRequest> {
mustBeAuth()
getNewConstitution(call.receiveOrBadRequest(), citizen).let {
ac.assert { canCreate(it, citizenOrNull) }
ac.canCreate(it, citizenOrNull).assert()
val c = repo.upsert(it) ?: error("Unable to create Constitution")
call.respond(
HttpStatusCode.Created,

View File

@@ -54,7 +54,7 @@ object FindConstitutions {
get<FindConstitutionsRequest> {
it.validate().badRequestIfNotValid()
val constitutions = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
ac.assert { canView(constitutions.result, citizenOrNull) }
ac.canView(constitutions.result, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
constitutions.toOutput { c ->

View File

@@ -27,7 +27,7 @@ object GetConstitution {
fun Route.getConstitution(ac: ConstitutionAccessControl, constitutionRepo: ConstitutionRepository) {
get<GetConstitutionRequest> {
val constitution = constitutionRepo.findById(it.constitution.id) ?: throw NotFoundException("Unable to find constitution ${it.constitution.id}")
ac.assert { canView(constitution, citizenOrNull) }
ac.canView(constitution, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
constitution.let { c ->

View File

@@ -7,10 +7,8 @@ import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.response.respondText
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.util.KtorExperimentalAPI
@KtorExperimentalLocationsAPI
@KtorExperimentalAPI
fun Route.definition() {
get("/") {
call.respondText("/openapi.yaml".readResource(), ContentType("text", "yaml"))

View File

@@ -3,10 +3,8 @@ package fr.dcproject.component.doc.routes
import io.ktor.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.ExperimentalCoroutinesApi
@KtorExperimentalAPI
@ExperimentalCoroutinesApi
@KtorExperimentalLocationsAPI
fun Routing.installDocRoutes() {

View File

@@ -1,11 +1,13 @@
package fr.dcproject.component.follow
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import org.koin.dsl.module
val followKoinModule = module {
single { FollowArticleRepository(get()) }
single { FollowConstitutionRepository(get()) }
single { FollowCitizenRepository(get()) }
single { FollowAccessControl() }
}

View File

@@ -8,19 +8,18 @@ import fr.dcproject.common.entity.HasTarget
import fr.dcproject.common.entity.TargetI
import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef
import java.util.UUID
open class FollowForView<T : TargetI>(
id: UUID = UUID.randomUUID(),
data class FollowForView<T : TargetI>(
override val id: UUID = UUID.randomUUID(),
override val createdBy: CitizenCreator,
override var target: T
) : ExtraI<T, CitizenRef>,
) : ExtraI<T, CitizenI>,
FollowRef(id),
Created<CitizenRef> by Created.Imp(createdBy)
Created<CitizenI> by Created.Imp(createdBy)
class FollowForUpdate<T : TargetI, C : CitizenI>(
id: UUID = UUID.randomUUID(),
data class FollowForUpdate<T : TargetI, C : CitizenI>(
override val id: UUID = UUID.randomUUID(),
override val target: T,
override val createdBy: C
) : FollowRef(id),
@@ -31,4 +30,4 @@ open class FollowRef(
override val id: UUID
) : FollowI
interface FollowI : EntityI
sealed interface FollowI : EntityI

View File

@@ -4,7 +4,9 @@ import fr.dcproject.common.entity.Entity
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.citizen.database.Citizen
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.constitution.database.ConstitutionForView
import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.postgresjson.connexion.Paginated
@@ -72,21 +74,24 @@ sealed class FollowRepository<IN : TargetRef, OUT : TargetRef>(override var requ
target: Entity,
bulkSize: Int = 300
): Flow<FollowForView<IN>> = flow {
var nextPage = 1
do {
val paginate = findFollowsByTarget(target, nextPage, bulkSize)
paginate.result.forEach {
var lastId: UUID? = null
while (true) {
val result = findFollowsByTarget(target, lastId, bulkSize)
if (result.count() == 0) {
break
}
result.forEach {
emit(it)
}
nextPage = paginate.currentPage + 1
} while (!paginate.isLastPage())
lastId = result.last().id
}
}
abstract fun findFollowsByTarget(
target: Entity,
page: Int = 1,
lastId: UUID?,
limit: Int = 300
): Paginated<FollowForView<IN>>
): List<FollowForView<IN>>
}
class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRef, ArticleForView>(requester) {
@@ -107,14 +112,14 @@ class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRe
override fun findFollowsByTarget(
target: Entity,
page: Int,
lastId: UUID?,
limit: Int
): Paginated<FollowForView<ArticleRef>> {
): List<FollowForView<ArticleRef>> {
return requester
.getFunction("find_follows_article_by_target")
.select(
page,
limit,
"start_id" to lastId,
"limit" to limit,
"target_id" to target.id
)
}
@@ -138,9 +143,34 @@ class FollowConstitutionRepository(requester: Requester) : FollowRepository<Cons
override fun findFollowsByTarget(
target: Entity,
page: Int,
lastId: UUID?,
limit: Int
): Paginated<FollowForView<ConstitutionRef>> {
): List<FollowForView<ConstitutionRef>> {
TODO("Not yet implemented")
}
}
class FollowCitizenRepository(requester: Requester) : FollowRepository<CitizenRef, Citizen>(requester) {
override fun findByCitizen(
citizenId: UUID,
page: Int,
limit: Int
): Paginated<FollowForView<Citizen>> {
return requester.run {
getFunction("find_follows_citizen_by_citizen")
.select(
page,
limit,
"created_by_id" to citizenId
)
}
}
override fun findFollowsByTarget(
target: Entity,
lastId: UUID?,
limit: Int
): List<FollowForView<CitizenRef>> {
TODO("Not yet implemented")
}
}

View File

@@ -28,7 +28,7 @@ object FollowArticle {
post<ArticleFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
ac.assert { canCreate(follow, citizenOrNull) }
ac.canCreate(follow, citizenOrNull).assert()
repo.follow(follow)
call.respond(HttpStatusCode.Created)
}

View File

@@ -7,6 +7,7 @@ import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.routes.citizen.toOutput
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -14,7 +15,6 @@ import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import org.joda.time.DateTime
import java.util.UUID
@KtorExperimentalLocationsAPI
@@ -27,22 +27,10 @@ object GetFollowArticle {
fun Route.getFollowArticle(repo: FollowArticleRepository, ac: FollowAccessControl) {
get<ArticleFollowRequest> {
repo.findFollow(citizen, it.article)?.let { follow ->
ac.assert { canView(follow, citizenOrNull) }
ac.canView(follow, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
follow.let { f ->
object {
val id: UUID = f.id
val createdBy: Any = f.createdBy.toOutput()
val target: Any = f.target.let { t ->
object {
val id: UUID = t.id
val reference: String = f.target.reference
}
}
val createdAt: DateTime = f.createdAt
}
}
follow.toOutput()
)
} ?: call.respond(HttpStatusCode.NoContent)
}

View File

@@ -28,7 +28,7 @@ object GetMyFollowsArticle {
get<CitizenFollowArticleRequest> {
mustBeAuth()
val follows = repo.findByCitizen(it.citizen)
ac.assert { canView(follows.result, citizenOrNull) }
ac.canView(follows.result, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
follows.toOutput { f ->

View File

@@ -28,7 +28,7 @@ object UnfollowArticle {
delete<ArticleFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
ac.assert { canDelete(follow, citizenOrNull) }
ac.canDelete(follow, citizenOrNull).assert()
repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent)
}

View File

@@ -0,0 +1,36 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowForUpdate
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object FollowCitizen {
@Location("/citizens/{citizen}/follows")
class CitizenFollowRequest(citizen: UUID) {
val citizen = CitizenRef(citizen)
}
fun Route.followCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
post<CitizenFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.citizen, createdBy = this.citizen)
ac.canCreate(follow, citizenOrNull).assert()
repo.follow(follow)
call.respond(HttpStatusCode.Created)
}
}
}

View File

@@ -0,0 +1,37 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowCitizenRepository
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object GetFollowCitizen {
@Location("/citizens/{citizen}/follows")
class CitizenFollowRequest(citizen: UUID) {
val citizen = CitizenRef(citizen)
}
fun Route.getFollowCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
get<CitizenFollowRequest> {
repo.findFollow(citizen, it.citizen)?.let { follow ->
ac.canView(follow, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
follow.toOutput()
)
} ?: call.respond(HttpStatusCode.NoContent)
}
}
}

View File

@@ -0,0 +1,50 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowCitizenRepository
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import org.joda.time.DateTime
import java.util.UUID
@KtorExperimentalLocationsAPI
object GetMyFollowsCitizen {
@Location("/citizens/{citizen}/follows/citizens")
class CitizenFollowCitizenRequest(citizen: UUID) {
val citizen = CitizenRef(citizen)
}
fun Route.getMyFollowsCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
get<CitizenFollowCitizenRequest> {
mustBeAuth()
val follows = repo.findByCitizen(it.citizen)
ac.canView(follows.result, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
follows.toOutput { f ->
object {
val id: UUID = f.id
val createdBy: Any = f.createdBy.toOutput()
val target: Any = f.target.let { t ->
object {
val id: UUID = t.id
val reference: String = f.target.reference
}
}
val createdAt: DateTime = f.createdAt
}
}
)
}
}
}

View File

@@ -0,0 +1,36 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowForUpdate
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.delete
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object UnfollowCitizen {
@Location("/citizens/{citizen}/follows")
class CitizenFollowRequest(citizen: UUID) {
val citizen = CitizenRef(citizen)
}
fun Route.unfollowCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
delete<CitizenFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.citizen, createdBy = this.citizen)
ac.canDelete(follow, citizenOrNull).assert()
repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent)
}
}
}

View File

@@ -0,0 +1,20 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.component.follow.routes.citizen.FollowCitizen.followCitizen
import fr.dcproject.component.follow.routes.citizen.GetFollowCitizen.getFollowCitizen
import fr.dcproject.component.follow.routes.citizen.GetMyFollowsCitizen.getMyFollowsCitizen
import fr.dcproject.component.follow.routes.citizen.UnfollowCitizen.unfollowCitizen
import io.ktor.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing
import org.koin.ktor.ext.get
@KtorExperimentalLocationsAPI
fun Routing.installFollowCitizenRoutes() {
authenticate(optional = true) {
followCitizen(get(), get())
unfollowCitizen(get(), get())
getFollowCitizen(get(), get())
getMyFollowsCitizen(get(), get())
}
}

View File

@@ -0,0 +1,20 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.common.response.toOutput
import fr.dcproject.component.follow.database.FollowForView
import org.joda.time.DateTime
import java.util.UUID
fun FollowForView<*>.toOutput(): Any = this.let { f ->
object {
val id: UUID = f.id
val createdBy: Any = f.createdBy.toOutput()
val target: Any = f.target.let { t ->
object {
val id: UUID = t.id
val reference: String = f.target.reference
}
}
val createdAt: DateTime = f.createdAt
}
}

View File

@@ -28,7 +28,7 @@ object FollowConstitution {
post<ConstitutionFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
ac.assert { canCreate(follow, citizenOrNull) }
ac.canCreate(follow, citizenOrNull).assert()
repo.follow(follow)
call.respond(HttpStatusCode.Created)
}

View File

@@ -7,6 +7,7 @@ import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.follow.routes.citizen.toOutput
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -14,7 +15,6 @@ import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import org.joda.time.DateTime
import java.util.UUID
@KtorExperimentalLocationsAPI
@@ -27,22 +27,10 @@ object GetFollowConstitution {
fun Route.getFollowConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) {
get<ConstitutionFollowRequest> {
repo.findFollow(citizen, it.constitution)?.let { follow ->
ac.assert { canView(follow, citizenOrNull) }
ac.canView(follow, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
follow.let { f ->
object {
val id: UUID = f.id
val createdBy: Any = f.createdBy.toOutput()
val target: Any = f.target.let { t ->
object {
val id: UUID = t.id
val reference: String = f.target.reference
}
}
val createdAt: DateTime = f.createdAt
}
}
follow.toOutput()
)
} ?: call.respond(HttpStatusCode.NotFound)
}

View File

@@ -28,7 +28,7 @@ object GetMyFollowsConstitution {
get<CitizenFollowConstitutionRequest> {
mustBeAuth()
val follows = repo.findByCitizen(it.citizen)
ac.assert { canView(follows.result, citizenOrNull) }
ac.canView(follows.result, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
follows.toOutput { f ->

View File

@@ -28,7 +28,7 @@ object UnfollowConstitution {
delete<ConstitutionUnfollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
ac.assert { canDelete(follow, citizenOrNull) }
ac.canDelete(follow, citizenOrNull).assert()
repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent)
}

View File

@@ -1,112 +0,0 @@
package fr.dcproject.component.notification
import com.rabbitmq.client.AMQP.BasicProperties
import com.rabbitmq.client.BuiltinExchangeType.DIRECT
import com.rabbitmq.client.ConnectionFactory
import com.rabbitmq.client.Consumer
import com.rabbitmq.client.DefaultConsumer
import com.rabbitmq.client.Envelope
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.follow.database.FollowForView
import io.ktor.utils.io.errors.IOException
import io.lettuce.core.RedisClient
import io.lettuce.core.api.async.RedisAsyncCommands
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.runBlocking
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class NotificationConsumer(
private val rabbitFactory: ConnectionFactory,
private val redisClient: RedisClient,
private val followConstitutionRepo: FollowConstitutionRepository,
private val followArticleRepo: FollowArticleRepository,
private val notificationEmailSender: NotificationEmailSender,
private val exchangeName: String,
) {
private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
private val redis: RedisAsyncCommands<String, String> = redisConnection.async() ?: error("Unable to connect to redis")
private val rabbitConnection = rabbitFactory.newConnection()
private val rabbitChannel = rabbitConnection.createChannel()
private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName)
fun close() {
rabbitChannel.close()
rabbitConnection.close()
}
fun start() {
/* Config Rabbit */
rabbitFactory.newConnection().use { connection ->
connection.createChannel().use { channel ->
channel.queueDeclare("push", true, false, false, null)
channel.queueDeclare("email", true, false, false, null)
channel.exchangeDeclare(exchangeName, DIRECT, true)
channel.queueBind("push", exchangeName, "")
channel.queueBind("email", exchangeName, "")
}
}
/* Define Consumer */
val consumerPush: Consumer = object : DefaultConsumer(rabbitChannel) {
@Throws(IOException::class)
override fun handleDelivery(
consumerTag: String,
envelope: Envelope,
properties: BasicProperties,
body: ByteArray
) = runBlocking {
followersFromMessage(body) {
redis.zadd(
"notification:${it.follow.createdBy.id}",
it.event.id,
it.rawMessage
)
}
rabbitChannel.basicAck(envelope.deliveryTag, false)
}
}
val consumerEmail: Consumer = object : DefaultConsumer(rabbitChannel) {
@Throws(IOException::class)
override fun handleDelivery(
consumerTag: String,
envelope: Envelope,
properties: BasicProperties,
body: ByteArray
) {
runBlocking {
followersFromMessage(body) {
notificationEmailSender.sendEmail(it.follow)
logger.debug("EmailSend to: ${it.follow.createdBy.id}")
}
}
rabbitChannel.basicAck(envelope.deliveryTag, false)
}
}
/* Launch Consumer */
rabbitChannel.basicConsume("push", false, consumerPush) // The front consume the redis via Websocket
rabbitChannel.basicConsume("email", false, consumerEmail)
}
private suspend fun followersFromMessage(body: ByteArray, action: suspend (DecodedMessage) -> Unit) {
val rawMessage: String = body.toString(Charsets.UTF_8)
val notification: EntityNotification = Notification.fromString(rawMessage)
val follows = when (notification.type) {
"article" -> followArticleRepo.findFollowsByTarget(notification.target)
"constitution" -> followConstitutionRepo.findFollowsByTarget(notification.target)
else -> error("event '${notification.type}' not implemented")
}
follows.collect { action(DecodedMessage(notification, rawMessage, it)) }
}
private class DecodedMessage(
val event: EntityNotification,
val rawMessage: String,
val follow: FollowForView<out TargetRef>
)
}

View File

@@ -0,0 +1,67 @@
package fr.dcproject.component.notification
import com.rabbitmq.client.BuiltinExchangeType.DIRECT
import com.rabbitmq.client.ConnectionFactory
import com.rabbitmq.client.DefaultConsumer
import fr.dcproject.common.entity.Entity
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.common.utils.consumeQueue
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.follow.database.FollowForView
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.runBlocking
abstract class NotificationConsumerAbstract(
private val rabbitFactory: ConnectionFactory,
private val followConstitutionRepo: FollowConstitutionRepository,
private val followArticleRepo: FollowArticleRepository,
private val followCitizenRepo: FollowCitizenRepository,
) {
private val rabbitConnection = rabbitFactory.newConnection()
private val rabbitChannel = rabbitConnection.createChannel()
fun close() {
rabbitChannel.close()
rabbitConnection.close()
}
fun declareQueue(queueName: String, exchangeName: String) {
rabbitFactory.newConnection().use { connection ->
connection.createChannel().use { channel ->
channel.queueDeclare(queueName, true, false, false, null)
channel.exchangeDeclare(exchangeName, DIRECT, true)
channel.queueBind(queueName, exchangeName, "")
}
}
}
protected fun consumeQueue(queueName: String, callback: DefaultConsumer.(DecodedMessage<*>) -> Unit) =
rabbitChannel.consumeQueue(queueName) { body ->
runBlocking {
followersFromMessage(body) {
callback(it)
}
}
}
protected suspend fun followersFromMessage(body: ByteArray, action: suspend (DecodedMessage<*>) -> Unit) {
val rawMessage: String = body.toString(Charsets.UTF_8)
val notification: EntityNotificationMessage<*> = NotificationMessage.fromString(rawMessage)
val follows = when (notification.type) {
"article" -> followArticleRepo.findFollowsByTarget(notification.target)
"constitution" -> followConstitutionRepo.findFollowsByTarget(notification.target)
"citizen" -> followCitizenRepo.findFollowsByTarget(notification.target)
else -> error("event '${notification.type}' not implemented")
}
follows.collect { action(DecodedMessage(notification, rawMessage, it)) }
}
protected class DecodedMessage <E : Entity> (
val event: EntityNotificationMessage<E>,
val rawMessage: String,
val follow: FollowForView<out TargetRef>
)
}

View File

@@ -1,70 +0,0 @@
package fr.dcproject.component.notification
import com.sendgrid.helpers.mail.Mail
import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.common.email.Mailer
import fr.dcproject.common.entity.EntityI
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.article.database.ArticleWithTitleI
import fr.dcproject.component.citizen.database.CitizenCreatorI
import fr.dcproject.component.citizen.database.CitizenRepository
import fr.dcproject.component.follow.database.FollowForView
import java.util.UUID
class NotificationEmailSender(
private val mailer: Mailer,
private val domain: String,
private val citizenRepo: CitizenRepository,
private val articleRepo: ArticleRepository
) {
fun sendEmail(follow: FollowForView<out TargetRef>) {
val citizen = citizenRepo.findById(follow.createdBy.id) ?: noCitizen(follow.createdBy.id)
val target = when (follow.target.reference) {
"article" ->
articleRepo.findById(follow.target.id) ?: noTarget(follow.target.id)
else -> noTarget(follow.target.id)
}
val subject = when (follow.target.reference) {
"article" -> """New version for article "${target.title}""""
else -> "Notification"
}
mailer.sendEmail {
Mail(
Email("notification@$domain"),
subject,
Email(citizen.email),
Content("text/plain", generateContent(citizen, target))
).apply {
addContent(Content("text/html", generateHtmlContent(citizen, target)))
}
}
}
private fun generateHtmlContent(citizen: CitizenCreatorI, target: EntityI): String? {
return when (target) {
is ArticleWithTitleI -> """
Hello ${citizen.name.getFullName()},<br/>
The article "${target.title}" was updated, check it <a href="http://$domain/articles/${target.id}">here</a>
""".trimIndent()
else -> noTarget(target.id)
}
}
private fun generateContent(citizen: CitizenCreatorI, target: EntityI): String {
return when (target) {
is ArticleWithTitleI -> """
Hello ${citizen.name.getFullName()},
The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id}
""".trimIndent()
else -> noTarget(target.id)
}
}
class NoCitizen(message: String) : Exception(message)
class NoTarget(message: String) : Exception(message)
private fun noCitizen(id: UUID): Nothing = throw NoCitizen("No Citizen with this id : $id")
private fun noTarget(id: UUID): Nothing = throw NoTarget("No Target with this id : $id")
}

View File

@@ -16,9 +16,9 @@ import java.util.concurrent.atomic.AtomicInteger
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true)
@JsonSubTypes(
JsonSubTypes.Type(value = ArticleUpdateNotification::class, name = "article")
JsonSubTypes.Type(value = ArticleUpdateNotificationMessage::class, name = "article")
)
open class Notification(
open class NotificationMessage(
val type: String,
val createdAt: DateTime = DateTime.now()
) {
@@ -48,16 +48,16 @@ open class Notification(
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
}
inline fun <reified T : Notification> fromString(raw: String): T = mapper.readValue(raw)
inline fun <reified T : NotificationMessage> fromString(raw: String): T = mapper.readValue(raw)
}
}
open class EntityNotification(
val target: Entity,
open class EntityNotificationMessage <E : Entity> (
open val target: E,
type: String,
val action: String
) : Notification(type)
) : NotificationMessage(type)
class ArticleUpdateNotification(
target: ArticleForView
) : EntityNotification(target, "article", "update")
data class ArticleUpdateNotificationMessage(
override val target: ArticleForView
) : EntityNotificationMessage<ArticleForView>(target, "article", "update")

View File

@@ -7,12 +7,15 @@ import kotlinx.coroutines.coroutineScope
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class Publisher(
class NotificationPublisherAsync(
private val factory: ConnectionFactory,
private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName),
private val logger: Logger = LoggerFactory.getLogger(NotificationPublisherAsync::class.qualifiedName),
private val exchangeName: String,
) {
suspend fun <T : EntityNotification> publish(it: T): Deferred<Unit> = coroutineScope {
/**
* Publish a new notification message to RabbitMQ
*/
suspend fun <T : EntityNotificationMessage<*>> publishAsync(it: T): Deferred<Unit> = coroutineScope {
async {
factory.newConnection().use { connection ->
connection.createChannel().use { channel ->

View File

@@ -0,0 +1,33 @@
package fr.dcproject.component.notification.email
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.notification.NotificationConsumerAbstract
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class NotificationEmailConsumer(
rabbitFactory: ConnectionFactory,
followConstitutionRepo: FollowConstitutionRepository,
followArticleRepo: FollowArticleRepository,
followCitizenRepo: FollowCitizenRepository,
private val notificationEmailSender: NotificationEmailSender,
private val exchangeName: String,
) : NotificationConsumerAbstract(rabbitFactory, followConstitutionRepo, followArticleRepo, followCitizenRepo) {
private val logger: Logger = LoggerFactory.getLogger(NotificationEmailConsumer::class.qualifiedName)
fun start() {
/* Config Rabbit */
declareQueue(QUEUE_NAME, exchangeName)
consumeQueue(QUEUE_NAME) { message ->
notificationEmailSender.sendEmail(message.follow)
logger.debug("EmailSend to: ${message.follow.createdBy.id}")
}
}
companion object {
private const val QUEUE_NAME = "email"
}
}

View File

@@ -0,0 +1,70 @@
package fr.dcproject.component.notification.email
import com.sendgrid.helpers.mail.Mail
import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.common.email.Mailer
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.citizen.database.Citizen
import fr.dcproject.component.citizen.database.CitizenRepository
import fr.dcproject.component.follow.database.FollowForView
import fr.dcproject.component.notification.email.content.ArticleNotificationEmailContent
import fr.dcproject.component.notification.email.content.CitizenNotificationEmailContent
import java.util.UUID
/**
* Send notification email on the follower
*/
class NotificationEmailSender(
private val mailer: Mailer,
private val domain: String,
private val citizenRepo: CitizenRepository,
private val articleRepo: ArticleRepository
) {
/**
* Send the Notification Email to the follower user
*/
fun sendEmail(follow: FollowForView<out TargetRef>) {
val citizen = citizenRepo.findById(follow.createdBy.id) ?: noCitizen(follow.createdBy.id)
/**
* Find the complete target entity by its ID according to its reference
*/
val target = when (follow.target.reference) {
"article" -> articleRepo.findById(follow.target.id) ?: noTarget(follow.target.id)
"citizen" -> citizenRepo.findById(follow.target.id) ?: noTarget(follow.target.id)
else -> noTarget(follow.target.id)
}
/**
* Find content of the email according to the target type
*/
val content = when (target) {
is ArticleForView -> ArticleNotificationEmailContent(citizen, target, domain)
is Citizen -> CitizenNotificationEmailContent(citizen, target, domain)
else -> noTargetTypeImplementation(follow.target.reference)
}
/* Send email */
mailer.sendEmail {
Mail(
Email("notification@$domain"),
content.subject,
Email(citizen.email),
Content("text/plain", content.content)
).apply {
addContent(Content("text/html", content.contentHtml))
}
}
}
class NoCitizen(message: String) : Exception(message)
class NoTarget(message: String) : Exception(message)
class NoTargetTypeImplement(message: String) : Exception(message)
private fun noCitizen(id: UUID): Nothing = throw NoCitizen("No Citizen with this id : $id")
private fun noTarget(id: UUID): Nothing = throw NoTarget("No Target with this id : $id")
private fun noTargetTypeImplementation(type: String): Nothing = throw NoTargetTypeImplement("No Target type implemented: $type")
}

View File

@@ -0,0 +1,29 @@
package fr.dcproject.component.notification.email.content
import fr.dcproject.component.article.database.ArticleWithTitleI
import fr.dcproject.component.citizen.database.Citizen
data class ArticleNotificationEmailContent(
private val citizen: Citizen,
private val target: ArticleWithTitleI,
private val domain: String,
) : NotificationEmailContent {
override val subject: String
get() = """New version for article "${target.title}""""
override val contentHtml
get() = run {
"""
Hello ${citizen.name.getFullName()},<br/>
The article "${target.title}" was updated, check it <a href="http://$domain/articles/${target.id}">here</a>
""".trimIndent()
}
override val content
get() = run {
"""
Hello ${citizen.name.getFullName()},
The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id}
""".trimIndent()
}
}

View File

@@ -0,0 +1,28 @@
package fr.dcproject.component.notification.email.content
import fr.dcproject.component.citizen.database.Citizen
data class CitizenNotificationEmailContent(
private val citizen: Citizen,
private val target: Citizen,
private val domain: String,
) : NotificationEmailContent {
override val subject: String
get() = """New activity for the citizen "${target.name}""""
override val contentHtml
get() = run {
"""
Hello ${citizen.name.getFullName()},
The citizen "${target.name}" was new activity, check it here: <a href="http://$domain/citizens/${target.id}">here</a>
""".trimIndent()
}
override val content
get() = run {
"""
Hello ${citizen.name.getFullName()},
The citizen "${target.name}" was new activity, check it here: http://$domain/citizens/${target.id}
""".trimIndent()
}
}

View File

@@ -0,0 +1,7 @@
package fr.dcproject.component.notification.email.content
interface NotificationEmailContent {
val subject: String
val content: String
val contentHtml: String
}

View File

@@ -0,0 +1,41 @@
package fr.dcproject.component.notification.push
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.notification.NotificationConsumerAbstract
import io.lettuce.core.RedisClient
import io.lettuce.core.api.async.RedisAsyncCommands
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class NotificationPushConsumer(
rabbitFactory: ConnectionFactory,
followConstitutionRepo: FollowConstitutionRepository,
followArticleRepo: FollowArticleRepository,
followCitizenRepo: FollowCitizenRepository,
redisClient: RedisClient,
private val exchangeName: String,
) : NotificationConsumerAbstract(rabbitFactory, followConstitutionRepo, followArticleRepo, followCitizenRepo) {
private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
private val redis: RedisAsyncCommands<String, String> = redisConnection.async() ?: error("Unable to connect to redis")
private val logger: Logger = LoggerFactory.getLogger(NotificationPushConsumer::class.qualifiedName)
fun start() {
/* Config Rabbit */
declareQueue(QUEUE_NAME, exchangeName)
consumeQueue(QUEUE_NAME) { message ->
redis.zadd(
"notification:${message.follow.createdBy.id}",
message.event.id,
message.rawMessage
)
logger.debug("Notification was transferred to the redis (follower: ${message.follow.createdBy.id})")
}
}
companion object {
private const val QUEUE_NAME = "push"
}
}

View File

@@ -1,8 +1,9 @@
package fr.dcproject.component.notification
package fr.dcproject.component.notification.push
import com.fasterxml.jackson.core.JsonProcessingException
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.notification.NotificationMessage
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.Frame.Text
import io.ktor.http.cio.websocket.readText
@@ -28,31 +29,42 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
class NotificationsPush(
/**
* Listen a custom flow to mark as read a message.
*
* And listen the redis subscription flow and call a callback when a new message arrives
*/
class NotificationPushListener(
private val redis: RedisAsyncCommands<String, String>,
private val redisConnectionPubSub: StatefulRedisPubSubConnection<String, String>,
citizen: CitizenI,
incoming: Flow<Notification>,
onReceive: suspend (Notification) -> Unit,
incoming: Flow<NotificationMessage>,
onReceive: suspend (NotificationMessage) -> Unit,
) {
class Builder(val redisClient: RedisClient) {
private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
private val redisConnectionPubSub = redisClient.connectPubSub() ?: error("Unable to connect to redis PubSub")
private val redis: RedisAsyncCommands<String, String> = redisConnection.async() ?: error("Unable to connect to redis Async")
class Builder(redisClient: RedisClient) {
private val redisConnection = redisClient.connect()
private val redisConnectionPubSub = redisClient.connectPubSub()
private val redis: RedisAsyncCommands<String, String> = redisConnection.async()
/**
* Build Listener with citizen, incoming flow and set an outgoing callback
*/
fun build(
citizen: CitizenI,
incoming: Flow<Notification>,
onReceive: suspend (Notification) -> Unit,
): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onReceive)
incoming: Flow<NotificationMessage>,
onReceive: suspend (NotificationMessage) -> Unit,
): NotificationPushListener = NotificationPushListener(redis, redisConnectionPubSub, citizen, incoming, onReceive)
/**
* Build NotificationPush with only a WebSocket session
*/
@ExperimentalCoroutinesApi
fun build(ws: DefaultWebSocketServerSession): NotificationsPush {
fun build(ws: DefaultWebSocketServerSession): NotificationPushListener {
/* Convert channel of string from websocket, to a flow of Notification object */
val incomingFlow: Flow<Notification> = ws.incoming.consumeAsFlow()
.mapNotNull<Frame, Text> { it as? Frame.Text }
val incomingFlow: Flow<NotificationMessage> = ws.incoming.consumeAsFlow()
.mapNotNull<Frame, Text> { it as? Text }
.map { it.readText() }
.map { Notification.fromString(it) }
.map { NotificationMessage.fromString(it) }
return build(ws.call.citizen, incomingFlow) {
ws.outgoing.send(Text(it.toString()))
@@ -62,30 +74,42 @@ class NotificationsPush(
}
}
/**
* The key of the SortedSet in Redis which contains all the messages of a user
*/
private val key = "notification:${citizen.id}"
private var score: Double = 0.0
/**
* The last score (a kind of sorted ids) of message
*/
private var lastScore: Double = 0.0
/**
* Configure the listener to listen all new notifications
*/
private val listener = object : RedisPubSubAdapter<String, String>() {
/* On new key publish */
override fun message(pattern: String?, channel: String?, message: String?) {
runBlocking {
getNotifications().collect {
getNewUnreadNotifications().collect {
onReceive(it)
}
}
}
}
/**
* Init the listener and the callback
*/
init {
/* Mark as read all incoming notifications */
GlobalScope.launch {
incoming.collect {
markAsRead(it)
it.markAsRead()
}
}
/* Get old notification and sent it to websocket */
runBlocking {
getNotifications().collect {
getNewUnreadNotifications().collect {
onReceive(it)
}
}
@@ -95,38 +119,55 @@ class NotificationsPush(
addListener(listener)
/* Register to the events */
async()?.psubscribe("__key*__:$key") ?: error("Unable to subscribe to redis events")
async()?.psubscribe("__key*__:$key")
}
}
/**
* Close the redis subscription
*/
fun close() {
redisConnectionPubSub.removeListener(listener)
}
/* Return flow with all new notifications */
private fun getNotifications() = flow<Notification> {
/**
* Get All new notification from redis and
* Return flow with notifications
*
* On start, on the first call, this method return all unread notification of the user
*
* Internally this method return all messages that greater of the lastScore,
* then define the lastScore with the score of the last message.
*/
private fun getNewUnreadNotifications() = flow<NotificationMessage> {
redis
.zrangebyscoreWithScores(
key,
Range.from(
Boundary.excluding(score),
Boundary.excluding(lastScore),
Boundary.including(Double.POSITIVE_INFINITY)
),
Limit.from(100)
)
.get().forEach {
emit(Notification.fromString(it.value))
if (it.score > score) score = it.score
/* Build message object from raw string and return it */
emit(NotificationMessage.fromString(it.value))
if (it.score > lastScore) lastScore = it.score
}
}
private suspend fun markAsRead(notificationMessage: Notification) = coroutineScope {
/**
* Mark one notification as read.
*
* Internally, this method remove the message of the SortedSet in redis
*/
private suspend fun NotificationMessage.markAsRead() = coroutineScope {
try {
redis.zremrangebyscore(
key,
Range.from(
Boundary.including(notificationMessage.id),
Boundary.including(notificationMessage.id)
Boundary.including(id),
Boundary.including(id)
)
)
} catch (e: JsonProcessingException) {

View File

@@ -1,6 +1,6 @@
package fr.dcproject.component.notification.routes
import fr.dcproject.component.notification.NotificationsPush
import fr.dcproject.component.notification.push.NotificationPushListener
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Route
import io.ktor.websocket.webSocket
@@ -13,8 +13,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
*/
@ExperimentalCoroutinesApi
@KtorExperimentalLocationsAPI
fun Route.notificationArticle(pushBuilder: NotificationsPush.Builder) {
fun Route.notificationArticle(pushListenerBuilder: NotificationPushListener.Builder) {
webSocket("/notifications") {
pushBuilder.build(this)
pushListenerBuilder.build(this)
}
}

View File

@@ -13,8 +13,8 @@ import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef
import java.util.UUID
open class Opinion<T : TargetI>(
id: UUID = UUID.randomUUID(),
data class Opinion<T : TargetI>(
override val id: UUID = UUID.randomUUID(),
override val createdBy: CitizenCreator,
override val target: T,
val choice: OpinionChoice
@@ -39,4 +39,4 @@ open class OpinionRef(
override val id: UUID
) : OpinionI, TargetRef(id)
interface OpinionI : EntityI
sealed interface OpinionI : EntityI

View File

@@ -7,6 +7,4 @@ interface Opinionable {
val opinions: Opinions
}
class OpinionableImp : Opinionable {
override var opinions: OpinionsMutable = mutableMapOf()
}
data class OpinionableImp(override var opinions: OpinionsMutable = mutableMapOf()) : Opinionable

View File

@@ -34,7 +34,7 @@ object GetCitizenOpinions {
get<CitizenOpinions> {
mustBeAuth()
val opinionsEntities: List<Opinion<ArticleRef>> = repo.findCitizenOpinionsByTargets(it.citizen, it.id)
ac.assert { canView(opinionsEntities, citizenOrNull) }
ac.canView(opinionsEntities, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.opinion.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
@@ -12,6 +13,9 @@ import fr.dcproject.component.opinion.database.Opinion
import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.connexion.Paginated
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -34,13 +38,24 @@ object GetMyOpinionsArticle {
limit: Int = 50
) : PaginatedRequestI by PaginatedRequest(page, limit) {
val citizen = CitizenRef(citizen)
fun validate() = Validation<CitizenOpinionsArticleRequest> {
CitizenOpinionsArticleRequest::page {
minimum(1)
}
CitizenOpinionsArticleRequest::limit {
minimum(1)
maximum(50)
}
}.validate(this)
}
fun Route.getMyOpinionsArticle(repo: OpinionArticleRepository, ac: OpinionAccessControl) {
get<CitizenOpinionsArticleRequest> {
mustBeAuth()
it.validate().badRequestIfNotValid()
val opinions: Paginated<Opinion<TargetRef>> = repo.findCitizenOpinions(citizen, it.page, it.limit)
ac.assert { canView(opinions.result, citizenOrNull) }
ac.canView(opinions.result, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,
opinions.toOutput { it.toOutput() }

View File

@@ -25,7 +25,7 @@ object GetOpinionChoice {
fun Route.getOpinionChoice(ac: OpinionChoiceAccessControl, opinionChoiceRepository: OpinionChoiceRepository) {
get<OpinionChoiceRequest> {
val opinionChoice = opinionChoiceRepository.findOpinionChoiceById(it.opinionChoice.id) ?: throw NotFoundException("OpinionChoice ${it.opinionChoice.id} not found")
ac.assert { canView(it.opinionChoice, citizenOrNull) }
ac.canView(it.opinionChoice, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,

View File

@@ -20,7 +20,7 @@ object GetOpinionChoices {
fun Route.getOpinionChoices(repo: OpinionChoiceRepository, ac: OpinionChoiceAccessControl) {
get<OpinionChoicesRequest> {
val opinionChoices = repo.findOpinionsChoices(it.targets)
ac.assert { canView(opinionChoices, citizenOrNull) }
ac.canView(opinionChoices, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,

View File

@@ -43,7 +43,7 @@ object OpinionArticle {
createdBy = citizen
)
}.let { opinions ->
ac.assert { canCreate(opinions, citizenOrNull) }
ac.canCreate(opinions, citizenOrNull).assert()
repo.updateOpinions(opinions)
}.let {
call.respond(

View File

@@ -2,7 +2,7 @@ package fr.dcproject.component.views.dto
import fr.dcproject.component.views.entity.ViewAggregation
class ViewAggregation(
data class ViewAggregation(
val total: Int,
val unique: Int
) {

View File

@@ -3,7 +3,7 @@ package fr.dcproject.component.views.entity
import fr.dcproject.common.entity.UpdatedAt
import fr.postgresjson.entity.EntityI
class ViewAggregation(
data class ViewAggregation(
val total: Int,
val unique: Int
) : EntityI,

View File

@@ -12,8 +12,8 @@ import fr.dcproject.component.citizen.database.CitizenCreatorI
import fr.dcproject.component.citizen.database.CitizenI
import java.util.UUID
class VoteForView<T : TargetI>(
id: UUID = UUID.randomUUID(),
data class VoteForView<T : TargetI>(
override val id: UUID = UUID.randomUUID(),
override val createdBy: CitizenCreator,
override val target: T,
val note: Int,
@@ -30,7 +30,7 @@ class VoteForView<T : TargetI>(
}
}
class VoteForUpdate<T : TargetI, C : CitizenI>(
data class VoteForUpdate<T : TargetI, C : CitizenI>(
override val id: UUID = UUID.randomUUID(),
override val note: Int,
override val target: T,

View File

@@ -2,10 +2,18 @@ package fr.dcproject.component.vote.dto
import fr.dcproject.component.vote.entity.Votable
class VoteAggregation(parent: Votable) {
val up: Int = parent.votes.up
val neutral: Int = parent.votes.neutral
val down: Int = parent.votes.down
val total: Int = parent.votes.total
val score: Int = parent.votes.score
data class VoteAggregation(
val up: Int,
val neutral: Int,
val down: Int,
val total: Int,
val score: Int
) {
constructor(parent: Votable) : this(
up = parent.votes.up,
neutral = parent.votes.neutral,
down = parent.votes.down,
total = parent.votes.total,
score = parent.votes.score
)
}

View File

@@ -4,6 +4,6 @@ interface Votable {
val votes: VoteAggregation
}
class VotableImp : Votable {
data class VotableImp(
override val votes: VoteAggregation = VoteAggregation()
}
) : Votable

View File

@@ -30,7 +30,7 @@ object GetCitizenVotes {
mustBeAuth()
val votes = repo.findCitizenVotesByTargets(it.citizen, it.id)
if (votes.isNotEmpty()) {
ac.assert { canView(votes, citizenOrNull) }
ac.canView(votes, citizenOrNull).assert()
}
call.respond(
HttpStatusCode.OK,

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.vote.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull
@@ -9,6 +10,9 @@ import fr.dcproject.component.vote.VoteAccessControl
import fr.dcproject.component.vote.database.VoteArticleRepository
import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -28,13 +32,24 @@ object GetCitizenVotesOnArticle {
val search: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) {
val citizen = CitizenRef(citizen)
fun validate() = Validation<CitizenVoteArticleRequest> {
CitizenVoteArticleRequest::page {
minimum(1)
}
CitizenVoteArticleRequest::limit {
minimum(1)
maximum(50)
}
}.validate(this)
}
fun Route.getCitizenVotesOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl) {
get<CitizenVoteArticleRequest> {
mustBeAuth()
it.validate().badRequestIfNotValid()
val votes = repo.findByCitizen(it.citizen, it.page, it.limit)
ac.assert { canView(votes.result, citizenOrNull) }
ac.canView(votes.result, citizenOrNull).assert()
call.respond(
HttpStatusCode.OK,

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.vote.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.article.database.ArticleRef
@@ -10,6 +11,9 @@ import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.vote.VoteAccessControl
import fr.dcproject.component.vote.database.VoteArticleRepository
import fr.dcproject.component.vote.database.VoteForUpdate
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call
import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode
@@ -25,20 +29,29 @@ object PutVoteOnArticle {
@Location("/articles/{article}/vote")
class ArticleVoteRequest(article: UUID) {
val article = ArticleRef(article)
data class Input(var note: Int)
data class Input(var note: Int) {
fun validate() = Validation<Input> {
Input::note {
minimum(-1)
maximum(1)
}
}.validate(this)
}
}
fun Route.putVoteOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl, articleRepo: ArticleRepository) {
put<ArticleVoteRequest> {
mustBeAuth()
val input = call.receiveOrBadRequest<ArticleVoteRequest.Input>()
.apply { validate().badRequestIfNotValid() }
val article = articleRepo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found")
val vote = VoteForUpdate(
target = article,
note = input.note,
createdBy = this.citizen
)
ac.assert { canCreate(vote, citizenOrNull) }
ac.canCreate(vote, citizenOrNull).assert()
val votes = repo.vote(vote)
call.respond(
HttpStatusCode.Created,

Some files were not shown because too many files have changed in this diff Show More