diff --git a/build.gradle.kts b/build.gradle.kts index 245ffa2..335a350 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { @@ -20,16 +21,19 @@ allprojects { ?.takeIf { version.toString().contains("SNAPSHOT") } ?.also { version = version.toString().replace("SNAPSHOT", "RC$it") } - tasks { - withType { - kotlinOptions.jvmTarget = "17" - } - withType { - options.encoding = "UTF-8" - sourceCompatibility = "17" - targetCompatibility = "17" + kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_21 + freeCompilerArgs = listOf( + "-opt-in=kotlin.RequiresOptIn", + "-Xcontext-receivers", + ) } } + + java { + toolchain.languageVersion.set(JavaLanguageVersion.of(21)) + } } tasks { diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index c9ad5e8..4b4db20 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -5,13 +5,13 @@ volumes: services: db: - image: postgres:latest + image: postgres:16.3 volumes: - postgres-data:/var/lib/postgresql/data environment: - POSTGRES_USER=admin - POSTGRES_PASSWORD=admin - - POSTGRES_DB=lab + - POSTGRES_DB=sgl ports: - "5432:5432" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b28bd73..234a847 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,6 @@ exposed = "0.49.0" [libraries] annotations = "org.jetbrains:annotations:24.0.1" -coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0" # com.expediagroup:graphql-kotlin-ktor-server:7.0.2 graphql-server = { module = "com.expediagroup:graphql-kotlin-ktor-server", version.ref = "graphql" } graphql-client = { module = "com.expediagroup:graphql-kotlin-ktor-client", version.ref = "graphql" } @@ -25,7 +24,7 @@ ktor-server-status-pages = { module = "io.ktor:ktor-server-status-pages" } ktor-serialization-kotlinx = { module = "io.ktor:ktor-serialization-kotlinx" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json" } # TODO: Remove kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } - +kotlinx-coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0" kubernetes-client = "io.fabric8:kubernetes-client:6.11.0" logging-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } logging-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } @@ -44,11 +43,13 @@ protobuf-kotlin = "com.google.protobuf:protobuf-kotlin:4.26.1" bcrypt = "org.mindrot:jbcrypt:0.4" +shiro = "org.apache.shiro:shiro-core:2.0.1" + [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ktlint = "org.jlleitschuh.gradle.ktlint:11.6.1" protobuf = "com.google.protobuf:0.9.4" -shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" } +shadow = { id = "io.github.goooler.shadow", version = "8.1.7" } ktor = "io.ktor.plugin:2.3.10" diff --git a/hub/build.gradle.kts b/hub/build.gradle.kts index 8e5537c..60dbabc 100644 --- a/hub/build.gradle.kts +++ b/hub/build.gradle.kts @@ -6,6 +6,7 @@ plugins { dependencies { implementation(libs.graphql.server) + implementation(libs.koin) implementation(libs.ktor.server.netty) implementation(libs.ktor.server.auth) implementation(libs.ktor.server.contentnegotiation) @@ -28,8 +29,23 @@ dependencies { implementation(libs.ktor.server.call.logging) implementation(libs.ktor.client.logging) implementation(libs.kotlin.reflect) + implementation(libs.kotlinx.coroutines) } application { mainClass.set("org.sourcegrade.lab.hub.MainKt") } + +tasks { + named("runShadow") { + environment("SGL_DB_URL", "jdbc:postgresql://localhost:5432/sgl") + environment("SGL_DB_USER", "admin") + environment("SGL_DB_PASSWORD", "admin") + environment("SGL_DEPLOYMENT_URL", "http://localhost:8080") + environment("SGL_AUTH_URL", "http://localhost:8080/auth") + environment("SGL_AUTH_CLIENT_ID", "sgl") + environment("SGL_AUTH_CLIENT_SECRET", "sgl") + environment("SGL_AUTH_ACCESS_TOKEN_URL", "http://localhost:8080/auth/token") + environment("SGL_AUTH_SCOPES", "openid profile email") + } +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/Main.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/Main.kt index 4e3e4ca..44c51cd 100644 --- a/hub/src/main/kotlin/org/sourcegrade/lab/hub/Main.kt +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/Main.kt @@ -1,5 +1,26 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + package org.sourcegrade.lab.hub -import io.ktor.server.netty.EngineMain +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty -fun main(args: Array) = EngineMain.main(args) +fun main() { + embeddedServer(Netty, port = 7500) { module() }.start(wait = true) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/Module.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/Module.kt index 7c71cf9..683e73a 100644 --- a/hub/src/main/kotlin/org/sourcegrade/lab/hub/Module.kt +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/Module.kt @@ -1,49 +1,122 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + package org.sourcegrade.lab.hub +import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks import com.expediagroup.graphql.server.ktor.GraphQL import com.expediagroup.graphql.server.ktor.graphQLGetRoute import com.expediagroup.graphql.server.ktor.graphQLPostRoute import com.expediagroup.graphql.server.ktor.graphQLSDLRoute import com.expediagroup.graphql.server.ktor.graphiQLRoute +import graphql.schema.GraphQLType import io.ktor.http.HttpHeaders import io.ktor.http.HttpMethod -import io.ktor.http.Url import io.ktor.server.application.Application import io.ktor.server.application.install -import io.ktor.server.config.tryGetString import io.ktor.server.plugins.callloging.CallLogging import io.ktor.server.plugins.cors.routing.CORS import io.ktor.server.request.path import io.ktor.server.routing.Routing +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.DatabaseConfig +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.transactions.transaction +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module +import org.koin.ktor.ext.inject +import org.koin.ktor.plugin.Koin +import org.sourcegrade.lab.hub.db.CourseMemberships +import org.sourcegrade.lab.hub.db.Criteria +import org.sourcegrade.lab.hub.db.DBModule +import org.sourcegrade.lab.hub.db.GradedCriteria +import org.sourcegrade.lab.hub.db.GradedRubrics +import org.sourcegrade.lab.hub.db.GradingRuns +import org.sourcegrade.lab.hub.db.Rubrics +import org.sourcegrade.lab.hub.db.SubmissionGroupCategories +import org.sourcegrade.lab.hub.db.SubmissionGroupMemberships +import org.sourcegrade.lab.hub.db.Submissions +import org.sourcegrade.lab.hub.db.Terms +import org.sourcegrade.lab.hub.db.assignment.Assignments +import org.sourcegrade.lab.hub.db.course.Courses +import org.sourcegrade.lab.hub.db.user.Users +import org.sourcegrade.lab.hub.graphql.AssignmentMutations +import org.sourcegrade.lab.hub.graphql.AssignmentQueries +import org.sourcegrade.lab.hub.graphql.CourseMutations +import org.sourcegrade.lab.hub.graphql.CourseQueries +import org.sourcegrade.lab.hub.graphql.Scalars +import org.sourcegrade.lab.hub.graphql.UserMutations +import org.sourcegrade.lab.hub.graphql.UserQueries import org.sourcegrade.lab.hub.http.authenticationModule -import org.sourcegrade.lab.hub.queries.CourseMutations -import org.sourcegrade.lab.hub.queries.CourseQueries -import org.sourcegrade.lab.hub.queries.HelloWorldQuery -import org.sourcegrade.lab.hub.queries.UserMutations -import org.sourcegrade.lab.hub.queries.UserQueries +import kotlin.reflect.KType fun Application.module() { - val environment = environment - val url = - Url( - environment.config.tryGetString("ktor.deployment.url") - ?: throw IllegalStateException("No deployment url set"), + install(Koin) { + modules( + module { + single { LogManager.getLogger("SGL Supervisor") } + singleOf(::UserQueries) + singleOf(::UserMutations) + singleOf(::AssignmentQueries) + singleOf(::AssignmentMutations) + singleOf(::CourseQueries) + singleOf(::CourseMutations) + }, + DBModule, ) - + } val databaseConfig = DatabaseConfig { keepLoadedReferencesOutOfTransaction = true } + val logger by inject() + + logger.info("Connecting to database...") Database.connect( - environment.config.tryGetString("ktor.db.url") ?: "", + url = getEnv("SGL_DB_URL"), driver = "org.postgresql.Driver", - user = environment.config.tryGetString("ktor.db.user") ?: "", - password = environment.config.tryGetString("ktor.db.password") ?: "", + user = getEnv("SGL_DB_USER"), + password = getEnv("SGL_DB_PASSWORD"), databaseConfig = databaseConfig, ) + logger.info("Finished connecting to database.") + logger.info("Creating tables...") + transaction { + SchemaUtils.createMissingTablesAndColumns( + Assignments, + CourseMemberships, + Courses, + Criteria, + GradedCriteria, + GradedRubrics, + GradingRuns, + Rubrics, + SubmissionGroupCategories, + SubmissionGroupMemberships, + Submissions, + Terms, + Users, + ) + } + logger.info("Finished creating tables.") install(CORS) { allowMethod(HttpMethod.Options) @@ -56,17 +129,21 @@ fun Application.module() { install(GraphQL) { schema { + hooks = object : SchemaGeneratorHooks { + override fun willGenerateGraphQLType(type: KType): GraphQLType? = Scalars.willGenerateGraphQLType(type) + } packages = listOf("org.sourcegrade.lab.hub") queries = listOf( - HelloWorldQuery(), - UserQueries(), - CourseQueries(), + inject().value, + inject().value, + inject().value, ) mutations = listOf( - UserMutations(), - CourseMutations(), + inject().value, + inject().value, + inject().value, ) } } @@ -85,3 +162,5 @@ fun Application.module() { filter { call -> call.request.path().startsWith("/") } } } + +internal fun getEnv(key: String): String = System.getenv(key) ?: throw IllegalStateException("$key not set in environment") diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/Routing.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/Routing.kt index 8d30a7a..bf583da 100644 --- a/hub/src/main/kotlin/org/sourcegrade/lab/hub/Routing.kt +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/Routing.kt @@ -1,3 +1,21 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + package org.sourcegrade.lab.hub import io.ktor.client.request.get diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/CourseMemberships.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/CourseMemberships.kt new file mode 100644 index 0000000..89ce8da --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/CourseMemberships.kt @@ -0,0 +1,54 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import kotlinx.datetime.Clock +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.sourcegrade.lab.hub.db.course.Courses +import org.sourcegrade.lab.hub.db.course.DBCourse +import org.sourcegrade.lab.hub.db.user.DBUser +import org.sourcegrade.lab.hub.db.user.Users +import org.sourcegrade.lab.hub.domain.Course +import org.sourcegrade.lab.hub.domain.UserMembership +import java.util.UUID + +internal object CourseMemberships : UUIDTable("sgl_course_membership") { + val createdUtc = timestamp("createdUtc").clientDefault { Clock.System.now() } + val userId = reference("user_id", Users) + val startUtc = timestamp("startUtc") + val endUtc = timestamp("endUtc").nullable() + val courseId = reference("course_id", Courses) +} + +@GraphQLIgnore +internal class DBCourseMembership(id: EntityID) : UUIDEntity(id), UserMembership { + override val uuid: UUID = id.value + override val createdUtc by CourseMemberships.createdUtc + override val startUtc by CourseMemberships.startUtc + override val endUtc by CourseMemberships.endUtc + override val user: DBUser by DBUser referencedOn CourseMemberships.userId + override val target: DBCourse by DBCourse referencedOn CourseMemberships.courseId + + companion object : EntityClass(CourseMemberships) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/Criteria.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/Criteria.kt new file mode 100644 index 0000000..181c072 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/Criteria.kt @@ -0,0 +1,53 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import kotlinx.datetime.Instant +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.SizedIterable +import org.sourcegrade.lab.hub.domain.Criterion +import org.sourcegrade.lab.hub.domain.Rubric +import java.util.UUID + +internal object Criteria : UUIDTable("sgl_criteria") { + val parentRubricId = reference("parent_rubric_id", Rubrics) + val parentCriterionId = reference("parent_criterion_id", Criteria).nullable() + val minPoints = integer("minPoints") + val maxPoints = integer("maxPoints") + val description = text("description") +} + +@GraphQLIgnore +class DBCriterion(id: EntityID) : UUIDEntity(id), Criterion { + override val uuid: UUID = id.value + override val createdUtc: Instant + get() = parentRubric.createdUtc + override val minPoints: Int by Criteria.minPoints + override val maxPoints: Int by Criteria.maxPoints + override val description: String by Criteria.description + override val parentRubric: Rubric by DBRubric referencedOn Criteria.parentRubricId + override val parentCriterion: DBCriterion? by DBCriterion optionalReferencedOn Criteria.parentCriterionId + override val childCriteria: SizedIterable by DBCriterion optionalReferrersOn Criteria.parentCriterionId + + companion object : EntityClass(Criteria) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/DBModule.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/DBModule.kt new file mode 100644 index 0000000..7a96268 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/DBModule.kt @@ -0,0 +1,79 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.named +import org.koin.core.module.dsl.withOptions +import org.koin.core.qualifier.named +import org.koin.dsl.module +import org.sourcegrade.lab.hub.db.assignment.AssignmentSnapshot +import org.sourcegrade.lab.hub.db.assignment.DBAssignment +import org.sourcegrade.lab.hub.db.assignment.DBAssignmentRepository +import org.sourcegrade.lab.hub.db.course.CourseSnapshot +import org.sourcegrade.lab.hub.db.course.DBCourse +import org.sourcegrade.lab.hub.db.course.DBCourseRepository +import org.sourcegrade.lab.hub.db.user.DBUser +import org.sourcegrade.lab.hub.db.user.DBUserRepository +import org.sourcegrade.lab.hub.db.user.UserSnapshot +import org.sourcegrade.lab.hub.domain.Assignment +import org.sourcegrade.lab.hub.domain.AssignmentRepository +import org.sourcegrade.lab.hub.domain.Course +import org.sourcegrade.lab.hub.domain.CourseRepository +import org.sourcegrade.lab.hub.domain.MutableAssignmentRepository +import org.sourcegrade.lab.hub.domain.MutableCourseRepository +import org.sourcegrade.lab.hub.domain.MutableUserRepository +import org.sourcegrade.lab.hub.domain.User +import org.sourcegrade.lab.hub.domain.UserRepository + +val DBModule = module { + + single> { + EntityConversionContextImpl { user, relations -> UserSnapshot.of(user, relations, get()) } + }.withOptions { + named("user") + } + + single> { + EntityConversionContextImpl(AssignmentSnapshot::of) + }.withOptions { + named("assignment") + } + + single> { + EntityConversionContextImpl { course, relations -> CourseSnapshot.of(course, relations) } + }.withOptions { + named("course") + } + + single { DBUserRepository(get(), get(named("user"))) }.withOptions { + bind() + bind() + } + + single { DBCourseRepository(get(), get(named("course"))) }.withOptions { + bind() + bind() + } + + single { DBAssignmentRepository(get(), get(named("assignment"))) }.withOptions { + bind() + bind() + } +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/EntityConversionContext.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/EntityConversionContext.kt new file mode 100644 index 0000000..c1cd7c8 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/EntityConversionContext.kt @@ -0,0 +1,96 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.dao.load +import org.jetbrains.exposed.dao.with +import org.jetbrains.exposed.sql.SizedIterable +import org.jetbrains.exposed.sql.mapLazy +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.sourcegrade.lab.hub.domain.DomainEntity +import org.sourcegrade.lab.hub.domain.Relation + +interface EntityConversionContext { + + fun createSnapshot(entity: N, relations: Set): E + + fun convertRelation(relation: Relation): Relation + + suspend fun entityConversion( + relations: List> = emptyList(), + statement: ConversionBody, + ): T +} + +typealias ConversionBody = suspend EntityConversion.() -> EntityConversion.BindResult + +class EntityConversionContextImpl( + private val snapshotFun: (N, Set) -> E, +) : EntityConversionContext { + + override fun createSnapshot(entity: N, relations: Set): E = snapshotFun(entity, relations) + + override fun convertRelation(relation: Relation): Relation { + + @Suppress("UNCHECKED_CAST") + return (relation as Relation) + } + + override suspend fun entityConversion( + relations: List>, + statement: ConversionBody, + ): T { + val ec = EntityConversion(context = this, relations) + return newSuspendedTransaction { statement(ec).result } + } +} + +@DslMarker +annotation class EntityConversionDsl + +class EntityConversion( + private val context: EntityConversionContext, + relations: List>, +) { + + private val relationSet: Set = relations.map { it.name }.toSet() + private val relationArray: Array> = relations.map { context.convertRelation(it) }.toTypedArray() + private fun N.createSnapshot(): E = context.createSnapshot(this, relationSet) + + @EntityConversionDsl + fun N?.bindNullable(): BindResult = + this?.bind() ?: BindResult(null) + + @EntityConversionDsl + fun N.bind(): BindResult = + BindResult((if (relationArray.isNotEmpty()) load(*relationArray) else this).createSnapshot()) + + @EntityConversionDsl + fun SizedIterable.bindIterable(): BindResult> = + BindResult((if (relationArray.isNotEmpty()) with(*relationArray) else this).mapLazy { it.createSnapshot() }) + + @EntityConversionDsl + fun SizedIterable.bindToList(): BindResult> = bindIterable().result.toList().bindNoop() + + @EntityConversionDsl + fun T.bindNoop(): BindResult = BindResult(this) + + class BindResult(val result: T) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/GradedCriteria.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/GradedCriteria.kt new file mode 100644 index 0000000..2895fd4 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/GradedCriteria.kt @@ -0,0 +1,53 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import kotlinx.datetime.Instant +import org.jetbrains.exposed.dao.Entity +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.UUIDTable +import org.sourcegrade.lab.hub.domain.GradedCriterion +import org.sourcegrade.lab.hub.domain.GradedRubric +import java.util.UUID + +object GradedCriteria : UUIDTable("sgl_graded_criteria") { + val parentGradedRubricId = reference("parent_graded_rubric_id", GradedRubrics) + val parentGradedCriterionId = reference("parent_graded_criterion_id", GradedCriteria).nullable() + val criterionId = reference("criterion_id", Criteria) + val achievedMinPoints = integer("achieved_min_points") + val achievedMaxPoints = integer("achieved_max_points") + val message = text("message") +} + +@GraphQLIgnore +class DBGradedCriterion(id: EntityID) : Entity(id), GradedCriterion { + override val uuid: UUID = id.value + override val createdUtc: Instant + get() = parentGradedRubric.createdUtc + override val parentGradedRubric: GradedRubric by DBGradedRubric referencedOn GradedCriteria.parentGradedRubricId + override val parentGradedCriterion: DBGradedCriterion? by DBGradedCriterion optionalReferencedOn GradedCriteria.parentGradedCriterionId + override val criterion: DBCriterion by DBCriterion referencedOn GradedCriteria.criterionId + override val achievedMinPoints: Int by GradedCriteria.achievedMinPoints + override val achievedMaxPoints: Int by GradedCriteria.achievedMaxPoints + override val message: String by GradedCriteria.message + + companion object : EntityClass(GradedCriteria) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/GradedRubrics.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/GradedRubrics.kt new file mode 100644 index 0000000..902189e --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/GradedRubrics.kt @@ -0,0 +1,55 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.jetbrains.exposed.dao.Entity +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.SizedIterable +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.sourcegrade.lab.hub.domain.GradedRubric +import java.util.UUID + +internal object GradedRubrics : UUIDTable("sgl_graded_rubrics") { + val createdUtc = timestamp("createdUtc").clientDefault { Clock.System.now() } + val rubricId = reference("rubric_id", Rubrics) + val name = varchar("name", 255) + val achievedMinPoints = integer("achieved_min_points") + val achievedMaxPoints = integer("achieved_max_points") +} + +@GraphQLIgnore +class DBGradedRubric(id: EntityID) : Entity(id), GradedRubric { + override val uuid: UUID = id.value + override val createdUtc: Instant by GradedRubrics.createdUtc + override val rubric: DBRubric by DBRubric referencedOn GradedRubrics.rubricId + override val name: String by GradedRubrics.name + override val achievedMinPoints: Int by GradedRubrics.achievedMinPoints + override val achievedMaxPoints: Int by GradedRubrics.achievedMaxPoints + override val allChildCriteria: SizedIterable by DBGradedCriterion referrersOn GradedCriteria.parentGradedRubricId + override val childCriteria: SizedIterable + get() = DBGradedCriterion.find { (GradedCriteria.parentGradedRubricId eq id) and GradedCriteria.parentGradedCriterionId.isNull() } + + companion object : EntityClass(GradedRubrics) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/GradingRuns.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/GradingRuns.kt new file mode 100644 index 0000000..3dffd3c --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/GradingRuns.kt @@ -0,0 +1,63 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.kotlin.datetime.duration +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.sourcegrade.lab.hub.domain.GradingRun +import java.util.UUID +import kotlin.time.Duration + +internal object GradingRuns : UUIDTable("sgl_grading_runs") { + val createdUtc = timestamp("createdUtc").clientDefault { Clock.System.now() } + val submissionId = reference("submission_id", Submissions) + val gradedRubricId = reference("graded_rubric_id", GradedRubrics) + val runtime = duration("runtime") + + // duplicated data from GradedRubric to minimize joins for frequent queries + val rubricId = reference("rubric_id", Rubrics) + val minPoints = integer("min_points") + val maxPoints = integer("max_points") + val achievedMinPoints = integer("achieved_min_points") + val achievedMaxPoints = integer("achieved_max_points") +} + +@GraphQLIgnore +internal class DBGradingRun(id: EntityID) : UUIDEntity(id), GradingRun { + override val uuid: UUID = id.value + override val createdUtc: Instant by GradingRuns.createdUtc + override val submission: DBSubmission by DBSubmission referencedOn GradingRuns.submissionId + override val gradedRubric: DBGradedRubric by DBGradedRubric referencedOn GradingRuns.gradedRubricId + override val runtime: Duration by GradingRuns.runtime + + override val rubric: DBRubric by DBRubric referencedOn GradingRuns.rubricId + override val minPoints: Int by GradingRuns.minPoints + override val maxPoints: Int by GradingRuns.maxPoints + override val achievedMinPoints: Int by GradingRuns.achievedMinPoints + override val achievedMaxPoints: Int by GradingRuns.achievedMaxPoints + + companion object : EntityClass(GradingRuns) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/ReferenceMutatorDelegate.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/ReferenceMutatorDelegate.kt new file mode 100644 index 0000000..d5a8aa0 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/ReferenceMutatorDelegate.kt @@ -0,0 +1,46 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import org.jetbrains.exposed.dao.Entity +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import kotlin.reflect.KMutableProperty0 +import kotlin.reflect.full.companionObjectInstance + +internal suspend inline fun , reified E : Entity> KMutableProperty0.mutateReference(id: ID): Boolean { + @Suppress("UNCHECKED_CAST") + val entityClass: EntityClass = + checkNotNull(E::class.companionObjectInstance as? EntityClass) { + "Companion EntityClass for ${E::class} not found" + } + + return newSuspendedTransaction { + val entity = entityClass.findById(id) + if (entity == null) { + false + } else { + set(entity) + true + } + } +} + +internal inline fun , reified E : Entity> EntityClass.findByIdNotNull(id: ID): E = + findById(id) ?: error("${E::class.simpleName} with uuid $id not found") diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/RelationOption.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/RelationOption.kt new file mode 100644 index 0000000..c02929c --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/RelationOption.kt @@ -0,0 +1,48 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import org.sourcegrade.lab.hub.domain.DomainEntity +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KClass +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty0 + +internal sealed interface RelationOption : ReadOnlyProperty { + data class Loaded(val value: T) : RelationOption { + override fun getValue(thisRef: DomainEntity, property: KProperty<*>): T = value + } + + data object NotLoaded : RelationOption { + override fun getValue(thisRef: DomainEntity, property: KProperty<*>): Nothing { + throw RelationNotLoadedException(thisRef::class, property.name) + } + } + + companion object { + fun of(property: KProperty0, predicate: (String) -> Boolean): RelationOption = + if (predicate(property.name)) Loaded(property.get()) else NotLoaded + + context(Set) + fun of(property: KProperty0): RelationOption = of(property) { contains(property.name) } + } +} + +class RelationNotLoadedException internal constructor(entity: KClass, relation: String) : + RuntimeException("Relation $relation not loaded for entity ${entity.simpleName}") diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/Rubrics.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/Rubrics.kt new file mode 100644 index 0000000..e031d6b --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/Rubrics.kt @@ -0,0 +1,59 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.SizedIterable +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.sourcegrade.lab.hub.db.assignment.Assignments +import org.sourcegrade.lab.hub.db.assignment.DBAssignment +import org.sourcegrade.lab.hub.domain.Assignment +import org.sourcegrade.lab.hub.domain.Criterion +import org.sourcegrade.lab.hub.domain.Rubric +import java.util.UUID + +internal object Rubrics : UUIDTable("sgl_rubrics") { + val createdUtc = timestamp("createdUtc").clientDefault { Clock.System.now() } + val name = varchar("name", 255) + val assignmentId = reference("assignment_id", Assignments) + val maxPoints = integer("max_points") + val minPoints = integer("min_points") +} + +@GraphQLIgnore +class DBRubric(id: EntityID) : UUIDEntity(id), Rubric { + override val uuid: UUID = id.value + override val createdUtc: Instant by Rubrics.createdUtc + override var name: String by Rubrics.name + override val minPoints: Int by Rubrics.minPoints + override val maxPoints: Int by Rubrics.maxPoints + override val assignment: Assignment by DBAssignment referencedOn Rubrics.assignmentId + override val allChildCriteria: SizedIterable by DBCriterion referrersOn Criteria.parentRubricId + override val childCriteria: SizedIterable + get() = DBCriterion.find { (Criteria.parentRubricId eq id) and Criteria.parentCriterionId.isNull() } + + companion object : EntityClass(Rubrics) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/SubmissionGroupCategories.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/SubmissionGroupCategories.kt new file mode 100644 index 0000000..0ef86ee --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/SubmissionGroupCategories.kt @@ -0,0 +1,122 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.apache.logging.log4j.Logger +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.SizedIterable +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.sourcegrade.lab.hub.db.course.Courses +import org.sourcegrade.lab.hub.db.course.DBCourse +import org.sourcegrade.lab.hub.domain.DomainEntityCollection +import org.sourcegrade.lab.hub.domain.MutableRepository +import org.sourcegrade.lab.hub.domain.MutableSubmissionGroupCategoryRepository +import org.sourcegrade.lab.hub.domain.Relation +import org.sourcegrade.lab.hub.domain.Repository +import org.sourcegrade.lab.hub.domain.SizedIterableCollection +import org.sourcegrade.lab.hub.domain.SubmissionGroupCategory +import org.sourcegrade.lab.hub.domain.SubmissionGroupCategoryCollection +import java.util.UUID + +internal object SubmissionGroupCategories : UUIDTable("sgl_submission_group_categories") { + val createdUtc = timestamp("createdUtc").clientDefault { Clock.System.now() } + val name = varchar("name", 255).uniqueIndex() + val courseId = reference("course_id", Courses) + val minSize = integer("min_size") + val maxSize = integer("max_size") +} + +@GraphQLIgnore +internal class DBSubmissionGroupCategory(id: EntityID) : UUIDEntity(id), SubmissionGroupCategory { + override val uuid: UUID = id.value + override val createdUtc: Instant by SubmissionGroupCategories.createdUtc + override var course: DBCourse by DBCourse referencedOn SubmissionGroupCategories.courseId + override var name: String by SubmissionGroupCategories.name + override var minSize: Int by SubmissionGroupCategories.minSize + override var maxSize: Int by SubmissionGroupCategories.maxSize + + companion object : EntityClass(SubmissionGroupCategories) +} + +internal class SubmissionGroupCategoryRepository( + private val logger: Logger, + private val conversionContext: EntityConversionContext, +) : MutableSubmissionGroupCategoryRepository, + Repository by UUIDEntityClassRepository(DBSubmissionGroupCategory, conversionContext), + EntityConversionContext by conversionContext { + + override suspend fun findByName(name: String, relations: List>): SubmissionGroupCategory? = + entityConversion(relations) { + DBSubmissionGroupCategory.find { SubmissionGroupCategories.name eq name }.firstOrNull().bindNullable() + } + + override suspend fun findAllByName( + partialName: String, + limit: DomainEntityCollection.Limit?, + orders: List, + ): SubmissionGroupCategoryCollection = + DBSubmissionGroupCategoryCollection(conversionContext, limit, orders) { + DBSubmissionGroupCategory.find { SubmissionGroupCategories.name like "%$partialName%" }.bindIterable() + } + + override suspend fun findAll( + limit: DomainEntityCollection.Limit?, + orders: List, + ): SubmissionGroupCategoryCollection = + DBSubmissionGroupCategoryCollection(conversionContext, limit, orders) { + DBSubmissionGroupCategory.all().bindIterable() + } + + override suspend fun create( + item: SubmissionGroupCategory.CreateDto, + relations: List>, + ): SubmissionGroupCategory = entityConversion(relations) { + val itemCourse = DBCourse.findByIdNotNull(item.courseUuid) + DBSubmissionGroupCategory.new { + course = itemCourse + name = item.name + minSize = item.minSize + maxSize = item.maxSize + }.also { + logger.info("Created new SubmissionGroupCategory ${it.id} with data $item") + }.bind() + } + + override suspend fun put( + item: SubmissionGroupCategory.CreateDto, + relations: List>, + ): MutableRepository.PutResult { + TODO("Not yet implemented") + } +} + +internal class DBSubmissionGroupCategoryCollection( + private val conversionContext: EntityConversionContext, + private val limit: DomainEntityCollection.Limit?, + private val orders: List, + private val body: ConversionBody>, +) : SubmissionGroupCategoryCollection, + DomainEntityCollection + by SizedIterableCollection(SubmissionGroupCategories, conversionContext, limit, orders, body) diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/SubmissionGroupMemberships.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/SubmissionGroupMemberships.kt new file mode 100644 index 0000000..2e086ad --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/SubmissionGroupMemberships.kt @@ -0,0 +1,52 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import kotlinx.datetime.Clock +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.sourcegrade.lab.hub.db.user.DBUser +import org.sourcegrade.lab.hub.db.user.Users +import org.sourcegrade.lab.hub.domain.SubmissionGroup +import org.sourcegrade.lab.hub.domain.UserMembership +import java.util.UUID + +internal object SubmissionGroupMemberships : UUIDTable("sgl_submission_group_membership") { + val createdUtc = timestamp("createdUtc").clientDefault { Clock.System.now() } + val startUtc = timestamp("startUtc") + val endUtc = timestamp("endUtc").nullable() + val userId = reference("user_id", Users) + val submissionGroupId = reference("submission_group_id", SubmissionGroups) +} + +@GraphQLIgnore +internal class DBSubmissionGroupMembership(id: EntityID) : UUIDEntity(id), UserMembership { + override val uuid: UUID = id.value + override val createdUtc by SubmissionGroupMemberships.createdUtc + override val startUtc by SubmissionGroupMemberships.startUtc + override val endUtc by SubmissionGroupMemberships.endUtc + override val user: DBUser by DBUser referencedOn SubmissionGroupMemberships.userId + override val target: DBSubmissionGroup by DBSubmissionGroup referencedOn SubmissionGroupMemberships.submissionGroupId + + companion object : EntityClass(SubmissionGroupMemberships) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/SubmissionGroups.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/SubmissionGroups.kt new file mode 100644 index 0000000..e60b10a --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/SubmissionGroups.kt @@ -0,0 +1,52 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.SizedIterable +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.sourcegrade.lab.hub.db.user.DBUser +import org.sourcegrade.lab.hub.domain.SubmissionGroup +import org.sourcegrade.lab.hub.domain.Term +import java.util.UUID + +internal object SubmissionGroups : UUIDTable("sgl_submission_groups") { + val createdUtc = timestamp("createdUtc").clientDefault { Clock.System.now() } + val name = varchar("name", 255).uniqueIndex() + val categoryId = reference("category_id", SubmissionGroupCategories) +} + +@GraphQLIgnore +internal class DBSubmissionGroup(id: EntityID) : UUIDEntity(id), SubmissionGroup { + override val uuid: UUID = id.value + override val createdUtc: Instant by SubmissionGroups.createdUtc + override val term: Term + get() = category.course.term + override val name: String by SubmissionGroups.name + override val category: DBSubmissionGroupCategory by DBSubmissionGroupCategory referencedOn SubmissionGroups.categoryId + override val members: SizedIterable by DBUser via SubmissionGroupMemberships + + companion object : EntityClass(SubmissionGroups) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/Submissions.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/Submissions.kt new file mode 100644 index 0000000..f35d9a5 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/Submissions.kt @@ -0,0 +1,61 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.SizedIterable +import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.sourcegrade.lab.hub.db.assignment.Assignments +import org.sourcegrade.lab.hub.db.assignment.DBAssignment +import org.sourcegrade.lab.hub.db.user.DBUser +import org.sourcegrade.lab.hub.db.user.Users +import org.sourcegrade.lab.hub.domain.Submission +import java.util.UUID + +internal object Submissions : UUIDTable("sgl_submissions") { + val createdUtc = timestamp("createdUtc").clientDefault { Clock.System.now() } + val assignmentId = reference("assignment_id", Assignments) + val submitterId = reference("submitter_id", Users) + val groupId = reference("group_id", SubmissionGroups) + val uploaded = timestamp("uploaded") +} + +@GraphQLIgnore +internal class DBSubmission(id: EntityID) : UUIDEntity(id), Submission { + override val uuid: UUID = id.value + override val createdUtc: Instant by Submissions.createdUtc + override val assignment: DBAssignment by DBAssignment referencedOn Submissions.assignmentId + override val submitter: DBUser by DBUser referencedOn Submissions.submitterId + override val group: DBSubmissionGroup by DBSubmissionGroup referencedOn Submissions.groupId + override val uploaded: Instant by Submissions.uploaded + override val gradingRuns: SizedIterable by DBGradingRun referrersOn GradingRuns.submissionId + override val lastGradingRun: DBGradingRun? + get() = DBGradingRun.find { GradingRuns.submissionId eq id } + .orderBy(GradingRuns.createdUtc to SortOrder.DESC) + .firstOrNull() + + companion object : EntityClass(Submissions) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/Terms.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/Terms.kt new file mode 100644 index 0000000..a6ffce7 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/Terms.kt @@ -0,0 +1,54 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import kotlinx.datetime.Instant +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.sourcegrade.lab.hub.domain.Term +import java.util.UUID + +internal object Terms : UUIDTable("sgl_terms") { + val name = varchar("name", 255).uniqueIndex() + val createdUtc = timestamp("createdUtc") + val start = timestamp("start") + val end = timestamp("end") +} + +@GraphQLIgnore +internal class DBTerm(id: EntityID) : UUIDEntity(id), Term { + override val uuid: UUID = id.value + override val createdUtc: Instant by Terms.createdUtc + override val name: String by Terms.name + override val start: Instant by Terms.start + override val end: Instant by Terms.end + + companion object : EntityClass(Terms) +} + +internal fun DBTerm.Companion.getCurrentOrNull(): DBTerm? = + DBTerm.all().orderBy(Terms.start to SortOrder.DESC).firstOrNull() + +internal fun DBTerm.Companion.getCurrent(): DBTerm = + checkNotNull(getCurrentOrNull()) { "No current term found" } diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/UUIDEntityClassRepository.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/UUIDEntityClassRepository.kt new file mode 100644 index 0000000..19aebb5 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/UUIDEntityClassRepository.kt @@ -0,0 +1,49 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db + +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.sourcegrade.lab.hub.domain.DomainEntity +import org.sourcegrade.lab.hub.domain.Relation +import org.sourcegrade.lab.hub.domain.Repository +import java.util.UUID + +internal class UUIDEntityClassRepository( + private val entityClass: EntityClass, + private val conversionContext: EntityConversionContext, +) : Repository, EntityConversionContext by conversionContext { + + override suspend fun findById(id: UUID, relations: List>): E? = + entityConversion(relations) { entityClass.findById(id).bindNullable() } + + override suspend fun deleteById(id: UUID): Boolean = + newSuspendedTransaction { + entityClass.findById(id) + ?.let { it.delete(); true } + ?: false + } + + override suspend fun exists(id: UUID): Boolean = + newSuspendedTransaction { entityClass.findById(id) != null } + + override suspend fun countAll(): Long = + newSuspendedTransaction { entityClass.all().count() } +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/assignment/AssignmentOps.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/assignment/AssignmentOps.kt new file mode 100644 index 0000000..93614a3 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/assignment/AssignmentOps.kt @@ -0,0 +1,26 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db.assignment + +import org.sourcegrade.lab.hub.domain.Assignment +import java.util.UUID + +suspend fun Assignment.setSubmissionGroupCategoryId(id: UUID): Boolean { + TODO("Not yet implemented") +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/assignment/AssignmentSnapshot.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/assignment/AssignmentSnapshot.kt new file mode 100644 index 0000000..27bba6a --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/assignment/AssignmentSnapshot.kt @@ -0,0 +1,58 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db.assignment + +import kotlinx.datetime.Instant +import org.sourcegrade.lab.hub.db.RelationOption +import org.sourcegrade.lab.hub.domain.Assignment +import org.sourcegrade.lab.hub.domain.Course +import org.sourcegrade.lab.hub.domain.SubmissionGroupCategory +import java.util.UUID + +internal class AssignmentSnapshot( + uuidOption: RelationOption, + createdUtcOption: RelationOption, + courseOption: RelationOption, + submissionGroupCategoryOption: RelationOption, + nameOption: RelationOption, + descriptionOption: RelationOption, + submissionDeadlineUtcOption: RelationOption, +) : Assignment { + override val uuid: UUID by uuidOption + override val createdUtc: Instant by createdUtcOption + override val course: Course by courseOption + override val submissionGroupCategory: SubmissionGroupCategory by submissionGroupCategoryOption + override val name: String by nameOption + override val description: String by descriptionOption + override val submissionDeadlineUtc: Instant by submissionDeadlineUtcOption + + companion object { + fun of(assignment: Assignment, relations: Set): AssignmentSnapshot = with(relations) { + AssignmentSnapshot( + RelationOption.of(assignment::uuid), + RelationOption.of(assignment::createdUtc), + RelationOption.of(assignment::course), + RelationOption.of(assignment::submissionGroupCategory), + RelationOption.of(assignment::name), + RelationOption.of(assignment::description), + RelationOption.of(assignment::submissionDeadlineUtc), + ) + } + } +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/assignment/Assignments.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/assignment/Assignments.kt new file mode 100644 index 0000000..37eca04 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/assignment/Assignments.kt @@ -0,0 +1,168 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db.assignment + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.apache.logging.log4j.Logger +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.SizedIterable +import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.jetbrains.exposed.sql.mapLazy +import org.jetbrains.exposed.sql.selectAll +import org.sourcegrade.lab.hub.db.ConversionBody +import org.sourcegrade.lab.hub.db.CourseMemberships +import org.sourcegrade.lab.hub.db.DBSubmissionGroupCategory +import org.sourcegrade.lab.hub.db.EntityConversionContext +import org.sourcegrade.lab.hub.db.SubmissionGroupCategories +import org.sourcegrade.lab.hub.db.Terms +import org.sourcegrade.lab.hub.db.UUIDEntityClassRepository +import org.sourcegrade.lab.hub.db.course.Courses +import org.sourcegrade.lab.hub.db.course.DBCourse +import org.sourcegrade.lab.hub.db.findByIdNotNull +import org.sourcegrade.lab.hub.db.user.membershipStatusPredicate +import org.sourcegrade.lab.hub.domain.Assignment +import org.sourcegrade.lab.hub.domain.AssignmentCollection +import org.sourcegrade.lab.hub.domain.DomainEntityCollection +import org.sourcegrade.lab.hub.domain.MutableAssignment +import org.sourcegrade.lab.hub.domain.MutableAssignmentRepository +import org.sourcegrade.lab.hub.domain.MutableRepository +import org.sourcegrade.lab.hub.domain.Relation +import org.sourcegrade.lab.hub.domain.Repository +import org.sourcegrade.lab.hub.domain.SizedIterableCollection +import org.sourcegrade.lab.hub.domain.Term +import org.sourcegrade.lab.hub.domain.UserMembership +import org.sourcegrade.lab.hub.domain.termPredicate +import java.util.UUID + +internal object Assignments : UUIDTable("sgl_assignments") { + val createdUtc = timestamp("createdUtc").clientDefault { Clock.System.now() } + val courseId = reference("course_id", Courses) + val submissionGroupCategory = reference("submission_group_category", SubmissionGroupCategories.id) + + val name = varchar("name", 255).uniqueIndex() + val description = varchar("description", 16 * 1024) + val submissionDeadline = timestamp("submissionDeadline") +} + +@GraphQLIgnore +internal class DBAssignment(id: EntityID) : UUIDEntity(id), MutableAssignment { + override val uuid: UUID = id.value + override val createdUtc: Instant by Assignments.createdUtc + + override var submissionGroupCategory: DBSubmissionGroupCategory by DBSubmissionGroupCategory referencedOn Assignments.submissionGroupCategory + override var course: DBCourse by DBCourse referencedOn Assignments.courseId + + override var name: String by Assignments.name + override var description: String by Assignments.description + override var submissionDeadlineUtc: Instant by Assignments.submissionDeadline + +// override suspend fun setSubmissionGroupCategoryId(id: UUID): Boolean = ::course.mutateReference(id) + + companion object : EntityClass(Assignments) +} + +internal class DBAssignmentRepository( + private val logger: Logger, + private val conversionContext: EntityConversionContext, +) : MutableAssignmentRepository, Repository by UUIDEntityClassRepository(DBAssignment, conversionContext), + EntityConversionContext by conversionContext { + override suspend fun findAllByCourse( + courseId: UUID, + limit: DomainEntityCollection.Limit?, + orders: List, + ): AssignmentCollection = + DBAssignmentCollection(conversionContext, limit, orders) { DBAssignment.find { Assignments.courseId eq courseId }.bindIterable() } + + override suspend fun findAllByName( + partialName: String, + limit: DomainEntityCollection.Limit?, + orders: List, + ): AssignmentCollection = + DBAssignmentCollection(conversionContext, limit, orders) { + DBAssignment.find { Assignments.name like "%$partialName%" }.bindIterable() + } + + override suspend fun findAllByUser( + userId: UUID, + term: Term.Matcher, + now: Instant, + limit: DomainEntityCollection.Limit?, + orders: List, + ): AssignmentCollection = + DBAssignmentCollection(conversionContext, limit, orders) { + Assignments.innerJoin(Courses).innerJoin(Terms).innerJoin(CourseMemberships).selectAll() + .where { + (CourseMemberships.userId eq userId) + .and(termPredicate(term, now)) + .and(membershipStatusPredicate(UserMembership.UserMembershipStatus.CURRENT, now)) + } + .mapLazy { DBAssignment.wrapRow(it) } + .orderBy(Assignments.submissionDeadline to SortOrder.DESC) + .bindIterable() + } + + override suspend fun findAll( + limit: DomainEntityCollection.Limit?, + orders: List, + ): AssignmentCollection = + DBAssignmentCollection(conversionContext, limit, orders) { DBAssignment.all().bindIterable() } + + override suspend fun create(item: Assignment.CreateAssignmentDto, relations: List>): Assignment = + entityConversion(relations) { + DBAssignment.new { + course = DBCourse.findByIdNotNull(item.courseId) + submissionGroupCategory = DBSubmissionGroupCategory.findByIdNotNull(item.submissionGroupCategoryId) + name = item.name + description = item.description + submissionDeadlineUtc = item.submissionDeadlineUtc + }.also { + logger.info("Created new Assignment ${it.uuid} with data $item") + }.bind() + } + + override suspend fun put( + item: Assignment.CreateAssignmentDto, + relations: List>, + ): MutableRepository.PutResult { + val existingAssignment = + DBAssignment.find { (Assignments.courseId eq item.courseId) and (Assignments.name eq item.name) }.firstOrNull() + return if (existingAssignment == null) { + MutableRepository.PutResult(create(item, relations), created = true) + } else { + logger.info("Loaded existing assignment ${existingAssignment.name} (${existingAssignment.uuid})") + MutableRepository.PutResult(existingAssignment, created = false) + } + } +} + +internal class DBAssignmentCollection( + private val conversionContext: EntityConversionContext, + private val limit: DomainEntityCollection.Limit?, + private val orders: List, + private val body: ConversionBody>, +) : AssignmentCollection, + DomainEntityCollection + by SizedIterableCollection(Assignments, conversionContext, limit, orders, body) diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/course/CourseSnapshot.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/course/CourseSnapshot.kt new file mode 100644 index 0000000..9ac8be7 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/course/CourseSnapshot.kt @@ -0,0 +1,76 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db.course + +import kotlinx.datetime.Instant +import org.sourcegrade.lab.hub.db.RelationOption +import org.sourcegrade.lab.hub.domain.AssignmentCollection +import org.sourcegrade.lab.hub.domain.Course +import org.sourcegrade.lab.hub.domain.DomainEntityCollection +import org.sourcegrade.lab.hub.domain.SubmissionGroupCategoryCollection +import org.sourcegrade.lab.hub.domain.Term +import org.sourcegrade.lab.hub.domain.User +import java.util.UUID + +internal class CourseSnapshot( + uuidOption: RelationOption, + createdUtcOption: RelationOption, + termOption: RelationOption, + submissionGroupCategoriesOption: RelationOption, + displaynameOption: RelationOption, +) : Course { + + override val uuid: UUID by uuidOption + override val createdUtc: Instant by createdUtcOption + override val term: Term by termOption + + override val owner: User + get() = TODO("Not yet implemented") + override val name: String + get() = TODO("Not yet implemented") + override val description: String + get() = TODO("Not yet implemented") + + override fun submissionGroupCategories( + limit: DomainEntityCollection.Limit?, + orders: List, + ): SubmissionGroupCategoryCollection { + TODO("Not yet implemented") + } + + override fun assignments( + limit: DomainEntityCollection.Limit?, + orders: List, + ): AssignmentCollection { + TODO("Not yet implemented") + } + + companion object { + fun of(user: Course, relations: Set): CourseSnapshot = with(relations) { + TODO() +// CourseSnapshot( +// RelationOption.of(user::uuid), +// RelationOption.of(user::createdUtc), +// RelationOption.of(user::email), +// RelationOption.of(user::username), +// RelationOption.of(user::displayname), +// ) + } + } +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/course/Courses.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/course/Courses.kt new file mode 100644 index 0000000..146d76b --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/course/Courses.kt @@ -0,0 +1,149 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db.course + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import com.expediagroup.graphql.generator.execution.OptionalInput +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.apache.logging.log4j.Logger +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.SizedIterable +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.sourcegrade.lab.hub.db.ConversionBody +import org.sourcegrade.lab.hub.db.DBSubmissionGroupCategory +import org.sourcegrade.lab.hub.db.DBSubmissionGroupCategoryCollection +import org.sourcegrade.lab.hub.db.DBTerm +import org.sourcegrade.lab.hub.db.EntityConversionContext +import org.sourcegrade.lab.hub.db.SubmissionGroupCategories +import org.sourcegrade.lab.hub.db.Terms +import org.sourcegrade.lab.hub.db.UUIDEntityClassRepository +import org.sourcegrade.lab.hub.db.assignment.Assignments +import org.sourcegrade.lab.hub.db.assignment.DBAssignment +import org.sourcegrade.lab.hub.db.findByIdNotNull +import org.sourcegrade.lab.hub.db.user.DBUser +import org.sourcegrade.lab.hub.db.user.Users +import org.sourcegrade.lab.hub.domain.Assignment +import org.sourcegrade.lab.hub.domain.AssignmentCollection +import org.sourcegrade.lab.hub.domain.Course +import org.sourcegrade.lab.hub.domain.CourseCollection +import org.sourcegrade.lab.hub.domain.DomainEntityCollection +import org.sourcegrade.lab.hub.domain.MutableCourse +import org.sourcegrade.lab.hub.domain.MutableCourseRepository +import org.sourcegrade.lab.hub.domain.MutableRepository +import org.sourcegrade.lab.hub.domain.Relation +import org.sourcegrade.lab.hub.domain.Repository +import org.sourcegrade.lab.hub.domain.SizedIterableCollection +import org.sourcegrade.lab.hub.domain.SubmissionGroupCategoryCollection +import java.util.UUID + +internal object Courses : UUIDTable("sgl_courses") { + val createdUtc = timestamp("createdUtc").clientDefault { Clock.System.now() } + val name = varchar("name", 255) + val description = varchar("description", 255) + val term = reference("term_id", Terms) + val ownerId = reference("owner_id", Users) +} + +@GraphQLIgnore +internal class DBCourse(id: EntityID) : UUIDEntity(id), MutableCourse { + override val uuid: UUID = id.value + override val createdUtc: Instant by Courses.createdUtc + override var term: DBTerm by DBTerm referencedOn Courses.term + val submissionGroupCategories: SizedIterable by DBSubmissionGroupCategory referrersOn SubmissionGroupCategories.courseId + val assignments: SizedIterable by DBAssignment referrersOn Assignments.courseId + override var owner: DBUser by DBUser referencedOn Courses.ownerId // TODO: Multiple owners + + override var name: String by Courses.name + override var description: String by Courses.description + + override fun submissionGroupCategories( + limit: DomainEntityCollection.Limit?, + orders: List, + ): SubmissionGroupCategoryCollection { + TODO() +// return DBSubmissionGroupCategoryCollection(conversionContext) { +// submissionGroupCategories.bindIterable() +// } + } + + override fun assignments( + limit: DomainEntityCollection.Limit?, + orders: List, + ): AssignmentCollection { + TODO("Not yet implemented") + } + + companion object : EntityClass(Courses) +} + +internal class DBCourseRepository( + private val logger: Logger, + private val conversionContext: EntityConversionContext, +) : MutableCourseRepository, Repository by UUIDEntityClassRepository(DBCourse, conversionContext), + EntityConversionContext by conversionContext { + override suspend fun findAll( + limit: DomainEntityCollection.Limit?, + orders: List, + ): CourseCollection = DBCourseCollection(conversionContext, limit, orders) { DBCourse.all().bindIterable() } + + override suspend fun findByName(name: String, relations: List>): Course? = + entityConversion(relations) { DBCourse.find { Courses.name eq name }.firstOrNull().bindNullable() } + + override suspend fun findAllByName(partialName: String): CourseCollection = + DBCourseCollection(conversionContext) { DBCourse.find { Courses.name like "%$partialName%" }.bindIterable() } + + override suspend fun findAllByDescription(partialDescription: String): CourseCollection = + DBCourseCollection(conversionContext) { DBCourse.find { Courses.description like "%$partialDescription%" }.bindIterable() } + + override suspend fun findAllByOwner(ownerId: UUID): CourseCollection = + DBCourseCollection(conversionContext) { DBCourse.find { Courses.ownerId eq ownerId }.bindIterable() } + + override suspend fun create(item: Course.CreateDto, relations: List>): Course = entityConversion(relations) { + val itemOwner = DBUser.findByIdNotNull(item.ownerUuid) + val itemTerm = DBTerm.findByIdNotNull(item.termUuid) + DBCourse.new { + name = item.name + owner = itemOwner + term = itemTerm + description = if (item.description is OptionalInput.Defined) { + requireNotNull(item.description.value) { "description" } + } else { + item.name + } + }.also { + logger.info("Created course ${it.uuid}") + }.bind() + } + + override suspend fun put(item: Course.CreateDto, relations: List>): MutableRepository.PutResult { + TODO("Not yet implemented") + } +} + +internal class DBCourseCollection( + private val conversionContext: EntityConversionContext, + private val limit: DomainEntityCollection.Limit? = null, + private val orders: List = emptyList(), + private val body: ConversionBody>, +) : CourseCollection, + DomainEntityCollection by SizedIterableCollection(Courses, conversionContext, limit, orders, body) diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/user/UserOps.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/user/UserOps.kt new file mode 100644 index 0000000..7877464 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/user/UserOps.kt @@ -0,0 +1,109 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db.user + +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.jetbrains.exposed.sql.Op +import org.jetbrains.exposed.sql.SizedIterable +import org.jetbrains.exposed.sql.SqlExpressionBuilder +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.innerJoin +import org.jetbrains.exposed.sql.mapLazy +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.sourcegrade.lab.hub.db.CourseMemberships +import org.sourcegrade.lab.hub.db.DBCourseMembership +import org.sourcegrade.lab.hub.db.DBSubmission +import org.sourcegrade.lab.hub.db.DBSubmissionGroupMembership +import org.sourcegrade.lab.hub.db.SubmissionGroupMemberships +import org.sourcegrade.lab.hub.db.SubmissionGroups +import org.sourcegrade.lab.hub.db.Submissions +import org.sourcegrade.lab.hub.db.Terms +import org.sourcegrade.lab.hub.db.course.Courses +import org.sourcegrade.lab.hub.domain.AssignmentCollection +import org.sourcegrade.lab.hub.domain.AssignmentRepository +import org.sourcegrade.lab.hub.domain.Course +import org.sourcegrade.lab.hub.domain.DomainEntityCollection +import org.sourcegrade.lab.hub.domain.Submission +import org.sourcegrade.lab.hub.domain.SubmissionGroup +import org.sourcegrade.lab.hub.domain.Term +import org.sourcegrade.lab.hub.domain.User +import org.sourcegrade.lab.hub.domain.UserMembership +import org.sourcegrade.lab.hub.domain.termPredicate + +internal fun SqlExpressionBuilder.membershipStatusPredicate(status: UserMembership.UserMembershipStatus, now: Instant): Op = + when (status) { + UserMembership.UserMembershipStatus.ALL -> Op.TRUE + UserMembership.UserMembershipStatus.FUTURE -> CourseMemberships.startUtc greater now + UserMembership.UserMembershipStatus.PAST -> CourseMemberships.endUtc less now + UserMembership.UserMembershipStatus.CURRENT -> CourseMemberships.endUtc.isNull() + } + +suspend fun User.assignments( + assignmentRepository: AssignmentRepository, + term: Term.Matcher = Term.Matcher.Current, + now: Instant = Clock.System.now(), + limit: DomainEntityCollection.Limit? = null, + orders: List = emptyList(), +): AssignmentCollection = assignmentRepository.findAllByUser(uuid, term, now, limit, orders) + +suspend fun User.courseMemberships( + status: UserMembership.UserMembershipStatus, + term: Term.Matcher, + now: Instant, +): SizedIterable> = newSuspendedTransaction { + CourseMemberships.innerJoin(Courses).innerJoin(Terms).selectAll() + .where { + (CourseMemberships.userId eq uuid) + .and(termPredicate(term, now)) + .and(membershipStatusPredicate(status, now)) + } + .mapLazy { DBCourseMembership.wrapRow(it) } +} + +suspend fun User.submissionGroupMemberships( + status: UserMembership.UserMembershipStatus, + term: Term.Matcher, + now: Instant, +): SizedIterable> = newSuspendedTransaction { + SubmissionGroupMemberships.innerJoin(SubmissionGroups).innerJoin(Terms).selectAll() + .where { + (CourseMemberships.userId eq uuid) + .and(termPredicate(term, now)) + .and(membershipStatusPredicate(status, now)) + } + .mapLazy { DBSubmissionGroupMembership.wrapRow(it) } +} + +suspend fun User.submissions( + status: Submission.SubmissionStatus, // TODO: Use parameter + term: Term.Matcher, + now: Instant, +): SizedIterable = newSuspendedTransaction { + Submissions.innerJoin(SubmissionGroupMemberships) { (SubmissionGroupMemberships.userId eq uuid) and termPredicate(term, now) } + .selectAll() + .mapLazy { DBSubmission.wrapRow(it) } +} + +// suspend fun assignmentParticipations( +// status: AssignmentParticipation.Status = AssignmentParticipation.Status.OPEN, +// term: Term.Matcher = Term.Matcher.Current, +// now: Instant = Clock.System.now(), +// ): SizedIterable diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/user/UserSnapshot.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/user/UserSnapshot.kt new file mode 100644 index 0000000..a702757 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/user/UserSnapshot.kt @@ -0,0 +1,104 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db.user + +import com.expediagroup.graphql.generator.execution.OptionalInput +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.sourcegrade.lab.hub.db.RelationOption +import org.sourcegrade.lab.hub.domain.AssignmentCollection +import org.sourcegrade.lab.hub.domain.AssignmentRepository +import org.sourcegrade.lab.hub.domain.DomainEntityCollection +import org.sourcegrade.lab.hub.domain.Term +import org.sourcegrade.lab.hub.domain.User +import org.sourcegrade.lab.hub.graphql.flatten +import org.sourcegrade.lab.hub.graphql.flattenList +import java.util.UUID + +internal class UserSnapshot( + private val assignmentRepository: AssignmentRepository, + override val uuid: UUID, + createdUtcOption: RelationOption, + emailOption: RelationOption, + usernameOption: RelationOption, + displaynameOption: RelationOption, +) : User { + + override val createdUtc: Instant by createdUtcOption + override val email: String by emailOption + override val username: String by usernameOption + override val displayname: String by displaynameOption + + override suspend fun assignments( + term: OptionalInput, + now: OptionalInput, + limit: OptionalInput, + orders: OptionalInput>, + ): AssignmentCollection = assignmentRepository.findAllByUser( + uuid, + term = Term.Matcher.fromNullableString(term.flatten()), + now = now.flatten { Clock.System.now() }, + limit = limit.flatten(), + orders = orders.flattenList(), + ) + + // TODO: Split into UserActions + // TODO: Get from UserMembershipRepository +// suspend fun assignments( +// term: String = Term.Matcher.Current.toString(), +// now: Instant = Clock.System.now(), +// ): AssignmentCollection = TODO() +// +// suspend fun courseMemberships( +// status: UserMembership.UserMembershipStatus = UserMembership.UserMembershipStatus.CURRENT, +// term: String = Term.Matcher.Current.toString(), +// now: Instant = Clock.System.now(), +// ): List> = TODO() +// +// suspend fun submissionGroupMemberships( +// status: UserMembership.UserMembershipStatus = UserMembership.UserMembershipStatus.CURRENT, +// term: String = Term.Matcher.Current.toString(), +// now: Instant = Clock.System.now(), +// ): List> = TODO() +// +// suspend fun submissions( +// status: Submission.SubmissionStatus = Submission.SubmissionStatus.ALL, +// term: String = Term.Matcher.Current.toString(), +// now: Instant = Clock.System.now(), +// ): SizedIterable = submissions(status, Term.Matcher.fromString(term), now) + + companion object { + fun of(user: User, relations: Set, assignmentRepository: AssignmentRepository): UserSnapshot = with(relations) { + UserSnapshot( + assignmentRepository, + user.uuid, + RelationOption.of(user::createdUtc), + RelationOption.of(user::email), + RelationOption.of(user::username), + RelationOption.of(user::displayname), + ) + } + } +} + +class PermissibleUser( + delegate: User, subject: Subject, +) : User { + val uuid by delegate.hasPermission("user.uuid", delegate.uuid) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/user/Users.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/user/Users.kt new file mode 100644 index 0000000..4d4c7b0 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/db/user/Users.kt @@ -0,0 +1,130 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.db.user + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import com.expediagroup.graphql.generator.execution.OptionalInput +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.apache.logging.log4j.Logger +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.SizedIterable +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.sourcegrade.lab.hub.db.ConversionBody +import org.sourcegrade.lab.hub.db.EntityConversionContext +import org.sourcegrade.lab.hub.db.UUIDEntityClassRepository +import org.sourcegrade.lab.hub.domain.AssignmentCollection +import org.sourcegrade.lab.hub.domain.DomainEntityCollection +import org.sourcegrade.lab.hub.domain.MutableRepository +import org.sourcegrade.lab.hub.domain.MutableUser +import org.sourcegrade.lab.hub.domain.MutableUserRepository +import org.sourcegrade.lab.hub.domain.Relation +import org.sourcegrade.lab.hub.domain.Repository +import org.sourcegrade.lab.hub.domain.SizedIterableCollection +import org.sourcegrade.lab.hub.domain.User +import org.sourcegrade.lab.hub.domain.UserCollection +import java.util.UUID + +internal object Users : UUIDTable("sgl_users") { + val createdUtc = timestamp("createdUtc").clientDefault { Clock.System.now() } + val email = varchar("email", 255).uniqueIndex() + val username = varchar("username", 255).uniqueIndex() + val displayname = varchar("displayname", 255) +} + +@GraphQLIgnore +internal class DBUser(id: EntityID) : UUIDEntity(id), MutableUser { + override val uuid: UUID = id.value + override val createdUtc: Instant by Users.createdUtc + override var email: String by Users.email + override var username: String by Users.username + override var displayname: String by Users.displayname + + override suspend fun assignments( + term: OptionalInput, + now: OptionalInput, + limit: OptionalInput, + orders: OptionalInput>, + ): AssignmentCollection { + TODO("Not yet implemented") + } + + companion object : EntityClass(Users) +} + +internal class DBUserRepository( + private val logger: Logger, + private val conversionContext: EntityConversionContext, +) : MutableUserRepository, Repository by UUIDEntityClassRepository(DBUser, conversionContext), + EntityConversionContext by conversionContext { + + override suspend fun findByUsername(username: String, relations: List>): User? = + entityConversion(relations) { DBUser.find { Users.username eq username }.firstOrNull().bindNullable() } + + override suspend fun findByEmail(email: String, relations: List>): User? = + entityConversion { DBUser.find { Users.email eq email }.firstOrNull().bindNullable() } + + override suspend fun findAllByUsername(partialUsername: String): UserCollection = + DBUserCollection(conversionContext) { DBUser.find { Users.username like "%$partialUsername%" }.bindIterable() } + + override suspend fun findAll(limit: DomainEntityCollection.Limit?, orders: List): UserCollection = + DBUserCollection(conversionContext, limit, orders) { DBUser.all().bindIterable() } + + override suspend fun create(item: User.CreateUserDto, relations: List>): User = entityConversion(relations) { + DBUser.new { + email = item.email + username = item.username + displayname = if (item.displayname is OptionalInput.Defined) { + requireNotNull(item.displayname.value) { "displayname" } + } else { + item.username + } + }.also { + logger.info("Created new user ${it.uuid} with data $item") + }.bind() + } + + override suspend fun put( + item: User.CreateUserDto, + relations: List>, + ): MutableRepository.PutResult = newSuspendedTransaction { + val existingUser = findByUsername(item.username, relations) + if (existingUser == null) { + MutableRepository.PutResult(create(item, relations), created = true) + } else { + logger.info( + "Loaded existing user ${existingUser.username} (${existingUser.uuid}) with" + + "display name ${existingUser.displayname} and email ${existingUser.email}", + ) + MutableRepository.PutResult(existingUser, created = false) + } + } +} + +internal class DBUserCollection( + private val conversionContext: EntityConversionContext, + private val limit: DomainEntityCollection.Limit? = null, + private val orders: List = emptyList(), + private val body: ConversionBody>, +) : UserCollection, + DomainEntityCollection by SizedIterableCollection(Users, conversionContext, limit, orders, body) diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Assignment.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Assignment.kt new file mode 100644 index 0000000..7d32975 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Assignment.kt @@ -0,0 +1,78 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import graphql.schema.DataFetchingEnvironment +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.sourcegrade.lab.hub.graphql.extractRelations +import java.util.UUID + +interface Assignment : DomainEntity { + val course: Course + val submissionGroupCategory: SubmissionGroupCategory + + val name: String + val description: String + val submissionDeadlineUtc: Instant + + data class CreateAssignmentDto( + val courseId: UUID, + val submissionGroupCategoryId: UUID, + val name: String, + val description: String, + val submissionDeadlineUtc: Instant, + ) : Creates +} + +interface MutableAssignment : Assignment { + override var name: String + override var description: String + override var submissionDeadlineUtc: Instant +} + +interface AssignmentRepository : CollectionRepository { + suspend fun findAllByCourse( + courseId: UUID, + limit: DomainEntityCollection.Limit? = null, + orders: List = emptyList(), + ): AssignmentCollection + + suspend fun findAllByName( + partialName: String, + limit: DomainEntityCollection.Limit? = null, + orders: List = emptyList(), + ): AssignmentCollection + + suspend fun findAllByUser( + userId: UUID, + term: Term.Matcher = Term.Matcher.Current, + now: Instant = Clock.System.now(), + limit: DomainEntityCollection.Limit? = null, + orders: List = emptyList(), + ): AssignmentCollection +} + +interface MutableAssignmentRepository : AssignmentRepository, MutableRepository + +interface AssignmentCollection : DomainEntityCollection { + override suspend fun count(): Long + override suspend fun empty(): Boolean + suspend fun list(dfe: DataFetchingEnvironment): List = list(dfe.extractRelations()) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/AssignmentParticipation.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/AssignmentParticipation.kt new file mode 100644 index 0000000..1c28942 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/AssignmentParticipation.kt @@ -0,0 +1,36 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import org.jetbrains.exposed.sql.SizedIterable + +interface AssignmentParticipation : DomainEntity { + val assignment: Assignment + val user: User + suspend fun submissions(): SizedIterable + + enum class Status { + ALL, + FUTURE, + OPEN, + NOT_SUBMITTED, + SUBMITTED, + EXPIRED, + } +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/CollectionRepository.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/CollectionRepository.kt new file mode 100644 index 0000000..1f0d545 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/CollectionRepository.kt @@ -0,0 +1,44 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +interface CollectionRepository> : Repository { + suspend fun findAll( + limit: DomainEntityCollection.Limit? = null, + orders: List = emptyList(), + ): C +} +// +//data class CollectionParameters( +// val page: OptionalInput, +// val order: List, +//) { +// data class Page(val num: Long, val size: OptionalInput) +// +// data class FieldOrdering(val field: String, val sortOrder: SortOrder = SortOrder.DESC) +// +// enum class SortOrder(val exposed: org.jetbrains.exposed.sql.SortOrder) { +// ASC(org.jetbrains.exposed.sql.SortOrder.ASC), +// DESC(org.jetbrains.exposed.sql.SortOrder.DESC), +// ASC_NULLS_FIRST(org.jetbrains.exposed.sql.SortOrder.ASC_NULLS_FIRST), +// DESC_NULLS_FIRST(org.jetbrains.exposed.sql.SortOrder.DESC_NULLS_FIRST), +// ASC_NULLS_LAST(org.jetbrains.exposed.sql.SortOrder.ASC_NULLS_LAST), +// DESC_NULLS_LAST(org.jetbrains.exposed.sql.SortOrder.DESC_NULLS_LAST) +// } +//} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Course.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Course.kt new file mode 100644 index 0000000..81ae6fb --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Course.kt @@ -0,0 +1,73 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import com.expediagroup.graphql.generator.execution.OptionalInput +import graphql.schema.DataFetchingEnvironment +import org.sourcegrade.lab.hub.graphql.extractRelations +import java.util.UUID + +interface Course : TermScoped { + val owner: User + + val name: String + val description: String + + fun submissionGroupCategories( + limit: DomainEntityCollection.Limit? = null, + orders: List = emptyList(), + ): SubmissionGroupCategoryCollection + + fun assignments( + limit: DomainEntityCollection.Limit? = null, + orders: List = emptyList(), + ): AssignmentCollection + + data class CreateDto( + val ownerUuid: UUID, + val termUuid: UUID, + val name: String, + val description: OptionalInput, + ) : Creates +} + +interface MutableCourse : Course { + override var name: String + override var description: String +} + +interface CourseRepository : CollectionRepository { + + suspend fun findByName(name: String, relations: List> = emptyList()): Course? + + suspend fun findAllByName(partialName: String): CourseCollection + + suspend fun findAllByDescription(partialDescription: String): CourseCollection // TODO: maybe instead CollectionRepo.search? + + suspend fun findAllByOwner(ownerId: UUID): CourseCollection +} + +interface MutableCourseRepository : CourseRepository, MutableRepository + +interface CourseCollection : DomainEntityCollection { + override suspend fun count(): Long + override suspend fun empty(): Boolean + + suspend fun list(dfe: DataFetchingEnvironment): List = list(dfe.extractRelations()) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Criterion.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Criterion.kt new file mode 100644 index 0000000..b03b81b --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Criterion.kt @@ -0,0 +1,30 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import org.jetbrains.exposed.sql.SizedIterable + +interface Criterion : DomainEntity { + val description: String + val minPoints: Int + val maxPoints: Int + val parentRubric: Rubric + val parentCriterion: Criterion? + val childCriteria: SizedIterable +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/DomainEntity.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/DomainEntity.kt new file mode 100644 index 0000000..357a5c9 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/DomainEntity.kt @@ -0,0 +1,37 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import kotlinx.datetime.Instant +import java.util.UUID +import kotlin.reflect.KProperty0 +import kotlin.reflect.KProperty1 + +interface DomainEntity { + val uuid: UUID + val createdUtc: Instant +} + +interface Creates + +interface IdempotentCreates : Creates { + val uuid: UUID +} + +typealias Relation = KProperty1 diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/DomainEntityCollection.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/DomainEntityCollection.kt new file mode 100644 index 0000000..ebf78bc --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/DomainEntityCollection.kt @@ -0,0 +1,89 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import com.expediagroup.graphql.generator.execution.OptionalInput +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.sql.Expression +import org.jetbrains.exposed.sql.SizedIterable +import org.jetbrains.exposed.sql.Table +import org.sourcegrade.lab.hub.db.ConversionBody +import org.sourcegrade.lab.hub.db.EntityConversionContext +import org.jetbrains.exposed.sql.SortOrder as ExposedSortOrder + +@GraphQLIgnore +interface DomainEntityCollection> { + + suspend fun count(): Long + + suspend fun empty(): Boolean + + @GraphQLIgnore + suspend fun list(relations: List> = emptyList()): List + + data class FieldOrdering(val field: String, val sortOrder: SortOrder = SortOrder.DESC) + + enum class SortOrder(val exposed: ExposedSortOrder) { + ASC(ExposedSortOrder.ASC), + DESC(ExposedSortOrder.DESC), + ASC_NULLS_FIRST(ExposedSortOrder.ASC_NULLS_FIRST), + DESC_NULLS_FIRST(ExposedSortOrder.DESC_NULLS_FIRST), + ASC_NULLS_LAST(ExposedSortOrder.ASC_NULLS_LAST), + DESC_NULLS_LAST(ExposedSortOrder.DESC_NULLS_LAST) + } + + data class Limit(val num: Int, val offset: Long) +} + +internal class SizedIterableCollection>( + private val table: Table, + private val conversionContext: EntityConversionContext, + private val limit: DomainEntityCollection.Limit?, + private val orders: List, + private val body: ConversionBody>, +) : DomainEntityCollection, EntityConversionContext by conversionContext { + + override suspend fun list(relations: List>): List { + println("list start: ${Thread.currentThread().name}") + val result = entityConversion(relations) { + body().result + .let { if (limit != null) it.limit(limit.num, limit.offset) else it } + .let { if (orders.isNotEmpty()) it.orderBy(table, orders) else it } + .toList().bindNoop() // eager loading is done in body + } + println("list end: ${Thread.currentThread().name}") + return result + } + + override suspend fun count(): Long = entityConversion { body().result.count().bindNoop() } + + override suspend fun empty(): Boolean = entityConversion { body().result.empty().bindNoop() } +} + +private fun SizedIterable.orderBy( + table: Table, + orders: List, +): SizedIterable = orderBy( + *orders.map { order -> + val field = (table::class.members.find { it.name == order.field }?.call(table) as? Expression<*> + ?: throw NoSuchFieldException("Field '${order.field}' does not exist on table '${table::class.simpleName}'")) + field to order.sortOrder.exposed + }.toTypedArray(), +) diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/GradedCriterion.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/GradedCriterion.kt new file mode 100644 index 0000000..b0581d0 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/GradedCriterion.kt @@ -0,0 +1,28 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +interface GradedCriterion : DomainEntity { + val parentGradedRubric: GradedRubric + val parentGradedCriterion: GradedCriterion? + val criterion: Criterion + val achievedMinPoints: Int + val achievedMaxPoints: Int + val message: String +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/GradedRubric.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/GradedRubric.kt new file mode 100644 index 0000000..28d1862 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/GradedRubric.kt @@ -0,0 +1,30 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import org.jetbrains.exposed.sql.SizedIterable + +interface GradedRubric : DomainEntity { + val rubric: Rubric + val name: String + val achievedMinPoints: Int + val achievedMaxPoints: Int + val allChildCriteria: SizedIterable + val childCriteria: SizedIterable +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/GradingRun.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/GradingRun.kt new file mode 100644 index 0000000..e4785de --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/GradingRun.kt @@ -0,0 +1,33 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import kotlin.time.Duration + +interface GradingRun : DomainEntity { + val submission: Submission + val gradedRubric: GradedRubric + val runtime: Duration + + val rubric: Rubric + val minPoints: Int + val maxPoints: Int + val achievedMinPoints: Int + val achievedMaxPoints: Int +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/MutableRepository.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/MutableRepository.kt new file mode 100644 index 0000000..b616cb8 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/MutableRepository.kt @@ -0,0 +1,29 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore + +@GraphQLIgnore +interface MutableRepository> : Repository { + suspend fun create(item: C, relations: List> = emptyList()): E + suspend fun put(item: C, relations: List> = emptyList()): PutResult + + data class PutResult(val entity: E, val created: Boolean) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Repository.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Repository.kt new file mode 100644 index 0000000..2cc91c3 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Repository.kt @@ -0,0 +1,28 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import java.util.UUID + +interface Repository { + suspend fun findById(id: UUID, relations: List> = emptyList()): E? + suspend fun deleteById(id: UUID): Boolean + suspend fun exists(id: UUID): Boolean + suspend fun countAll(): Long +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Rubric.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Rubric.kt new file mode 100644 index 0000000..3c95c37 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Rubric.kt @@ -0,0 +1,30 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import org.jetbrains.exposed.sql.SizedIterable + +interface Rubric : DomainEntity { + val name: String + val minPoints: Int + val maxPoints: Int + val assignment: Assignment + val allChildCriteria: SizedIterable + val childCriteria: SizedIterable +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Submission.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Submission.kt new file mode 100644 index 0000000..01197e3 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Submission.kt @@ -0,0 +1,50 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import kotlinx.datetime.Instant +import org.jetbrains.exposed.sql.SizedIterable +import java.util.UUID + +interface Submission : DomainEntity { + val assignment: Assignment + val submitter: User + val group: SubmissionGroup + val uploaded: Instant + val gradingRuns: SizedIterable + val lastGradingRun: GradingRun? + + enum class SubmissionStatus { + ALL, + PENDING_GRADE, + GRADED, + } + + data class CreateDto( + val assignmentId: UUID, + val bytes: ByteArray, + ) : Creates +} + +interface SubmissionRepository : Repository { + suspend fun findByAssignment(assignmentId: UUID): SizedIterable + suspend fun findByUserAndAssignment(userId: UUID, assignmentId: UUID): SizedIterable +} + +interface MutableSubmissionRepository : SubmissionRepository, MutableRepository diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/SubmissionGroup.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/SubmissionGroup.kt new file mode 100644 index 0000000..b540e60 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/SubmissionGroup.kt @@ -0,0 +1,27 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import org.jetbrains.exposed.sql.SizedIterable + +interface SubmissionGroup : TermScoped { + val name: String + val category: SubmissionGroupCategory + val members: SizedIterable +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/SubmissionGroupCategory.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/SubmissionGroupCategory.kt new file mode 100644 index 0000000..918d972 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/SubmissionGroupCategory.kt @@ -0,0 +1,55 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import graphql.schema.DataFetchingEnvironment +import org.sourcegrade.lab.hub.graphql.extractRelations +import java.util.UUID + +interface SubmissionGroupCategory : DomainEntity { + val course: Course + var name: String + var minSize: Int + var maxSize: Int + + data class CreateDto( + val courseUuid: UUID, + val name: String, + val minSize: Int, + val maxSize: Int, + ) : Creates +} + +interface SubmissionGroupCategoryRepository : CollectionRepository { + suspend fun findByName(name: String, relations: List> = emptyList()): SubmissionGroupCategory? + suspend fun findAllByName( + partialName: String, + limit: DomainEntityCollection.Limit? = null, + orders: List = emptyList(), + ): SubmissionGroupCategoryCollection +} + +interface MutableSubmissionGroupCategoryRepository : MutableRepository, + SubmissionGroupCategoryRepository + +interface SubmissionGroupCategoryCollection : DomainEntityCollection { + override suspend fun count(): Long + override suspend fun empty(): Boolean + suspend fun list(dfe: DataFetchingEnvironment): List = list(dfe.extractRelations()) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Term.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Term.kt new file mode 100644 index 0000000..0023c9b --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/Term.kt @@ -0,0 +1,99 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import graphql.schema.DataFetchingEnvironment +import kotlinx.datetime.Instant +import org.jetbrains.exposed.sql.Op +import org.jetbrains.exposed.sql.SqlExpressionBuilder +import org.jetbrains.exposed.sql.and +import org.sourcegrade.lab.hub.db.Terms +import org.sourcegrade.lab.hub.graphql.extractRelations +import java.util.UUID + +interface Term : DomainEntity { + val name: String + val start: Instant + val end: Instant? + + data class CreateDto( + val name: String, + val start: Instant, + val end: Instant?, + ) : Creates + + sealed interface Matcher { + data object All : Matcher + data object Current : Matcher + data class ByName(val name: String) : Matcher { + init { + require(name.isNotBlank()) + } + + override fun toString(): String = "ByName($name)" + } + + data class ById(val id: UUID) : Matcher { + override fun toString(): String = "ById($id)" + } + + companion object { + fun fromString(value: String): Matcher = when (value) { + "All" -> All + "Current" -> Current + else -> { + regex.matchEntire(value)?.let { match -> + val type = match.groups["type"]?.value + val param = match.groups["param"]?.value!! + when (type) { + "ByName" -> ByName(param) + "ById" -> ById(UUID.fromString(param)) + else -> throw IllegalArgumentException("Invalid type: $type") + } + } ?: throw IllegalArgumentException("Invalid matcher: $value") + } + } + + fun fromNullableString(value: String?): Matcher = value?.let { fromString(it) } ?: Current + + private val regex = "(?[a-zA-Z])\\((?.+\\))".toRegex() + } + } +} + +interface TermRepository : CollectionRepository { + suspend fun findByName(name: String, relations: List>): Term? + suspend fun findByTime(now: Instant, relations: List>): Term? +} + +interface MutableTermRepository : TermRepository, MutableRepository + +interface TermCollection : DomainEntityCollection { + override suspend fun count(): Long + override suspend fun empty(): Boolean + + suspend fun list(dfe: DataFetchingEnvironment): List = list(dfe.extractRelations()) +} + +internal fun SqlExpressionBuilder.termPredicate(term: Term.Matcher, now: Instant): Op = when (term) { + is Term.Matcher.All -> Op.TRUE + is Term.Matcher.Current -> (Terms.start lessEq now) and (Terms.end greaterEq now) + is Term.Matcher.ByName -> Terms.name.eq(term.name) + is Term.Matcher.ById -> Terms.id.eq(term.id) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/TermScoped.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/TermScoped.kt new file mode 100644 index 0000000..172933f --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/TermScoped.kt @@ -0,0 +1,23 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +interface TermScoped : DomainEntity { + val term: Term +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/User.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/User.kt new file mode 100644 index 0000000..2ade876 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/User.kt @@ -0,0 +1,63 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import com.expediagroup.graphql.generator.execution.OptionalInput +import graphql.schema.DataFetchingEnvironment +import kotlinx.datetime.Instant +import org.sourcegrade.lab.hub.graphql.extractRelations + +interface User : DomainEntity { + val email: String + val username: String + val displayname: String + + suspend fun assignments( + term: OptionalInput, + now: OptionalInput, + limit: OptionalInput, + orders: OptionalInput>, + ): AssignmentCollection + + data class CreateUserDto( + val email: String, + val username: String, + val displayname: OptionalInput = OptionalInput.Defined(username), + ) : Creates +} + +interface MutableUser : User { + override var email: String + override var username: String + override var displayname: String +} + +interface UserRepository : CollectionRepository { + suspend fun findByUsername(username: String, relations: List> = emptyList()): User? + suspend fun findByEmail(email: String, relations: List> = emptyList()): User? + suspend fun findAllByUsername(partialUsername: String): UserCollection +} + +interface MutableUserRepository : UserRepository, MutableRepository + +interface UserCollection : DomainEntityCollection { + override suspend fun count(): Long + override suspend fun empty(): Boolean + suspend fun list(dfe: DataFetchingEnvironment): List = list(dfe.extractRelations()) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/UserMembership.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/UserMembership.kt new file mode 100644 index 0000000..492758a --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/domain/UserMembership.kt @@ -0,0 +1,55 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.domain + +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.jetbrains.exposed.sql.SizedIterable +import java.util.UUID + +interface UserMembership : DomainEntity { + val startUtc: Instant + val endUtc: Instant? + val user: User + val target: T + + data class CreateDto( + val userId: UUID, + val targetId: UUID, + val startUtc: Instant? = null, // or else now + ) : Creates> + + enum class UserMembershipStatus { + ALL, + FUTURE, + CURRENT, + PAST, + } +} + +interface UserMembershipRepository : Repository> { + suspend fun find( + status: UserMembership.UserMembershipStatus = UserMembership.UserMembershipStatus.CURRENT, + term: Term.Matcher = Term.Matcher.Current, + now: Instant = Clock.System.now(), + ): SizedIterable +} + +interface MutableUserMembershipRepository : UserMembershipRepository, + MutableRepository, UserMembership.CreateDto> diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/AssignmentQueries.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/AssignmentQueries.kt new file mode 100644 index 0000000..1a11678 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/AssignmentQueries.kt @@ -0,0 +1,105 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.graphql + +import com.expediagroup.graphql.generator.execution.OptionalInput +import com.expediagroup.graphql.server.operations.Mutation +import com.expediagroup.graphql.server.operations.Query +import graphql.schema.DataFetchingEnvironment +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.apache.logging.log4j.Logger +import org.sourcegrade.lab.hub.domain.Assignment +import org.sourcegrade.lab.hub.domain.AssignmentCollection +import org.sourcegrade.lab.hub.domain.AssignmentRepository +import org.sourcegrade.lab.hub.domain.DomainEntityCollection +import org.sourcegrade.lab.hub.domain.MutableAssignmentRepository +import org.sourcegrade.lab.hub.domain.MutableRepository +import org.sourcegrade.lab.hub.domain.Term +import java.util.UUID + +class AssignmentQueries( + private val logger: Logger, + private val repository: AssignmentRepository, +) : Query { + fun assignment(): AssignmentQuery = AssignmentQuery(logger, repository) +} + +class AssignmentQuery( + private val logger: Logger, + private val repository: AssignmentRepository, +) { + suspend fun findAll( + limit: OptionalInput, + orders: OptionalInput>, + ): AssignmentCollection = repository.findAll(limit = limit.flatten(), orders = orders.flattenList()) + + suspend fun findById(dfe: DataFetchingEnvironment, id: UUID): Assignment? = repository.findById(id, dfe.extractRelations()) + suspend fun deleteById(id: UUID): Boolean = repository.deleteById(id) + suspend fun exists(id: UUID): Boolean = repository.exists(id) + suspend fun countAll(): Long = repository.countAll() + + suspend fun findAllByCourse( + courseId: UUID, + limit: OptionalInput, + orders: OptionalInput>, + ): AssignmentCollection = repository.findAllByCourse(courseId, limit.flatten(), orders.flattenList()) + + suspend fun findAllByName( + partialName: String, + limit: OptionalInput, + orders: OptionalInput>, + ): AssignmentCollection = repository.findAllByName(partialName, limit.flatten(), orders.flattenList()) + + suspend fun findAllByUser( + userId: UUID, + term: OptionalInput, + now: OptionalInput, + limit: OptionalInput, + orders: OptionalInput>, + ): AssignmentCollection = repository.findAllByUser( + userId, + term = Term.Matcher.fromNullableString(term.flatten()), + now = now.flatten { Clock.System.now() }, + limit = limit.flatten(), + orders = orders.flattenList(), + ) +} + +class AssignmentMutations( + private val logger: Logger, + private val repository: MutableAssignmentRepository, +) : Mutation { + fun assignment(): AssignmentMutation = AssignmentMutation(logger, repository) +} + +class AssignmentMutation( + private val logger: Logger, + private val repository: MutableAssignmentRepository, +) { + suspend fun createAssignment(dfe: DataFetchingEnvironment, input: Assignment.CreateAssignmentDto): Assignment = + repository.create(input, dfe.extractRelations()) + + suspend fun put(dfe: DataFetchingEnvironment, item: Assignment.CreateAssignmentDto): AssignmentPutResult = + repository.put(item, dfe.extractRelations()).convert() + + data class AssignmentPutResult(val entity: Assignment, val created: Boolean) + + private fun MutableRepository.PutResult.convert(): AssignmentPutResult = AssignmentPutResult(entity, created) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/CourseQueries.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/CourseQueries.kt new file mode 100644 index 0000000..4a796b3 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/CourseQueries.kt @@ -0,0 +1,75 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.graphql + +import com.expediagroup.graphql.generator.execution.OptionalInput +import com.expediagroup.graphql.server.operations.Mutation +import com.expediagroup.graphql.server.operations.Query +import graphql.schema.DataFetchingEnvironment +import org.apache.logging.log4j.Logger +import org.sourcegrade.lab.hub.domain.Course +import org.sourcegrade.lab.hub.domain.CourseCollection +import org.sourcegrade.lab.hub.domain.CourseRepository +import org.sourcegrade.lab.hub.domain.DomainEntityCollection +import org.sourcegrade.lab.hub.domain.MutableCourseRepository +import org.sourcegrade.lab.hub.domain.MutableRepository +import java.util.UUID + +class CourseQueries( + private val logger: Logger, + private val repository: CourseRepository, +) : Query { + fun course(): CourseQuery = CourseQuery(logger, repository) +} + +class CourseQuery( + private val logger: Logger, + private val repository: CourseRepository, +) { + suspend fun findAll( + limit: OptionalInput, + orders: OptionalInput>, + ): CourseCollection = repository.findAll(limit.flatten(), orders.flattenList()) + + suspend fun findById(dfe: DataFetchingEnvironment, id: UUID): Course? = repository.findById(id, dfe.extractRelations()) + suspend fun exists(id: UUID): Boolean = repository.exists(id) + suspend fun countAll(): Long = repository.countAll() +} + +class CourseMutations( + private val logger: Logger, + private val repository: MutableCourseRepository, +) : Mutation { + fun course(): CourseMutation = CourseMutation(logger, repository) +} + +class CourseMutation( + private val logger: Logger, + private val repository: MutableCourseRepository, +) { + suspend fun create(dfe: DataFetchingEnvironment, item: Course.CreateDto): Course = + repository.create(item, dfe.extractRelations()) + + suspend fun put(dfe: DataFetchingEnvironment, item: Course.CreateDto): PutResult = + repository.put(item, dfe.extractRelations()).convert() + + data class PutResult(val entity: Course, val created: Boolean) + + private fun MutableRepository.PutResult.convert(): PutResult = PutResult(entity, created) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/RelationOps.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/RelationOps.kt new file mode 100644 index 0000000..93271d3 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/RelationOps.kt @@ -0,0 +1,45 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.graphql + +import com.expediagroup.graphql.generator.execution.OptionalInput +import graphql.schema.DataFetchingEnvironment +import org.sourcegrade.lab.hub.domain.DomainEntity +import org.sourcegrade.lab.hub.domain.Relation +import kotlin.reflect.KFunction +import kotlin.reflect.KProperty1 + +internal inline fun DataFetchingEnvironment.extractRelations(): List> = + selectionSet.immediateFields.mapNotNull { field -> coerceRelation(field.name) } + +internal inline fun coerceRelation(relation: String): Relation? = + E::class.members.find { it.name == relation }?.let { prop -> + @Suppress("UNCHECKED_CAST") + when (prop) { + is KProperty1<*, *> -> (prop as KProperty1) + is KFunction<*> -> null + else -> error("No relation $relation found on ${E::class.simpleName}, available relations are: ${E::class.members.map { it.name }}") + } + } + +fun OptionalInput.flatten(): T? = (this as? OptionalInput.Defined)?.value + +fun OptionalInput.flatten(default: () -> T): T = flatten() ?: default() + +fun OptionalInput>.flattenList(): List = (this as? OptionalInput.Defined)?.value ?: emptyList() diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/Scalars.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/Scalars.kt new file mode 100644 index 0000000..36a3c2c --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/Scalars.kt @@ -0,0 +1,122 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.graphql + +import graphql.GraphQLContext +import graphql.execution.CoercedVariables +import graphql.language.IntValue +import graphql.language.StringValue +import graphql.language.Value +import graphql.schema.Coercing +import graphql.schema.GraphQLScalarType +import graphql.schema.GraphQLType +import kotlinx.datetime.Instant +import org.jetbrains.exposed.sql.SizedIterable +import org.jetbrains.exposed.sql.transactions.transaction +import java.util.Locale +import java.util.UUID +import kotlin.reflect.KType +import kotlin.time.Duration + +internal object Scalars { + + fun willGenerateGraphQLType(type: KType): GraphQLType? = + when (type.classifier) { + Instant::class -> instant + UUID::class -> uuid + Long::class -> long + Duration::class -> duration + SizedIterable::class -> sizedIterable + else -> null + } + + private val instant: GraphQLScalarType = GraphQLScalarType.newScalar() + .name("Instant") + .coercing( + object : Coercing { + override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): Instant = + Instant.parse(input as String) + + override fun serialize(dataFetcherResult: Any, graphQLContext: GraphQLContext, locale: Locale): String = + (dataFetcherResult as Instant).toString() + }, + ).build() + + private val uuid = GraphQLScalarType.newScalar() + .name("UUID") + .coercing( + object : Coercing { + override fun parseLiteral( + input: Value<*>, + variables: CoercedVariables, + graphQLContext: GraphQLContext, + locale: Locale, + ): UUID = UUID.fromString((input as StringValue).value) + + override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): UUID = + UUID.fromString(input.toString()) + + override fun serialize(dataFetcherResult: Any, graphQLContext: GraphQLContext, locale: Locale): String = + (dataFetcherResult as UUID).toString() + }, + ).build() + + private val long = GraphQLScalarType.newScalar() + .name("Long") + .coercing( + object : Coercing { + override fun parseLiteral( + input: Value<*>, + variables: CoercedVariables, + graphQLContext: GraphQLContext, + locale: Locale, + ): Long = (input as IntValue).value.toLong() + + override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): Long = + (input as Int).toLong() + + override fun serialize(dataFetcherResult: Any, graphQLContext: GraphQLContext, locale: Locale): String = + dataFetcherResult.toString() + }, + ).build() + + private val duration = GraphQLScalarType.newScalar() + .name("Duration") + .coercing( + object : Coercing { + override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): Duration = + Duration.parse(input as String) + + override fun serialize(dataFetcherResult: Any, graphQLContext: GraphQLContext, locale: Locale): String = + (dataFetcherResult as Duration).toString() + }, + ).build() + + private val sizedIterable = GraphQLScalarType.newScalar() + .name("SizedIterable") + .coercing( + object : Coercing, List<*>> { + override fun serialize(dataFetcherResult: Any, graphQLContext: GraphQLContext, locale: Locale): List<*> { + val result = transaction { (dataFetcherResult as SizedIterable<*>).toList() } + println("SizedIterable serialized to $result") + return result + } + }, + ).build() +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/TermQueries.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/TermQueries.kt new file mode 100644 index 0000000..adc6669 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/TermQueries.kt @@ -0,0 +1,62 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.graphql + +import com.expediagroup.graphql.generator.execution.OptionalInput +import graphql.schema.DataFetchingEnvironment +import org.apache.logging.log4j.Logger +import org.sourcegrade.lab.hub.domain.DomainEntityCollection +import org.sourcegrade.lab.hub.domain.MutableTermRepository +import org.sourcegrade.lab.hub.domain.Term +import org.sourcegrade.lab.hub.domain.TermCollection +import org.sourcegrade.lab.hub.domain.TermRepository +import java.util.UUID + +class TermQueries( + private val logger: Logger, + private val repository: TermRepository, +) { + fun term(): TermQuery = TermQuery(logger, repository) +} + +class TermQuery( + private val logger: Logger, + private val repository: TermRepository, +) { + suspend fun findAll( + limit: OptionalInput, + orders: OptionalInput>, + ): TermCollection = repository.findAll(limit.flatten(), orders.flattenList()) + + suspend fun findById(dfe: DataFetchingEnvironment, id: UUID): Term? = repository.findById(id, dfe.extractRelations()) + suspend fun exists(id: UUID): Boolean = repository.exists(id) + suspend fun countAll(): Long = repository.countAll() +} + +class TermMutations( + private val logger: Logger, + private val repository: MutableTermRepository, +) + +class TermMutation( + private val logger: Logger, + private val repository: MutableTermRepository, +) { + +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/UserQueries.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/UserQueries.kt new file mode 100644 index 0000000..e4c4576 --- /dev/null +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/graphql/UserQueries.kt @@ -0,0 +1,82 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.sourcegrade.lab.hub.graphql + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import com.expediagroup.graphql.generator.execution.OptionalInput +import com.expediagroup.graphql.server.operations.Mutation +import com.expediagroup.graphql.server.operations.Query +import graphql.schema.DataFetchingEnvironment +import org.apache.logging.log4j.Logger +import org.sourcegrade.lab.hub.domain.DomainEntityCollection +import org.sourcegrade.lab.hub.domain.MutableRepository +import org.sourcegrade.lab.hub.domain.MutableUserRepository +import org.sourcegrade.lab.hub.domain.User +import org.sourcegrade.lab.hub.domain.UserCollection +import org.sourcegrade.lab.hub.domain.UserRepository +import java.util.UUID + +class UserQueries( + private val logger: Logger, + private val repository: UserRepository, +) : Query { + fun user(): UserQuery = UserQuery(logger, repository) +} + +@GraphQLDescription("Query user collection") +class UserQuery( + private val logger: Logger, + private val repository: UserRepository, +) { + suspend fun findAll( + limit: OptionalInput, + orders: OptionalInput>, + ): UserCollection = repository.findAll(limit.flatten(), orders.flattenList()) + + suspend fun findById(dfe: DataFetchingEnvironment, id: UUID): User? = repository.findById(id, dfe.extractRelations()) + suspend fun deleteById(id: UUID): Boolean = repository.deleteById(id) + suspend fun exists(id: UUID): Boolean = repository.exists(id) + suspend fun countAll(): Long = repository.countAll() + + suspend fun findByUsername(dfe: DataFetchingEnvironment, username: String): User? = + repository.findByUsername(username, dfe.extractRelations()) + + suspend fun findAllByUsername(dfe: DataFetchingEnvironment, partialUsername: String): UserCollection = + repository.findAllByUsername(partialUsername) +} + +class UserMutations( + private val logger: Logger, + private val repository: MutableUserRepository, +) : Mutation { + fun user(): UserMutation = UserMutation(logger, repository) +} + +@GraphQLDescription("Mutation user collection") +class UserMutation( + private val logger: Logger, + private val repository: MutableUserRepository, +) { + suspend fun create(dfe: DataFetchingEnvironment, item: User.CreateUserDto): User = repository.create(item, dfe.extractRelations()) + suspend fun put(dfe: DataFetchingEnvironment, item: User.CreateUserDto): UserPutResult = repository.put(item, dfe.extractRelations()).convert() + + data class UserPutResult(val entity: User, val created: Boolean) + + private fun MutableRepository.PutResult.convert(): UserPutResult = UserPutResult(entity, created) +} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/http/AuthenticationModule.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/http/AuthenticationModule.kt index 393c5e6..18da5d7 100644 --- a/hub/src/main/kotlin/org/sourcegrade/lab/hub/http/AuthenticationModule.kt +++ b/hub/src/main/kotlin/org/sourcegrade/lab/hub/http/AuthenticationModule.kt @@ -1,3 +1,21 @@ +/* + * Lab - SourceGrade.org + * Copyright (C) 2019-2024 Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + package org.sourcegrade.lab.hub.http import io.ktor.client.HttpClient @@ -39,10 +57,13 @@ import io.ktor.util.pipeline.PipelineContext import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.sourcegrade.lab.hub.models.User -import org.sourcegrade.lab.hub.models.Users +import org.koin.ktor.ext.inject +import org.sourcegrade.lab.hub.domain.MutableUserRepository +import org.sourcegrade.lab.hub.domain.User +import org.sourcegrade.lab.hub.domain.UserRepository +import org.sourcegrade.lab.hub.getEnv import java.io.File +import java.util.UUID import kotlin.collections.set import kotlin.time.Duration.Companion.hours import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation @@ -82,7 +103,7 @@ fun Application.authenticationModule() { } // oidc authentication oauth("Authentik") { - val url = ktorEnv.config.property("ktor.deployment.url").getString() + val url = getEnv("SGL_DEPLOYMENT_URL") urlProvider = { "$url$callback" } @@ -91,18 +112,12 @@ fun Application.authenticationModule() { val oauthSettings = OAuthServerSettings.OAuth2ServerSettings( name = "Authentik", - authorizeUrl = ktorEnv.config.property("ktor.oauth.authorizeUrl") - .getString(), - accessTokenUrl = ktorEnv.config.property("ktor.oauth.accessTokenUrl") - .getString(), + authorizeUrl = getEnv("SGL_AUTH_URL"), + accessTokenUrl = getEnv("SGL_AUTH_ACCESS_TOKEN_URL"), requestMethod = HttpMethod.Post, - clientId = ktorEnv.config.property("ktor.oauth.clientId") - .getString(), - clientSecret = ktorEnv.config.property("ktor.oauth.clientSecret") - .getString(), - defaultScopes = ktorEnv.config.tryGetString("ktor.oauth.scopes") - ?.split(" ") - ?: listOf("openid", "profile", "email"), + clientId = getEnv("SGL_AUTH_CLIENT_ID"), + clientSecret = getEnv("SGL_AUTH_CLIENT_SECRET"), + defaultScopes = getEnv("SGL_AUTH_SCOPES").split(" "), onStateCreated = { call, state -> // saves new state with redirect url value call.request.queryParameters["redirectUrl"]?.let { @@ -114,6 +129,7 @@ fun Application.authenticationModule() { } } routing { + val userRepository = inject().value route("/api/session") { install(ServerContentNegotiation) { json( @@ -140,21 +156,18 @@ fun Application.authenticationModule() { header("Authorization", "Bearer ${principal.accessToken}") }.body() + val createDto = User.CreateUserDto( + email = userInfo.email, + username = userInfo.preferredUsername, + ) + // find user in db - val user = - newSuspendedTransaction { - User.find { Users.email eq userInfo.email }.firstOrNull() - } ?: newSuspendedTransaction { - User.new { - username = userInfo.preferredUsername - email = userInfo.email - } - } + val user = userRepository.put(createDto).entity val session = UserSession( - user.id.value, + user.uuid.toString(), checkNotNull(principal.state) { "No state" }, principal.accessToken, userInfo.email, @@ -176,7 +189,7 @@ fun Application.authenticationModule() { } } get("current-user") { - withUser { call.respond(it.toDTO()) } + withUser(userRepository) { call.respond(it) } // TODO: Conversion to DTO } } } @@ -192,9 +205,9 @@ suspend fun PipelineContext.withUserSession(block: su } } -suspend fun PipelineContext.withUser(block: suspend (User) -> Unit) { +suspend fun PipelineContext.withUser(userRepository: UserRepository, block: suspend (User) -> Unit) { withUserSession { session -> - val user = newSuspendedTransaction { User.findById(session.userId) } + val user = userRepository.findById(UUID.fromString(session.userId),) checkNotNull(user) { "Could not find user ${session.email} in DB" } block(user) } diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/models/CourseMembers.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/models/CourseMembers.kt deleted file mode 100644 index ee03f9b..0000000 --- a/hub/src/main/kotlin/org/sourcegrade/lab/hub/models/CourseMembers.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.sourcegrade.lab.hub.models - -import kotlinx.serialization.Serializable -import org.jetbrains.exposed.dao.EntityClass -import org.jetbrains.exposed.dao.id.EntityID -import org.jetbrains.exposed.sql.ResultRow - -object CourseMembers : Models("course_members") { - val course = reference("course_id", Courses) - val user = reference("user_id", Users) - override val primaryKey = PrimaryKey(course, user, name = "pk_${tableName}_course_user") -} - -@Serializable -class CourseMemberDTO( - val id: String, - val course: String, - val user: String, -) - -class CourseMember(id: EntityID) : Model(id) { - companion object : EntityClass(CourseMembers) - - var course by Course referencedOn CourseMembers.course - var user by User referencedOn CourseMembers.user - - override fun toDTO(): CourseMemberDTO { - return CourseMemberDTO( - this.id.value, - this.course.id.value, - this.user.id.value, - ) - } -} - -fun ResultRow.toCourseMemberDTO(): CourseMemberDTO { - return CourseMemberDTO( - this[CourseMembers.id].value, - this[CourseMembers.course].value, - this[CourseMembers.user].value, - ) -} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/models/Courses.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/models/Courses.kt deleted file mode 100644 index 5c92faa..0000000 --- a/hub/src/main/kotlin/org/sourcegrade/lab/hub/models/Courses.kt +++ /dev/null @@ -1,65 +0,0 @@ -package org.sourcegrade.lab.hub.models - -import kotlinx.serialization.Serializable -import org.jetbrains.exposed.dao.EntityClass -import org.jetbrains.exposed.dao.id.EntityID -import org.jetbrains.exposed.sql.ResultRow -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction - -enum class SemesterType { - WS, - SS, -} - -object Courses : Models("courses") { - val name = varchar("name", 255) - val description = varchar("description", 255) - val semesterType = enumerationByName("semesterType", 2, SemesterType::class) - val semesterStartYear = integer("semesterStartYear") -} - -class Course(id: EntityID) : Model(id) { - companion object : EntityClass(Courses) - - var name by Courses.name - var description by Courses.description - var semesterType by Courses.semesterType - var semesterStartYear by Courses.semesterStartYear - var members by User via CourseMembers - - override fun toDTO(): CourseDTO { - return CourseDTO( - this.id.value, - this.name, - this.description, - this.semesterType, - this.semesterStartYear, - ) - } -} - -@Serializable -class CourseDTO( - val id: String, - val name: String, - val description: String, - val semesterType: SemesterType, - val semesterStartYear: Int, -) { - suspend fun members(): List { - return newSuspendedTransaction { - Course.findById(this@CourseDTO.id)?.members?.map { it.toDTO() } - ?: throw IllegalArgumentException("No Course with id $id found") - } - } -} - -fun ResultRow.toCourseDTO(): CourseDTO { - return CourseDTO( - this[Courses.id].value, - this[Courses.name], - this[Courses.description], - this[Courses.semesterType], - this[Courses.semesterStartYear], - ) -} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/models/DummyDataPopulator.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/models/DummyDataPopulator.kt deleted file mode 100644 index 26e6c2a..0000000 --- a/hub/src/main/kotlin/org/sourcegrade/lab/hub/models/DummyDataPopulator.kt +++ /dev/null @@ -1,102 +0,0 @@ -package org.sourcegrade.lab.hub.models - -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.StdOutSqlLogger -import org.jetbrains.exposed.sql.addLogger -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction - -suspend fun main(args: Array) { - // read HOCON config file - val config = com.typesafe.config.ConfigFactory.load() - val dbConfig = config.getConfig("ktor.db") - Database.connect( - url = dbConfig.getString("url"), - driver = "org.postgresql.Driver", - user = dbConfig.getString("user"), - password = dbConfig.getString("password"), - ) - newSuspendedTransaction { - addLogger(StdOutSqlLogger) - - // delete and re-create tables - val wantedTables = listOf(Users, Courses, CourseMembers) - - val tables = SchemaUtils.listTables() - if (tables.isNotEmpty()) { - SchemaUtils.drop(*wantedTables.toTypedArray()) - } - SchemaUtils.create(*wantedTables.toTypedArray()) - - val dummyUsers = - listOf( - User.new { - username = "John Doe" - email = "test@example.com" -// password = "test" - }, - User.new { - username = "John Smith" - email = "john.smith@aol.com" -// password = "toast" - }, - User.new { - username = "Bernd Scheuert" - email = "b.scheuert@gmail.com" -// password = "meinnameistbernd" - }, - ) - - val dummyCourses = - listOf( - Course.new { - name = "Funktionale und objektorientierte Programmierkonzepte" - description = - "In diesem Kurs lernen Sie die Grundlagen der funktionalen und objektorientierten Programmierung kennen." - semesterType = SemesterType.WS - semesterStartYear = 2023 - }, - Course.new { - name = "Software Engineering" - description = "In diesem Kurs lernen Sie die Grundlagen des Software Engineerings kennen." - semesterType = SemesterType.SS - semesterStartYear = 2023 - }, - Course.new { - name = "Mathematik I für Informatiker" - description = - "In diesem Kurs lernen Sie die Grundlagen der Mathematik kennen. Dazu gehören unter anderem die Grundlagen der Analysis und der Linearen Algebra." - semesterType = SemesterType.WS - semesterStartYear = 2023 - }, - ) - - val dummyCourseMembers = - listOf( - CourseMember.new { - course = dummyCourses[0] - user = dummyUsers[0] - }, - CourseMember.new { - course = dummyCourses[0] - user = dummyUsers[1] - }, - CourseMember.new { - course = dummyCourses[1] - user = dummyUsers[1] - }, - CourseMember.new { - course = dummyCourses[1] - user = dummyUsers[2] - }, - CourseMember.new { - course = dummyCourses[2] - user = dummyUsers[0] - }, - CourseMember.new { - course = dummyCourses[2] - user = dummyUsers[2] - }, - ) - } -} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/models/Models.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/models/Models.kt deleted file mode 100644 index 90cd0f7..0000000 --- a/hub/src/main/kotlin/org/sourcegrade/lab/hub/models/Models.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.sourcegrade.lab.hub.models - -import org.jetbrains.exposed.dao.Entity -import org.jetbrains.exposed.dao.id.EntityID -import org.jetbrains.exposed.dao.id.IdTable -import org.jetbrains.exposed.sql.Column -import java.util.UUID - -open class Models : IdTable { - constructor() : super() - constructor(name: String) : super(name) - - final override val id: Column> = - varchar("id", 36) - .clientDefault { UUID.randomUUID().toString() }.entityId() - override val primaryKey = PrimaryKey(id, name = "pk_${tableName}_id") -} - -abstract class Model(id: EntityID) : Entity(id) { - abstract fun toDTO(): T -} - -// TODO: add https://github.com/JetBrains/Exposed/issues/497#issuecomment-520266191 diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/models/User.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/models/User.kt deleted file mode 100644 index d12d6f9..0000000 --- a/hub/src/main/kotlin/org/sourcegrade/lab/hub/models/User.kt +++ /dev/null @@ -1,52 +0,0 @@ -package org.sourcegrade.lab.hub.models - -import kotlinx.serialization.Serializable -import org.jetbrains.exposed.dao.EntityClass -import org.jetbrains.exposed.dao.id.EntityID -import org.jetbrains.exposed.sql.ResultRow -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction - -object Users : Models("users") { - val email = varchar("email", 255).uniqueIndex() - val username = varchar("username", 255).uniqueIndex() -} - -@Serializable -class UserDTO( - val id: String, - val username: String, - val email: String, -) { - suspend fun courses(): List { - return newSuspendedTransaction { - User.findById(this@UserDTO.id)?.courses?.map { it.toDTO() } - ?: throw IllegalArgumentException("No User with id $id found") - } - } -} - -class User(id: EntityID) : Model(id) { - companion object : EntityClass(Users) - - var username by Users.username - - // var password by Users.password - var email by Users.email - var courses by Course via CourseMembers - - override fun toDTO(): UserDTO { - return UserDTO( - this.id.value, - this.username, - this.email, - ) - } -} - -fun ResultRow.toUserDTO(): UserDTO { - return UserDTO( - this[Users.id].value, - this[Users.username], - this[Users.email], - ) -} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/queries/BasicQuery.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/queries/BasicQuery.kt deleted file mode 100644 index 46fb668..0000000 --- a/hub/src/main/kotlin/org/sourcegrade/lab/hub/queries/BasicQuery.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.sourcegrade.lab.hub.queries - -import graphql.schema.DataFetchingEnvironment -import org.jetbrains.exposed.dao.EntityClass -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.sourcegrade.lab.hub.models.Model - -class BasicQuery(private val entityClass: EntityClass>) { - suspend fun helloWorld(): String { - return "Hello World" - } -} - -// generics not supported by gql :( -abstract class BasicMutation(private val entityClass: EntityClass>) { - protected fun requireId(environment: DataFetchingEnvironment): String { - return environment.executionStepInfo.parent?.arguments?.get("id") as? String - ?: throw IllegalArgumentException("id is required") - } - - suspend fun fetch(environment: DataFetchingEnvironment): T { - val id = requireId(environment) - return newSuspendedTransaction { - entityClass.findById(id)?.toDTO() ?: throw IllegalArgumentException("No user with id $id found") - } - } - - suspend fun delete(environment: DataFetchingEnvironment): T { - val id = requireId(environment) - return newSuspendedTransaction { - entityClass.findById(id)?.apply { - delete() - }?.toDTO() ?: throw IllegalArgumentException("No user with id $id found") - } - } - - abstract suspend fun create( - environment: DataFetchingEnvironment, - input: T, - ): T -} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/queries/CoursEndpoints.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/queries/CoursEndpoints.kt deleted file mode 100644 index 4dabaef..0000000 --- a/hub/src/main/kotlin/org/sourcegrade/lab/hub/queries/CoursEndpoints.kt +++ /dev/null @@ -1,117 +0,0 @@ -package org.sourcegrade.lab.hub.queries - -import com.expediagroup.graphql.generator.annotations.GraphQLDescription -import com.expediagroup.graphql.generator.annotations.GraphQLName -import com.expediagroup.graphql.server.operations.Mutation -import com.expediagroup.graphql.server.operations.Query -import graphql.schema.DataFetchingEnvironment -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.sourcegrade.lab.hub.models.Course -import org.sourcegrade.lab.hub.models.CourseDTO -import org.sourcegrade.lab.hub.models.SemesterType -import org.sourcegrade.lab.hub.models.UserDTO - -@GraphQLDescription("Query collection for Courses") -class CourseQuery { - private fun requireId(environment: DataFetchingEnvironment): String { - return environment.executionStepInfo.parent?.arguments?.get("id") as? String - ?: throw IllegalArgumentException("id is required") - } - - @GraphQLDescription("Get a list of all Courses") - suspend fun fetchAll(): List { - return newSuspendedTransaction { - Course.all().map { it.toDTO() } - } - } - - @GraphQLDescription("Get a single Course by id") - suspend fun fetch(environment: DataFetchingEnvironment): CourseDTO { - val id = requireId(environment) - return newSuspendedTransaction { - Course.findById(id)?.toDTO() ?: throw IllegalArgumentException("No Course with id $id found") - } - } - - @GraphQLDescription("Get the members of a Course by id") - suspend fun members(environment: DataFetchingEnvironment): List { - val id = requireId(environment) - return newSuspendedTransaction { - Course.findById(id)?.members?.toList()?.map { it.toDTO() } ?: throw IllegalArgumentException("No Course with id $id found") - } - } -} - -class CourseQueries : Query { - suspend fun course( - environment: DataFetchingEnvironment, - id: String? = null, - ): CourseQuery { - return CourseQuery() - } -} - -@GraphQLDescription("Mutation collection for Courses") -class CourseMutation { - private fun requireId(environment: DataFetchingEnvironment): String { - return environment.executionStepInfo.parent?.arguments?.get("id") as? String - ?: throw IllegalArgumentException("id is required") - } - - data class CreateCourseInput(val name: String, val description: String, val semesterType: SemesterType, val semesterStartYear: Int) - - @GraphQLDescription("Create a new Course") - suspend fun create( - environment: DataFetchingEnvironment, - input: CreateCourseInput, - ): CourseDTO { - return newSuspendedTransaction { - Course.new { - this.name = input.name - this.description = input.description - this.semesterType = input.semesterType - this.semesterStartYear = input.semesterStartYear - }.toDTO() - } - } - - @GraphQLDescription("Delete a Course by id") - suspend fun delete(environment: DataFetchingEnvironment): CourseDTO { - val id = requireId(environment) - return newSuspendedTransaction { - Course.findById(id)?.apply { - delete() - }?.toDTO() ?: throw IllegalArgumentException("No Course with id $id found") - } - } - - @GraphQLDescription("Get a single Course by id") - suspend fun fetch(environment: DataFetchingEnvironment): CourseDTO { - val id = requireId(environment) - return newSuspendedTransaction { - Course.findById(id)?.toDTO() ?: throw IllegalArgumentException("No Course with id $id found") - } - } - - @GraphQLDescription("Update a Course's Name") - suspend fun updateName( - environment: DataFetchingEnvironment, - @GraphQLName("name") newName: String, - ): String { - val id = requireId(environment) - return newSuspendedTransaction { - Course.findById(id)?.apply { - name = newName - }?.toDTO() ?: throw IllegalArgumentException("No Course with id $id found") - }.name - } -} - -class CourseMutations : Mutation { - suspend fun course( - environment: DataFetchingEnvironment, - id: String? = null, - ): CourseMutation { - return CourseMutation() - } -} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/queries/HelloWorldQuery.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/queries/HelloWorldQuery.kt deleted file mode 100644 index e78a596..0000000 --- a/hub/src/main/kotlin/org/sourcegrade/lab/hub/queries/HelloWorldQuery.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.sourcegrade.lab.hub.queries - -import com.expediagroup.graphql.server.operations.Query - -class HelloWorldQuery : Query { - fun helloWorld(name: String? = null) = "Hello, ${name ?: "World"}!" -} diff --git a/hub/src/main/kotlin/org/sourcegrade/lab/hub/queries/UserEndpoints.kt b/hub/src/main/kotlin/org/sourcegrade/lab/hub/queries/UserEndpoints.kt deleted file mode 100644 index fd5295f..0000000 --- a/hub/src/main/kotlin/org/sourcegrade/lab/hub/queries/UserEndpoints.kt +++ /dev/null @@ -1,127 +0,0 @@ -package org.sourcegrade.lab.hub.queries - -import com.expediagroup.graphql.generator.annotations.GraphQLDescription -import com.expediagroup.graphql.generator.annotations.GraphQLName -import com.expediagroup.graphql.server.operations.Mutation -import com.expediagroup.graphql.server.operations.Query -import graphql.schema.DataFetchingEnvironment -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.sourcegrade.lab.hub.models.CourseDTO -import org.sourcegrade.lab.hub.models.User -import org.sourcegrade.lab.hub.models.UserDTO - -@GraphQLDescription("Query collection for users") -class UserQuery { - private fun requireId(environment: DataFetchingEnvironment): String { - return environment.executionStepInfo.parent?.arguments?.get("id") as? String - ?: throw IllegalArgumentException("id is required") - } - - @GraphQLDescription("Get a list of all users") - suspend fun fetchAll(): List { - return newSuspendedTransaction { - User.all().map { it.toDTO() } - } - } - - @GraphQLDescription("Get a single user by id") - suspend fun fetch(environment: DataFetchingEnvironment): UserDTO { - val id = requireId(environment) - return newSuspendedTransaction { - User.findById(id)?.toDTO() ?: throw IllegalArgumentException("No user with id $id found") - } - } - - @GraphQLDescription("Get the courses of a user by id") - suspend fun courses(environment: DataFetchingEnvironment): List { - val id = requireId(environment) - return newSuspendedTransaction { - User.findById(id)?.courses?.toList()?.map { it.toDTO() } ?: throw IllegalArgumentException("No user with id $id found") - } - } -} - -class UserQueries : Query { - suspend fun user( - environment: DataFetchingEnvironment, - id: String? = null, - ): UserQuery { - return UserQuery() - } -} - -@GraphQLDescription("Mutation collection for users") -class UserMutation { - private fun requireId(environment: DataFetchingEnvironment): String { - return environment.executionStepInfo.parent?.arguments?.get("id") as? String - ?: throw IllegalArgumentException("id is required") - } - - data class CreateUserInput(val email: String, val username: String, val password: String? = null) - - @GraphQLDescription("Create a new user") - suspend fun create( - environment: DataFetchingEnvironment, - input: CreateUserInput, - ): UserDTO { - return newSuspendedTransaction { - User.new { - this.email = input.email - this.username = input.username - }.toDTO() - } - } - - @GraphQLDescription("Delete a user by id") - suspend fun delete(environment: DataFetchingEnvironment): UserDTO { - val id = requireId(environment) - return newSuspendedTransaction { - User.findById(id)?.apply { - delete() - }?.toDTO() ?: throw IllegalArgumentException("No user with id $id found") - } - } - - @GraphQLDescription("Get a single user by id") - suspend fun fetch(environment: DataFetchingEnvironment): UserDTO { - val id = requireId(environment) - return newSuspendedTransaction { - User.findById(id)?.toDTO() ?: throw IllegalArgumentException("No user with id $id found") - } - } - - @GraphQLDescription("Update a user's email") - suspend fun updateEmail( - environment: DataFetchingEnvironment, - @GraphQLName("email") newEmail: String, - ): String { - val id = requireId(environment) - return newSuspendedTransaction { - User.findById(id)?.apply { - email = newEmail - }?.toDTO() ?: throw IllegalArgumentException("No user with id $id found") - }.email - } - - @GraphQLDescription("Update a user's username") - suspend fun updateUsername( - environment: DataFetchingEnvironment, - @GraphQLName("username") newUsername: String, - ): String { - val id = requireId(environment) - return newSuspendedTransaction { - User.findById(id)?.apply { - username = newUsername - }?.toDTO() ?: throw IllegalArgumentException("No user with id $id found") - }.username - } -} - -class UserMutations : Mutation { - suspend fun user( - environment: DataFetchingEnvironment, - id: String? = null, - ): UserMutation { - return UserMutation() - } -} diff --git a/model/src/main/protobuf/criterion.proto b/model/src/main/protobuf/criterion.proto index 4b94f46..5f51a21 100644 --- a/model/src/main/protobuf/criterion.proto +++ b/model/src/main/protobuf/criterion.proto @@ -2,7 +2,6 @@ syntax = "proto3"; package org.sourcegrade.lab.model.rubric; -import "number_range.proto"; import "point_range.proto"; import "test_run.proto"; import "criterion_accumulator.proto"; @@ -17,7 +16,7 @@ message Criterion { // The description of the criterion. ShowcaseString description = 3; // The possible amount of points that can be achieved. - NumberRange possible_points = 4; + PointRange possible_points = 4; // The points that have been achieved. PointRange achieved_points = 5; // The message displayed if the criterion is not fulfilled. diff --git a/model/src/main/protobuf/number_range.proto b/model/src/main/protobuf/number_range.proto deleted file mode 100644 index fd21583..0000000 --- a/model/src/main/protobuf/number_range.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto3"; - -package org.sourcegrade.lab.model.rubric; - -// Defines a range of numbers. The range is inclusive. -message NumberRange { - // Minimum value of the range (inclusive) - int32 min = 1; - // Maximum value of the range (inclusive) - int32 max = 2; -} diff --git a/model/src/main/protobuf/point_range.proto b/model/src/main/protobuf/point_range.proto index 92cef76..1bc7a9b 100644 --- a/model/src/main/protobuf/point_range.proto +++ b/model/src/main/protobuf/point_range.proto @@ -2,14 +2,10 @@ syntax = "proto3"; package org.sourcegrade.lab.model.rubric; -import "number_range.proto"; - -// Defines a range of points. The range is inclusive. +// Defines a range of numbers. The range is inclusive. message PointRange { - oneof value { - // Single point value - int32 single = 1; - // Range of points - NumberRange range = 2; - } + // Minimum value of the range (inclusive) + int32 min = 1; + // Maximum value of the range (inclusive) + int32 max = 2; } diff --git a/model/src/main/protobuf/rubric.proto b/model/src/main/protobuf/rubric.proto index 5f44452..3e1d39a 100644 --- a/model/src/main/protobuf/rubric.proto +++ b/model/src/main/protobuf/rubric.proto @@ -5,7 +5,6 @@ package org.sourcegrade.lab.model.rubric; import "point_range.proto"; import "criterion.proto"; import "showcase_string.proto"; -import "number_range.proto"; // Represents a rubric containing criteria. message Rubric { @@ -16,7 +15,7 @@ message Rubric { // The description of the rubric. ShowcaseString description = 3; // The possible amount of points that can be achieved. - NumberRange possible_points = 4; + PointRange possible_points = 4; // The points that have been achieved. PointRange achieved_points = 5; // The criteria of the rubric.