From 23e7598e9f16c9ee8f0aa80798aa1227b002635d Mon Sep 17 00:00:00 2001 From: Joseph Ivie Date: Thu, 18 Apr 2024 16:12:43 -0600 Subject: [PATCH 1/2] Version updates, socket --- build.gradle.kts | 4 +- ...dpoints.kt => ClientModelRestEndpoints.kt} | 8 +- ...nts.kt => MockClientModelRestEndpoints.kt} | 4 +- .../lightningserver/db/ModelCache.kt | 54 ++-- .../db/ModelRestEndpointsTest.kt | 6 +- demo/build.gradle.kts | 13 +- demo/src/main/kotlin/Server.kt | 240 ++---------------- demo/src/main/kotlin/TestModelEndpoints.kt | 26 -- demo/src/test/kotlin/ServerTest.kt | 2 - gradle.properties | 5 +- gradle/wrapper/gradle-wrapper.properties | 2 +- server-aws/build.gradle.kts | 4 +- .../lightningserver/aws/AwsAdapter.kt | 3 +- server-azure/build.gradle.kts | 4 +- server-core/build.gradle.kts | 4 +- .../lightningserver/cache/DatabaseAsCache.kt | 61 +++++ .../lightningserver/db/ModelRestEndpoints.kt | 32 ++- .../db/ModelRestUpdatesWebsocket.kt | 2 +- .../lightningserver/db/restApiWebsocket.kt | 2 +- server-dynamodb/build.gradle.kts | 4 +- server-mongo/build.gradle.kts | 4 +- server-postgresql/build.gradle.kts | 6 +- .../lightningserver/db/PostgresCollection.kt | 4 +- .../lightningserver/db/returningSupport.kt | 6 +- server-redis/build.gradle.kts | 3 +- server-sentry/build.gradle.kts | 2 +- 26 files changed, 197 insertions(+), 308 deletions(-) rename client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/{ModelRestEndpoints.kt => ClientModelRestEndpoints.kt} (85%) rename client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/{MockModelRestEndpoints.kt => MockClientModelRestEndpoints.kt} (97%) delete mode 100644 demo/src/main/kotlin/TestModelEndpoints.kt create mode 100644 server-core/src/main/kotlin/com/lightningkite/lightningserver/cache/DatabaseAsCache.kt diff --git a/build.gradle.kts b/build.gradle.kts index 29bcf02f..98f0f048 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,10 +15,10 @@ buildscript { } dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") - classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") + classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.20") classpath("com.lightningkite:deploy-helpers:0.0.7") classpath("com.android.tools.build:gradle:7.4.2") - classpath("org.owasp:dependency-check-gradle:9.0.9") + classpath("org.owasp:dependency-check-gradle:9.1.0") classpath("com.github.ben-manes:gradle-versions-plugin:0.51.0") } } diff --git a/client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/ModelRestEndpoints.kt b/client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/ClientModelRestEndpoints.kt similarity index 85% rename from client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/ModelRestEndpoints.kt rename to client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/ClientModelRestEndpoints.kt index 9525b785..71c47a0c 100644 --- a/client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/ModelRestEndpoints.kt +++ b/client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/ClientModelRestEndpoints.kt @@ -5,7 +5,7 @@ import com.lightningkite.kiteui.TypedWebSocket import com.lightningkite.kiteui.reactive.* import kotlinx.serialization.KSerializer -interface ModelRestEndpoints, ID : Comparable> { +interface ClientModelRestEndpoints, ID : Comparable> { suspend fun default(): T = throw IllegalArgumentException() suspend fun query(input: Query): List suspend fun queryPartial(input: QueryPartial): List> @@ -26,11 +26,11 @@ interface ModelRestEndpoints, ID : Comparable> { suspend fun groupAggregate(input: GroupAggregateQuery): Map } -interface ModelRestEndpointsPlusWs, ID : Comparable> : ModelRestEndpoints { +interface ClientModelRestEndpointsPlusWs, ID : Comparable> : ClientModelRestEndpoints { suspend fun watch(): TypedWebSocket, ListChange> } -interface ModelRestEndpointsPlusUpdatesWebsocket, ID : Comparable> : ModelRestEndpoints { +interface ClientModelRestEndpointsPlusUpdatesWebsocket, ID : Comparable> : ClientModelRestEndpoints { suspend fun updates(): TypedWebSocket, CollectionUpdates> } @@ -53,7 +53,7 @@ interface ModelCollection, ID : Comparable> { } interface CachingModelRestEndpoints, ID : Comparable> : ModelCollection { - val skipCache: ModelRestEndpoints + val skipCache: ClientModelRestEndpoints fun totallyInvalidate() } diff --git a/client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/MockModelRestEndpoints.kt b/client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/MockClientModelRestEndpoints.kt similarity index 97% rename from client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/MockModelRestEndpoints.kt rename to client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/MockClientModelRestEndpoints.kt index bfa3e538..27badcab 100644 --- a/client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/MockModelRestEndpoints.kt +++ b/client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/MockClientModelRestEndpoints.kt @@ -6,8 +6,8 @@ import com.lightningkite.kiteui.launchGlobal import com.lightningkite.kiteui.reactive.Constant import com.lightningkite.kiteui.reactive.Readable -class MockModelRestEndpoints, ID : Comparable>(val log: (String) -> Unit) : - ModelRestEndpointsPlusWs, ModelRestEndpointsPlusUpdatesWebsocket { +class MockClientModelRestEndpoints, ID : Comparable>(val log: (String) -> Unit) : + ClientModelRestEndpointsPlusWs, ClientModelRestEndpointsPlusUpdatesWebsocket { val items = HashMap() val watchers = ArrayList<(changes: List>) -> Unit>() override suspend fun query(input: Query): List { diff --git a/client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/ModelCache.kt b/client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/ModelCache.kt index fa5a9c0a..0eb4d0b5 100644 --- a/client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/ModelCache.kt +++ b/client/src/commonMain/kotlin/com/lightningkite/lightningserver/db/ModelCache.kt @@ -10,7 +10,7 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes class ModelCache, ID : Comparable>( - override val skipCache: ModelRestEndpoints, + override val skipCache: ClientModelRestEndpoints, val serializer: KSerializer, cacheTime: Duration = 5.minutes, ) : CachingModelRestEndpoints { @@ -40,7 +40,7 @@ class ModelCache, ID : Comparable>( listeners.toList().forEach { try { it() - } catch(e: Exception) { + } catch (e: Exception) { e.printStackTrace2() } } @@ -57,16 +57,16 @@ class ModelCache, ID : Comparable>( } override val state: ReadableState - get() = if(upToDate) ReadableState(value) else ReadableState.notReady + get() = if (upToDate) ReadableState(value) else ReadableState.notReady override suspend infix fun set(value: T?) { if (value == null) delete() else { val result = skipCache.replace(id, value) this.value = result - for (query in queries) { - query.value.onNewValue(result) - query.value.refreshIfNeeded() + for (query in queries.values.toList()) { + query.onNewValue(result) + query.refreshIfNeeded() } } } @@ -79,10 +79,10 @@ class ModelCache, ID : Comparable>( } // TODO: we can do better than this // val oldValue = value value = result - for (query in queries) { + for (query in queries.values.toList()) { // oldValue?._id?.let { query.value.onRemoved(it) } - result?.let { query.value.onNewValue(it) } - query.value.refreshIfNeeded() + result?.let { query.onNewValue(it) } + query.refreshIfNeeded() } return result } @@ -101,9 +101,9 @@ class ModelCache, ID : Comparable>( // println("Assigned value to null") value = null oldValue?.let { value -> - for (query in queries) { - query.value.onRemoved(id) - query.value.refreshIfNeeded() + for (query in queries.values.toList()) { + query.onRemoved(id) + query.refreshIfNeeded() } } } @@ -111,7 +111,7 @@ class ModelCache, ID : Comparable>( private var totalInvalidation: Double = 0.0 - override fun totallyInvalidate(){ + override fun totallyInvalidate() { totalInvalidation = clockMillis() cache.values.forEach { it.invalidate() } } @@ -144,10 +144,12 @@ class ModelCache, ID : Comparable>( private val listeners = ArrayList<() -> Unit>() val inUse: Boolean get() = listeners.isNotEmpty() override fun addListener(listener: () -> Unit): () -> Unit { + println("Listener to $query added") listeners.add(listener) return { val pos = listeners.indexOfFirst { it === listener } if (pos != -1) { + println("Listener to $query removed") listeners.removeAt(pos) unreportedChanges = true } @@ -160,8 +162,14 @@ class ModelCache, ID : Comparable>( return } if (hasMorePages) { - val lastItem = cache[ids.lastOrNull() ?: return]?.value ?: return - if (comparator.compare(item, lastItem) > 0) return + val lastItem = cache[ids.lastOrNull() ?: run { + return + }]?.value ?: run { + return + } + if (comparator.compare(item, lastItem) > 0) { + return + } } if (item._id in ids) { unreportedChanges = true @@ -206,7 +214,7 @@ class ModelCache, ID : Comparable>( } private val currentSocket: Async?> = asyncGlobal { - (skipCache as? ModelRestEndpointsPlusUpdatesWebsocket)?.updates()?.apply { + (skipCache as? ClientModelRestEndpointsPlusUpdatesWebsocket)?.updates()?.apply { onMessage { it.updates.forEach { new -> cache.getOrPut(new._id) { WritableModelImpl(new._id) }.apply { @@ -227,7 +235,7 @@ class ModelCache, ID : Comparable>( override val connected: Readable get() = it.connected override fun send(condition: Condition) = it.send(condition) } - } ?: (skipCache as? ModelRestEndpointsPlusWs)?.watch()?.apply { + } ?: (skipCache as? ClientModelRestEndpointsPlusWs)?.watch()?.apply { onMessage { val old = it.old val new = it.new @@ -326,7 +334,12 @@ class ModelCache, ID : Comparable>( val id = new._id val impl = cache.getOrPut(id) { WritableModelImpl(id) } impl.value = new - queries.forEach { it.value.onNewValue(new); it.value.refreshIfNeeded() } + println("---INSERT $new---") + queries.forEach { + println("Updating query ${it.key}") + it.value.onNewValue(new) + it.value.refreshIfNeeded() + } return impl } @@ -363,7 +376,7 @@ class ModelCache, ID : Comparable>( var locked = false suspend fun regularly() { - if(locked) return + if (locked) return locked = true try { if (listeningDirty) { @@ -376,8 +389,11 @@ class ModelCache, ID : Comparable>( updateSocket(if (subConditions.isEmpty()) Condition.Never() else Condition.Or(subConditions)) } for (query in queries.values.toList()) { + println("Checkingg query ${query.query}") + println("query.inUse(${query.inUse}) && !query.upToDate(${query.upToDate})") if (query.inUse && !query.upToDate) { skipCache.query(query.query).let { + println("Query got result, applying") for (item in it) cache.getOrPut(item._id) { WritableModelImpl(item._id) }.value = item query.reset(it.map { it._id }) } diff --git a/client/src/commonTest/kotlin/com/lightningkite/lightningserver/db/ModelRestEndpointsTest.kt b/client/src/commonTest/kotlin/com/lightningkite/lightningserver/db/ModelRestEndpointsTest.kt index 1b75eab5..2f05a2af 100644 --- a/client/src/commonTest/kotlin/com/lightningkite/lightningserver/db/ModelRestEndpointsTest.kt +++ b/client/src/commonTest/kotlin/com/lightningkite/lightningserver/db/ModelRestEndpointsTest.kt @@ -26,13 +26,13 @@ class ModelRestEndpointsTest { prepareModels() val collectionsToTest = listOf( MockModelCollection(SampleModel.serializer()), - ModelCache(MockModelRestEndpoints(::println), SampleModel.serializer()), + ModelCache(MockClientModelRestEndpoints(::println), SampleModel.serializer()), ModelCache( - object : ModelRestEndpointsPlusWs by MockModelRestEndpoints(::println) {}, + object : ClientModelRestEndpointsPlusWs by MockClientModelRestEndpoints(::println) {}, SampleModel.serializer() ), ModelCache( - object : ModelRestEndpoints by MockModelRestEndpoints(::println) {}, + object : ClientModelRestEndpoints by MockClientModelRestEndpoints(::println) {}, SampleModel.serializer() ) ) diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index 1cfb0a49..6cd850f4 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -29,7 +29,18 @@ repositories { val ktorVersion:String by project dependencies { - api(project(":server")) + api(project(":server-aws")) + api(project(":server-azure")) + api(project(":server-core")) + api(project(":server-testing")) + api(project(":server-dynamodb")) + api(project(":server-firebase")) + api(project(":server-ktor")) + api(project(":server-memcached")) + api(project(":server-mongo")) + api(project(":server-redis")) + api(project(":server-sentry")) + api(project(":server-sftp")) ksp(project(":processor")) implementation("com.lightningkite:kotliner-cli:1.0.3") implementation("io.ktor:ktor-server-call-logging:$ktorVersion") diff --git a/demo/src/main/kotlin/Server.kt b/demo/src/main/kotlin/Server.kt index 6dbc68fc..c63e4dcf 100644 --- a/demo/src/main/kotlin/Server.kt +++ b/demo/src/main/kotlin/Server.kt @@ -51,233 +51,33 @@ import kotlin.time.Duration import kotlin.random.Random import kotlin.time.Duration.Companion.minutes import com.lightningkite.UUID +import com.lightningkite.lightningserver.exceptions.BadRequestException +import com.lightningkite.lightningserver.http.post +import com.lightningkite.lightningserver.notifications.NotificationSettings +import com.lightningkite.lightningserver.typed.api import com.lightningkite.uuid +import kotlinx.serialization.Serializable object Server : ServerPathGroup(ServerPath.root) { val database = setting("database", DatabaseSettings()) - val email = setting("email", EmailSettings()) - val jwtSigner = setting("jwt", JwtSigner()) - val sms = setting("sms", SMSSettings()) - val files = setting("files", FilesSettings()) - val cache = setting("cache", CacheSettings()) + val notifications = setting("notifications", NotificationSettings()) - init { - Metrics - PostgresDatabase - DynamoDbCache - MongoDatabase - MemcachedCache - SentryExceptionReporter - S3FileSystem - prepareModels() - Tasks.onSettingsReady { - Metrics.main() - println("Files started, got ${files().root.url}") - } - Serialization.handler(FileRedirectHandler) - startupOnce("adminUser", database) { - database().collection().insertOne( - User( - email = "joseph+admin@lightningkite.com", - isSuperUser = true - ) - ) - } - Authentication.isDeveloper = authRequired { - (it.get() as User).isSuperUser - } - Authentication.isSuperUser = authRequired { - (it.get() as User).isSuperUser - } - } - - val userInfo = ModelInfoWithDefault( - getCollection = { - database().collection() - .interceptCreate { it.copy(hashedPassword = it.hashedPassword.secureHash()) } - .interceptModification { - it.map(path().hashedPassword) { - when (it) { - is Modification.Assign -> it.copy(it.value.secureHash()) - else -> throw IllegalStateException() - } - } - } - }, - defaultItem = { User(email = "") }, - forUser = { user -> - val everyone: Condition = Condition.Always() - val self: Condition = condition { it._id eq user._id } - val admin: Condition = if (user.isSuperUser) Condition.Always() else Condition.Never() - withPermissions( - ModelPermissions( - create = everyone, - read = self or admin, - readMask = mask { - it.hashedPassword.maskedTo("MASKED").unless(admin) - }, - update = self or admin, - updateRestrictions = updateRestrictions { - it.isSuperUser.requires(admin) - }, - delete = self or admin - ) - ) - } - ) - val user = object : ServerPathGroup(path("user")) { - val rest = ModelRestEndpoints(path("rest"), userInfo) - } - val auth = object : ServerPathGroup(path("auth")) { - val emailAccess = userInfo.userEmailAccess { User(email = it) } - val passAccess = - userInfo.userPasswordAccess { username, hashed -> User(email = username, hashedPassword = hashed) } - val baseAuth = BaseAuthEndpoints(path, emailAccess, jwtSigner) - val emailAuth = EmailAuthEndpoints(baseAuth, emailAccess, cache, email) - val passAuth = PasswordAuthEndpoints(baseAuth, passAccess) - } - - // val auth2 = object : ServerPathGroup(path("auth2")) { -// val info = ModelInfo( -// getCollection = { database().collection() }, -// forUser = { this } -// ) -// val emailAccess = info.userEmailAccess { UserAlt(email = it) } -// val baseAuth = BaseAuthEndpoints(path, emailAccess, jwtSigner, expiration = 365.days, emailExpiration = 1.hours) -// val emailAuth = EmailAuthEndpoints(baseAuth, emailAccess, cache, email) -// -// init { -// path.docName = "auth2" -// } -// } - val uploadEarly = UploadEarlyEndpoint(path("upload"), files, database) - val testModel = TestModelEndpoints(path("test-model")) - - val root = path.get.handler { - HttpResponse.plainText("Hello ${it.user()}") - } - - val socket = path("socket").websocket( - connect = { println("Connected $it - you are ${it.user()}") }, - message = { - println("Message $it") - it.id.send(it.content) - if (it.content == "die") { - throw Exception("You asked me to die!") - } - }, - disconnect = { println("Disconnect $it") } - ) - - val task = task("Sample Task") { it: Int -> - val id = uuid() - println("Got input $it in the sample task $id") - var value = cache().get("key") - println("From cache is $value for task $id") - delay(1000L) - value = cache().get("key") - println("One second later, from cache is $value for task $id") - println("Finishing sample task $id") - } - - val runTask = path("run-task").get.handler { - val number = Random.nextInt(0, 100) - task(number) - HttpResponse.plainText("OK") - } - - val testPrimitive = path("test-primitive").get.typed( - summary = "Get Test Primitive", - errorCases = listOf(), - implementation = { user: User?, input: Unit -> "42 is great" } - ) - val testObject = path("test-object").get.typed( - summary = "Get Test Object", - errorCases = listOf(), - examples = listOf(ApiExample(input = Unit, output = TestModel())), - implementation = { user: User?, input: Unit -> - TestModel() - } - ) - val die = path("die").get.handler { throw Exception("OUCH") } - - val databaseCheck = path("database-check").get.handler { - HttpResponse.plainText(database().collection()::class.qualifiedName ?: "???") - } - - val testSchedule = schedule("test-schedule", 1.minutes) { - println("Hello schedule!") - } - val testSchedule2 = schedule("test-schedule2", 1.minutes) { - println("Hello schedule 2!") - } - - val hasInternet = path("has-internet").get.handler { - println("Checking for internet...") - val response = client.get("https://lightningkite.com") - HttpResponse.plainText("Got status ${response.status}") - } - - val dieSlowly = path("die-slowly").get.handler { - Thread.sleep(60_000L) - HttpResponse.plainText("Impossible.") + val root = path("/").get.handler { + HttpResponse.plainText("Hello world!") } - - val multiplex = path("multiplex").websocket(MultiplexWebSocketHandler(cache)) - - val meta = path("meta").metaEndpoints() - - val weirdAuth = path("weird-auth").get.typed( - summary = "Get weird auth", - errorCases = listOf(), - implementation = { user: RequestAuth, _: Unit -> - "ID is ${user.id}" + val newEndpoint = path("test").post.api( + summary = "Test Endpoint", + authOptions = noAuth, + implementation = { it: TestObj -> + val result = database().collection().insertOne(it) + notifications().send(listOf("target token here"), "title") + return@api result ?: throw BadRequestException() } ) - - val pins = PinHandler(cache, "pins") - val proofPhone = SmsProofEndpoints(path("proof/phone"), pins, sms) - val proofEmail = EmailProofEndpoints(path("proof/email"), pins, email, { to, pin -> - Email( - subject = "Log In Code", - to = listOf(EmailLabeledValue(to)), - plainText = "Your PIN is $pin." - ) - }) - val proofOtp = OneTimePasswordProofEndpoints(path("proof/otp"), database, cache) - val proofPassword = PasswordProofEndpoints(path("proof/password"), database, cache) - val subjects = AuthEndpointsForSubject( - path("subject"), - object : Authentication.SubjectHandler { - override val name: String get() = "User" - override val authType: AuthType get() = AuthType() - override val idSerializer: KSerializer - get() = userInfo.serialization.idSerializer - override val subjectSerializer: KSerializer - get() = userInfo.serialization.serializer - - override suspend fun fetch(id: UUID): User = userInfo.collection().get(id) ?: throw NotFoundException() - override suspend fun findUser(property: String, value: String): User? = when(property) { - "email" -> userInfo.collection().findOne(condition { it.email eq value }) - "_id" -> userInfo.collection().get(uuid(value)) - else -> null - } - override val knownCacheTypes: List> = listOf(EmailCacheKey) - - override suspend fun desiredStrengthFor(result: User): Int = if(result.isSuperUser) Int.MAX_VALUE else 5 - }, - database = database - ) } - -object EmailCacheKey : RequestAuth.CacheKey() { - override val name: String - get() = "email" - override val serializer: KSerializer - get() = String.serializer() - override val validFor: Duration - get() = 5.minutes - - override suspend fun calculate(auth: RequestAuth): String = auth.get().email -} \ No newline at end of file +@Serializable +data class TestObj( + val x: Int, + val y: String +) \ No newline at end of file diff --git a/demo/src/main/kotlin/TestModelEndpoints.kt b/demo/src/main/kotlin/TestModelEndpoints.kt deleted file mode 100644 index 9fe2fe5f..00000000 --- a/demo/src/main/kotlin/TestModelEndpoints.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.lightningkite.lightningserver.demo - -import com.lightningkite.lightningdb.* -import com.lightningkite.lightningserver.auth.AuthOptions -import com.lightningkite.lightningserver.auth.Authentication -import com.lightningkite.lightningserver.auth.RequestAuth -import com.lightningkite.lightningserver.auth.noAuth -import com.lightningkite.lightningserver.core.ServerPath -import com.lightningkite.lightningserver.core.ServerPathGroup -import com.lightningkite.lightningserver.db.* -import com.lightningkite.lightningserver.typed.AuthAccessor -import java.util.* -import kotlin.random.Random - -class TestModelEndpoints(path: ServerPath): ServerPathGroup(path) { - val info = modelInfoWithDefault( - serialization = ModelSerializationInfo(), - authOptions = noAuth, - getBaseCollection = {Server.database().collection()}, - defaultItem = { TestModel() }, - ) - - val rest = ModelRestEndpoints(path("rest"), info) - val restWebsocket = path("rest").restApiWebsocket(Server.database, info) - val dump = ModelDumpEndpoints(path("rest"), info, Authentication.isSuperUser, Server.files, Server.email) -} diff --git a/demo/src/test/kotlin/ServerTest.kt b/demo/src/test/kotlin/ServerTest.kt index bd13fd56..35a60ed1 100644 --- a/demo/src/test/kotlin/ServerTest.kt +++ b/demo/src/test/kotlin/ServerTest.kt @@ -19,12 +19,10 @@ object TestSettings { init { Server Settings.populateDefaults(mapOf( - Server.database.name to DatabaseSettings("ram"), generalSettings.name to GeneralServerSettings( debug = true ) )) - Server.files() engine = UnitTestEngine } } diff --git a/gradle.properties b/gradle.properties index 1137034a..8be3a4d8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,6 +5,5 @@ kotlin.code.style=official kspVersion=1.9.23-1.0.19 kotlinVersion=1.9.23 kotlinXSerialization=1.6.3 -logBack=1.4.14 -coroutines=1.7.3 -ktorVersion=2.3.8 +coroutines=1.8.0 +ktorVersion=2.3.9 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 59bc51a2..48c0a02c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/server-aws/build.gradle.kts b/server-aws/build.gradle.kts index 8900c056..8dae33ed 100644 --- a/server-aws/build.gradle.kts +++ b/server-aws/build.gradle.kts @@ -15,7 +15,7 @@ plugins { val kotlinVersion: String by project val khrysalisVersion: String by project val coroutines: String by project -val awsVersion = "2.23.4" +val awsVersion = "2.25.24" dependencies { api(project(":server-dynamodb")) api(project(":server-core")) @@ -27,7 +27,7 @@ dependencies { api("software.amazon.awssdk:apigatewaymanagementapi:$awsVersion") api("software.amazon.awssdk:cloudwatch:$awsVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutines") - api("com.amazonaws:aws-lambda-java-core:1.2.2") + api("com.amazonaws:aws-lambda-java-core:1.2.3") api("com.amazonaws:aws-lambda-java-events:3.11.4") runtimeOnly("com.amazonaws:aws-lambda-java-log4j2:1.6.0") testImplementation("org.jetbrains.kotlin:kotlin-test-junit") diff --git a/server-aws/src/main/kotlin/com/lightningkite/lightningserver/aws/AwsAdapter.kt b/server-aws/src/main/kotlin/com/lightningkite/lightningserver/aws/AwsAdapter.kt index 1c6ba170..8bdcf25a 100644 --- a/server-aws/src/main/kotlin/com/lightningkite/lightningserver/aws/AwsAdapter.kt +++ b/server-aws/src/main/kotlin/com/lightningkite/lightningserver/aws/AwsAdapter.kt @@ -529,8 +529,7 @@ abstract class AwsAdapter : RequestStreamHandler, Resource { statusCode = HttpStatus.NoContent.code, headers = mapOf( HttpHeader.AccessControlAllowOrigin to (headers[HttpHeader.Origin] ?: "*"), - HttpHeader.AccessControlAllowMethods to (headers[HttpHeader.AccessControlRequestMethod] - ?: "GET"), + HttpHeader.AccessControlAllowMethods to "GET,POST,PUT,PATCH,DELETE,HEAD", HttpHeader.AccessControlAllowHeaders to (cors.allowedHeaders.joinToString(", ")), HttpHeader.AccessControlAllowCredentials to "true", ) diff --git a/server-azure/build.gradle.kts b/server-azure/build.gradle.kts index 26debe5f..a6db5c61 100644 --- a/server-azure/build.gradle.kts +++ b/server-azure/build.gradle.kts @@ -16,8 +16,8 @@ val kotlinVersion: String by project val khrysalisVersion: String by project dependencies { api(project(":server-core")) - api("com.microsoft.azure.functions:azure-functions-java-library:2.0.1") - api("com.azure:azure-storage-blob:12.21.0") + api("com.microsoft.azure.functions:azure-functions-java-library:3.1.0") + api("com.azure:azure-storage-blob:12.25.3") testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation(project(":server-testing")) diff --git a/server-core/build.gradle.kts b/server-core/build.gradle.kts index c6263c69..e9cae016 100644 --- a/server-core/build.gradle.kts +++ b/server-core/build.gradle.kts @@ -28,7 +28,7 @@ dependencies { // implementation("com.fasterxml.jackson.core:jackson-databind:2.13.3") // End Security - implementation("ch.qos.logback:logback-classic:$logBack") + implementation("ch.qos.logback:logback-classic:1.5.3") // implementation("com.lightningkite.khrysalis:jvm-runtime:$khrysalisVersion") @@ -44,7 +44,7 @@ dependencies { api("org.jetbrains.kotlinx:kotlinx-serialization-properties:$kotlinXSerialization") api("org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlinXSerialization") api("io.github.pdvrieze.xmlutil:serialization-jvm:0.86.3") - api("org.mongodb:bson:4.11.1") + api("org.mongodb:bson:5.0.1") api("com.github.jershell:kbson:0.5.0") api("com.charleskorn.kaml:kaml:0.58.0") api(kotlin("reflect")) diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/cache/DatabaseAsCache.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/cache/DatabaseAsCache.kt new file mode 100644 index 00000000..db61eb99 --- /dev/null +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/cache/DatabaseAsCache.kt @@ -0,0 +1,61 @@ +//package com.lightningkite.lightningserver.cache +// +//import com.lightningkite.lightningdb.* +//import com.lightningkite.lightningserver.logger +//import com.lightningkite.lightningserver.serialization.Serialization +//import com.lightningkite.now +//import kotlinx.datetime.Instant +//import kotlinx.serialization.KSerializer +//import kotlinx.serialization.Serializable +//import kotlin.time.Duration +//import java.util.concurrent.ConcurrentHashMap +//import kotlin.time.Duration.Companion.days +// +///** +// * A Cache implementation that exists entirely in the applications Heap. There are no external connections. +// * This is NOT meant for persistent or long term storage. This cache will be completely erased everytime the application is stopped. +// * This is useful in places that persistent data is not needed and speed is desired such as Unit Tests +// */ +//open class DatabaseAsCache(val database: ()->Database, val maxExpiry: Duration = 1.days) : Cache { +// fun collection() = database().collection(CacheEntry.serializer(), "CacheEntry") +// @Suppress("UNCHECKED_CAST") +// override suspend fun get(key: String, serializer: KSerializer): T? { +// return collection().get(key)?.value?.let { Serialization.json.decodeFromString(serializer, it) } +// } +// +// override suspend fun set(key: String, value: T, serializer: KSerializer, timeToLive: Duration?) { +// collection().upsertOneById(key, CacheEntry(key, Serialization.json.encodeToString(serializer, value), now() + (timeToLive ?: maxExpiry))) +// } +// +// override suspend fun setIfNotExists( +// key: String, +// value: T, +// serializer: KSerializer, +// timeToLive: Duration? +// ): Boolean { +// collection().upsertOneById(key, CacheEntry(key, Serialization.json.encodeToString(serializer, value), now() + (timeToLive ?: maxExpiry))) +// } +// +// override suspend fun add(key: String, value: Int, timeToLive: Duration?) { +// val entry = entries[key]?.takeIf { it.expires == null || it.expires > System.currentTimeMillis() } +// val current = entry?.value +// val new = when (current) { +// is Byte -> (current + value).toByte() +// is Short -> (current + value).toShort() +// is Int -> (current + value) +// is Long -> (current + value) +// is Float -> (current + value) +// is Double -> (current + value) +// else -> value +// } +// entries[key] = Entry(new, timeToLive?.inWholeMilliseconds?.let { System.currentTimeMillis() + it }) +// } +// +// override suspend fun remove(key: String) { +// entries.remove(key) +// } +//} +// +//@Serializable +//@GenerateDataClassPaths +//data class CacheEntry(override val _id: String, val value: String, val expires: Instant): HasId \ No newline at end of file diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelRestEndpoints.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelRestEndpoints.kt index 0219c905..eb9608b3 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelRestEndpoints.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelRestEndpoints.kt @@ -41,7 +41,7 @@ open class ModelRestEndpoints?, T : HasId, ID : Comparable("id", info.serialization.idSerializer) val wholePath = TypedServerPath0(path) val bulkPath = TypedServerPath0(path).path("bulk") - val interfaceName = Documentable.InterfaceInfo("ModelRestEndpoints", listOf( + val interfaceName = Documentable.InterfaceInfo("ClientModelRestEndpoints", listOf( info.serialization.serializer, info.serialization.idSerializer )) @@ -280,6 +280,36 @@ open class ModelRestEndpoints?, T : HasId, ID : Comparable)?.let { +// detailPath.path("upsert").patch.api( +// belongsToInterface = interfaceName, +// authOptions = info.authOptions, +// inputType = Modification.serializer(info.serialization.serializer), +// outputType = info.serialization.serializer, +// summary = "Upsert Over Default", +// description = "Creates or updates a ${collectionName} based on the default value", +// errorCases = listOf(), +// examples = exampleItem()?.let { +// sampleModifications().map { mod -> +// ApiExample(mod, mod(it)) +// } +// } ?: listOf(), +// successCode = HttpStatus.Created, +// implementation = { value: Modification -> +// try { +// info.collection(this) +// .upsertOne(Condition.OnField(info.serialization.serializer._id(), Condition.Equal(path1)), value, value(info.defaultItem(this.authOrNull).let { +// info.serialization.serializer._id().setCopy(it, path1) +// })) +// .new +// ?: throw NotFoundException() +// } catch (e: UniqueViolationException) { +// throw BadRequestException(detail = "unique", message = e.message, cause = e) +// } +// } +// ) +// } + // This is used replace many objects at once. This does make individual calls to the database. Kmongo does not have a many replace option. val bulkReplace = wholePath.put.api( belongsToInterface = interfaceName, diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelRestUpdatesWebsocket.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelRestUpdatesWebsocket.kt index 98d5cc91..30f5f812 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelRestUpdatesWebsocket.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelRestUpdatesWebsocket.kt @@ -37,7 +37,7 @@ class ModelRestUpdatesWebsocket?, T : HasId, ID : Comparable< private val modelName = info.serialization.serializer.descriptor.serialName.substringBefore('<').substringAfterLast('.') private val modelIdentifier = info.serialization.serializer.descriptor.serialName private val helper = ModelRestUpdatesWebsocketHelper[database] - private val interfaceName = Documentable.InterfaceInfo("ModelRestEndpointsPlusUpdatesWebsocket", listOf( + private val interfaceName = Documentable.InterfaceInfo("ClientModelRestEndpointsPlusUpdatesWebsocket", listOf( info.serialization.serializer, info.serialization.idSerializer )) diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/restApiWebsocket.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/restApiWebsocket.kt index 216bfeee..8c097100 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/restApiWebsocket.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/restApiWebsocket.kt @@ -35,7 +35,7 @@ fun ?, T : HasId, ID : Comparable> ServerPath.restApiWebs val modelName = info.serialization.serializer.descriptor.serialName.substringBefore('<').substringAfterLast('.') val modelIdentifier = info.serialization.serializer.descriptor.serialName val helper = RestApiWebsocketHelper[database] - val interfaceName = Documentable.InterfaceInfo("ModelRestEndpointsPlusWs", listOf( + val interfaceName = Documentable.InterfaceInfo("ClientModelRestEndpointsPlusWs", listOf( info.serialization.serializer, info.serialization.idSerializer )) diff --git a/server-dynamodb/build.gradle.kts b/server-dynamodb/build.gradle.kts index 0a8f5ca7..58df043b 100644 --- a/server-dynamodb/build.gradle.kts +++ b/server-dynamodb/build.gradle.kts @@ -28,8 +28,8 @@ dependencies { api(project(":server-core")) // api(platform("software.amazon.awssdk:s3:2.17.232")) api("software.amazon.awssdk:dynamodb:2.23.4") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.3") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.8.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.8.0") testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation(project(":server-testing")) ksp(project(":processor")) diff --git a/server-mongo/build.gradle.kts b/server-mongo/build.gradle.kts index 75abcd2b..686b8c9b 100644 --- a/server-mongo/build.gradle.kts +++ b/server-mongo/build.gradle.kts @@ -20,8 +20,8 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines") - implementation("de.flapdoodle.embed:de.flapdoodle.embed.mongo:4.12.2") - api("org.mongodb:mongodb-driver-kotlin-coroutine:4.11.1") + implementation("de.flapdoodle.embed:de.flapdoodle.embed.mongo:4.12.3") + api("org.mongodb:mongodb-driver-kotlin-coroutine:5.0.1") kspTest(project(":processor")) testImplementation(project(":server-testing")) diff --git a/server-postgresql/build.gradle.kts b/server-postgresql/build.gradle.kts index 6b301271..712554b4 100644 --- a/server-postgresql/build.gradle.kts +++ b/server-postgresql/build.gradle.kts @@ -23,17 +23,17 @@ repositories { val kotlinVersion: String by project val khrysalisVersion: String by project -val exposedVersion = "0.39.2" +val exposedVersion = "0.49.0" dependencies { api(project(":server-core")) api("org.jetbrains.exposed:exposed-core:$exposedVersion") api("org.jetbrains.exposed:exposed-java-time:$exposedVersion") api("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") - api("org.postgresql:postgresql:42.7.2") + api("org.postgresql:postgresql:42.7.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation(project(":server-testing")) - testImplementation("io.zonky.test:embedded-postgres:2.0.5") + testImplementation("io.zonky.test:embedded-postgres:2.0.6") ksp(project(":processor")) kspTest(project(":processor")) } diff --git a/server-postgresql/src/main/kotlin/com/lightningkite/lightningserver/db/PostgresCollection.kt b/server-postgresql/src/main/kotlin/com/lightningkite/lightningserver/db/PostgresCollection.kt index 6dc958eb..6d4dc8a3 100644 --- a/server-postgresql/src/main/kotlin/com/lightningkite/lightningserver/db/PostgresCollection.kt +++ b/server-postgresql/src/main/kotlin/com/lightningkite/lightningserver/db/PostgresCollection.kt @@ -250,7 +250,7 @@ class PostgresCollection( return t { table.deleteWhere( limit = 1, - op = { condition(condition, serializer, table).asOp() } + op = { it.condition(condition, serializer, table).asOp() } ) > 0 } } @@ -266,7 +266,7 @@ class PostgresCollection( override suspend fun deleteManyIgnoringOld(condition: Condition): Int { return t { table.deleteWhere( - op = { condition(condition, serializer, table).asOp() } + op = { it.condition(condition, serializer, table).asOp() } ) } } diff --git a/server-postgresql/src/main/kotlin/com/lightningkite/lightningserver/db/returningSupport.kt b/server-postgresql/src/main/kotlin/com/lightningkite/lightningserver/db/returningSupport.kt index 22a91822..8e4f0374 100644 --- a/server-postgresql/src/main/kotlin/com/lightningkite/lightningserver/db/returningSupport.kt +++ b/server-postgresql/src/main/kotlin/com/lightningkite/lightningserver/db/returningSupport.kt @@ -58,7 +58,7 @@ class DeleteReturningStatement( ) : ReturningStatement(StatementType.DELETE, listOf(table)) { override val set: FieldSet = returning ?: table - override fun prepareSQL(transaction: Transaction): String = buildString { + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String = buildString { append("DELETE FROM ") append(transaction.identity(table)) if (where != null) { @@ -123,7 +123,7 @@ class UpdateReturningStatement( private val firstDataSet: List, Any?>> get() = values.toList() - override fun prepareSQL(transaction: Transaction): String = + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String = with(QueryBuilder(true)) { +"UPDATE " table.describe(transaction, this) @@ -245,7 +245,7 @@ class UpdateReturningOldStatement( private val firstDataSet: List, Any?>> get() = values.toList() - override fun prepareSQL(transaction: Transaction): String = + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String = with(QueryBuilder(true)) { +"UPDATE " table.describe(transaction, this) diff --git a/server-redis/build.gradle.kts b/server-redis/build.gradle.kts index ab6a92f6..11dab04f 100644 --- a/server-redis/build.gradle.kts +++ b/server-redis/build.gradle.kts @@ -17,8 +17,9 @@ val khrysalisVersion: String by project val coroutines:String by project dependencies { api(project(":server-core")) - api("io.lettuce:lettuce-core:6.3.1.RELEASE") + api("io.lettuce:lettuce-core:6.3.2.RELEASE") api("org.signal:embedded-redis:0.8.3") + api("com.google.guava:guava:33.0.0-jre") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines") testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation(project(":server-testing")) diff --git a/server-sentry/build.gradle.kts b/server-sentry/build.gradle.kts index d3f73157..52c8124c 100644 --- a/server-sentry/build.gradle.kts +++ b/server-sentry/build.gradle.kts @@ -17,7 +17,7 @@ val khrysalisVersion: String by project val coroutines: String by project dependencies { api(project(":server-core")) - api("io.sentry:sentry:7.2.0") + api("io.sentry:sentry:7.6.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines") testImplementation("org.jetbrains.kotlin:kotlin-test-junit") ksp(project(":processor")) From 629429a46f91e690b32b01859bfd3e8c3ea5cbb4 Mon Sep 17 00:00:00 2001 From: Joseph Ivie Date: Tue, 23 Apr 2024 10:25:29 -0600 Subject: [PATCH 2/2] Address MFA vulnerability --- .../auth/subject/AuthEndpointsForSubject.kt | 10 +- .../test/AuthEndpointsForSubjectTest.kt | 22 ++ .../lightningdb/test/ConditionTests.kt | 290 ++++++++++++++++++ .../lightningkite/lightningdb/test/models.kt | 2 + 4 files changed, 323 insertions(+), 1 deletion(-) diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/subject/AuthEndpointsForSubject.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/subject/AuthEndpointsForSubject.kt index 0eecad7c..4e4305c4 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/subject/AuthEndpointsForSubject.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/subject/AuthEndpointsForSubject.kt @@ -146,6 +146,11 @@ class AuthEndpointsForSubject, ID : Comparable>( detail = "invalid-proof", message = "A given proof was invalid." ) + val errorIrrelevantProof = LSError( + 400, + detail = "irrelevant-proof", + message = "A given proof was not related to the user." + ) val errorExpiredProof = LSError( 400, detail = "expired-proof", @@ -200,7 +205,7 @@ class AuthEndpointsForSubject, ID : Comparable>( outputType = IdAndAuthMethods.serializer(handler.idSerializer), summary = "Log In", description = "Attempt to log in as a ${handler.name} using various proofs.", - errorCases = listOf(errorNoSingleUser, errorInvalidProof), + errorCases = listOf(errorNoSingleUser, errorInvalidProof, errorIrrelevantProof), implementation = { proofs: List -> proofs.forEach { if (!proofHasher().verify(it)) throw HttpStatusException(errorInvalidProof.copy(data = it.via)) @@ -209,6 +214,9 @@ class AuthEndpointsForSubject, ID : Comparable>( val used = proofs.map { it.via }.toSet() val users = proofs.mapNotNull { handler.findUser(it.property, it.value) }.distinctBy { it._id } val subject = users.singleOrNull() ?: throw HttpStatusException(errorNoSingleUser) + proofs.forEach { + if(handler.get(subject, it.property) != it.value) throw HttpStatusException(errorIrrelevantProof.copy(data = it.via)) + } val strength = proofs.groupBy { it.property }.values.sumOf { it.maxOf { it.strength } } val proofMethods = handler.proofMethods .filter { it.established(handler, subject) } diff --git a/server-core/src/test/kotlin/com/lightningkite/lightningserver/auth/subject/test/AuthEndpointsForSubjectTest.kt b/server-core/src/test/kotlin/com/lightningkite/lightningserver/auth/subject/test/AuthEndpointsForSubjectTest.kt index b6df4635..3f71e634 100644 --- a/server-core/src/test/kotlin/com/lightningkite/lightningserver/auth/subject/test/AuthEndpointsForSubjectTest.kt +++ b/server-core/src/test/kotlin/com/lightningkite/lightningserver/auth/subject/test/AuthEndpointsForSubjectTest.kt @@ -167,6 +167,28 @@ class AuthEndpointsForSubjectTest { val result = TestSettings.testUserSubject.login.test(null, listOf(proof)) assert(result.session != null) } + + // Can't log in with wrong phone + run { + val proofA = run { + val key = TestSettings.proofSms.start.test(null, "8001002001") + val pin = pinRegex.find(TestSMSClient.lastMessageSent?.message ?: "")!!.value + TestSettings.proofSms.prove.test(null, FinishProof(key, pin)) + } + val proofB = run { + val key = TestSettings.proofEmail.start.test(null, "notadmin@test.com") + val pin = (TestSettings.email() as TestEmailClient).lastEmailSent?.also { println(it) }?.plainText?.let { + pinRegex.find(it)?.value + }!! + TestSettings.proofEmail.prove.test(null, FinishProof(key, pin)) + } + try { + val result = TestSettings.testUserSubject.login.test(null, listOf(proofA, proofB)) + fail() + } catch(e: Exception) { + assertContains(e.message ?: "", "not related", ignoreCase = true) + } + } } @Test diff --git a/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/ConditionTests.kt b/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/ConditionTests.kt index ac2023ce..91382c18 100644 --- a/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/ConditionTests.kt +++ b/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/ConditionTests.kt @@ -10,6 +10,7 @@ import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertTrue import com.lightningkite.lightningdb.* +import java.util.* abstract class ConditionTests() { @@ -2570,4 +2571,293 @@ abstract class ConditionTests() { Unit } + + + //// + + + @Test fun test_UUID_eq() = runBlocking { + val collection = database.collection("LargeTestModel_test_UUID_eq") + val lower = LargeTestModel(uuid = UUID(0L, 1L)) + val higher = LargeTestModel(uuid = UUID(0L, 3L)) + val manualList = listOf(lower, higher) + collection.insertOne(lower) + collection.insertOne(higher) + val condition = path().uuid eq UUID(0L, 3L) + val results = collection.find(condition).toList() + assertContains(results, higher) + assertTrue(lower !in results) + assertEquals(manualList.filter { condition(it) }.sortedBy { it._id }, results.sortedBy { it._id }) + Unit + } + + @Test fun test_UUID_ne() = runBlocking { + val collection = database.collection("LargeTestModel_test_UUID_ne") + val lower = LargeTestModel(uuid = UUID(0L, 1L)) + val higher = LargeTestModel(uuid = UUID(0L, 3L)) + val manualList = listOf(lower, higher) + collection.insertOne(lower) + collection.insertOne(higher) + val condition = path().uuid ne UUID(0L, 3L) + val results = collection.find(condition).toList() + assertContains(results, lower) + assertTrue(higher !in results) + assertEquals(manualList.filter { condition(it) }.sortedBy { it._id }, results.sortedBy { it._id }) + Unit + } + + @Test fun test_UUID_nullable_eq() = runBlocking { + val collection = database.collection("LargeTestModel_test_UUID_nullable_eq") + val lower = LargeTestModel(uuidNullable = null) + val higher = LargeTestModel(uuidNullable = UUID(0L, 3L)) + val manualList = listOf(lower, higher) + collection.insertOne(lower) + collection.insertOne(higher) + val condition = path().uuidNullable.notNull eq UUID(0L, 3L) + val results = collection.find(condition).toList() + assertContains(results, higher) + assertTrue(lower !in results) + assertEquals(manualList.filter { condition(it) }.sortedBy { it._id }, results.sortedBy { it._id }) + Unit + } + + @Test fun test_UUID_nullable() = runBlocking { + val collection = database.collection("test_UUID_nullable") + val lower = LargeTestModel(uuidNullable = null) + val higher = LargeTestModel(uuidNullable = UUID(0L, 3L)) + val manualList = listOf(lower, higher) + collection.insertOne(lower) + collection.insertOne(higher) + val condition = path().uuidNullable eq null + val results = collection.find(condition).toList() + assertContains(results, lower) + assertTrue(higher !in results) + assertEquals(manualList.filter { condition(it) }.sortedBy { it._id }, results.sortedBy { it._id }) + Unit + } + @Test fun test_UUID_inside() = runBlocking { + val collection = database.collection("LargeTestModel_test_UUID_inside") + val higher = LargeTestModel(uuid = UUID(0L, 3L)) + val lower = LargeTestModel(uuid = UUID(0L, 1L)) + val manualList = listOf(lower, higher) + collection.insertOne(lower) + collection.insertOne(higher) + val condition = path().uuid inside listOf(UUID(0L, 3L)) + val results = collection.find(condition).toList() + assertContains(results, higher) + assertTrue(lower !in results) + assertEquals(manualList.filter { condition(it) }.sortedBy { it._id }, results.sortedBy { it._id }) + Unit + } + + @Test fun test_UUID_inside_nullable() = runBlocking { + val collection = database.collection("LargeTestModel_test_UUID_inside_nullable") + val higher = LargeTestModel(uuidNullable = UUID(0L, 3L)) + val isNull = LargeTestModel(uuidNullable = null) + val manualList = listOf(isNull, higher) + collection.insertOne(isNull) + collection.insertOne(higher) + val condition = path().uuidNullable.notNull inside listOf(UUID(0L, 3L)) + val results = collection.find(condition).toList() + assertContains(results, higher) + assertTrue(isNull !in results) + assertEquals(manualList.filter { condition(it) }.sortedBy { it._id }, results.sortedBy { it._id }) + Unit + } + + @Test fun test_UUID_notIn() = runBlocking { + val collection = database.collection("test_UUID_notIn") + val lower = LargeTestModel(uuid = UUID(0L, 1L)) + val higher = LargeTestModel(uuid = UUID(0L, 3L)) + val manualList = listOf(lower, higher) + collection.insertOne(lower) + collection.insertOne(higher) + val condition = path().uuid notIn listOf(UUID(0L, 3L)) + val results = collection.find(condition).toList() + assertContains(results, lower) + assertTrue(higher !in results) + assertEquals(manualList.filter { condition(it) }.sortedBy { it._id }, results.sortedBy { it._id }) + Unit + } + + @Test fun test_UUID_gt() = runBlocking { + val collection = database.collection("LargeTestModel_test_UUID_gt") + val lower = LargeTestModel(uuid = UUID(0L, 1L)) + val higher = LargeTestModel(uuid = UUID(0L, 3L)) + val middle = LargeTestModel(uuid = UUID(0L, 2L)) + val manualList = listOf(lower, higher, middle) + collection.insertOne(lower) + collection.insertOne(higher) + collection.insertOne(middle) + val condition = path().uuid gt UUID(0L, 2L) + val results = collection.find(condition).toList() + assertContains(results, higher) + assertTrue(lower !in results) + assertTrue(middle !in results) + assertEquals(manualList.filter { condition(it) }.sortedBy { it._id }, results.sortedBy { it._id }) + Unit + } + + @Test fun test_UUID_lt() = runBlocking { + val collection = database.collection("LargeTestModel_test_UUID_lt") + val lower = LargeTestModel(uuid = UUID(0L, 1L)) + val higher = LargeTestModel(uuid = UUID(0L, 3L)) + val middle = LargeTestModel(uuid = UUID(0L, 2L)) + val manualList = listOf(lower, higher, middle) + collection.insertOne(lower) + collection.insertOne(higher) + collection.insertOne(middle) + val condition = path().uuid lt UUID(0L, 2L) + val results = collection.find(condition).toList() + assertContains(results, lower) + assertTrue(higher !in results) + assertTrue(middle !in results) + assertEquals(manualList.filter { condition(it) }.sortedBy { it._id }, results.sortedBy { it._id }) + Unit + } + + @Test fun test_UUID_gte() = runBlocking { + val collection = database.collection("LargeTestModel_test_UUID_gte") + val lower = LargeTestModel(uuid = UUID(0L, 1L)) + val higher = LargeTestModel(uuid = UUID(0L, 3L)) + val middle = LargeTestModel(uuid = UUID(0L, 2L)) + val manualList = listOf(lower, higher, middle) + collection.insertOne(lower) + collection.insertOne(higher) + collection.insertOne(middle) + val condition = path().uuid gte UUID(0L, 2L) + val results = collection.find(condition).toList() + assertContains(results, middle) + assertContains(results, higher) + assertTrue(lower !in results) + assertEquals(manualList.filter { condition(it) }.sortedBy { it._id }, results.sortedBy { it._id }) + Unit + } + + @Test fun test_UUID_lte() = runBlocking { + val collection = database.collection("LargeTestModel_test_UUID_lte") + val lower = LargeTestModel(uuid = UUID(0L, 1L)) + val higher = LargeTestModel(uuid = UUID(0L, 3L)) + val middle = LargeTestModel(uuid = UUID(0L, 2L)) + val manualList = listOf(lower, higher, middle) + collection.insertOne(lower) + collection.insertOne(higher) + collection.insertOne(middle) + val condition = path().uuid lte UUID(0L, 2L) + val results = collection.find(condition).toList() + assertContains(results, middle) + assertContains(results, lower) + assertTrue(higher !in results) + assertEquals(manualList.filter { condition(it) }.sortedBy { it._id }, results.sortedBy { it._id }) + Unit + } + + @Test fun UUIUUID_gt_nullable() = runBlocking { + val collection = database.collection("@") + val isNull = LargeTestModel(uuidNullable = null) + val lower = LargeTestModel(uuidNullable = UUID(0L, 1L)) + val higher = LargeTestModel(uuidNullable = UUID(0L, 3L)) + val middle = LargeTestModel(uuidNullable = UUID(0L, 2L)) + val manualList = listOf(lower, higher, middle, isNull) + collection.insertOne(lower) + collection.insertOne(higher) + collection.insertOne(middle) + collection.insertOne(isNull) + val condition = path().uuidNullable.notNull gt UUID(0L, 2L) + val results = collection.find(condition).toList() + assertContains(results, higher) + assertTrue(lower !in results) + assertTrue(middle !in results) + assertTrue(isNull !in results) + assertEquals(manualList.filter { condition(it) }.sortedBy { it._id }, results.sortedBy { it._id }) + Unit + } + @Test fun test_UUID_lt_nullable() = runBlocking { + val collection = database.collection("test_UUID_lt_nullable") + val isNull = LargeTestModel(uuidNullable = null) + val lower = LargeTestModel(uuidNullable = UUID(0L, 1L)) + val higher = LargeTestModel(uuidNullable = UUID(0L, 3L)) + val middle = LargeTestModel(uuidNullable = UUID(0L, 2L)) + val manualList = listOf(lower, higher, middle, isNull) + collection.insertOne(lower) + collection.insertOne(higher) + collection.insertOne(middle) + collection.insertOne(isNull) + val condition = path().uuidNullable.notNull lt UUID(0L, 2L) + val results = collection.find(condition).toList() + assertContains(results, lower) + assertTrue(higher !in results) + assertTrue(middle !in results) + assertTrue(isNull !in results) + assertEquals(manualList.filter { condition(it) }.sortedBy { it._id }, results.sortedBy { it._id }) + Unit + } + @Test fun test_UUID_gte_nullable() = runBlocking { + val collection = database.collection("test_UUID_gte_nullable") + val isNull = LargeTestModel(uuidNullable = null) + val lower = LargeTestModel(uuidNullable = UUID(0L, 1L)) + val higher = LargeTestModel(uuidNullable = UUID(0L, 3L)) + val middle = LargeTestModel(uuidNullable = UUID(0L, 2L)) + val manualList = listOf(lower, higher, middle, isNull) + collection.insertOne(lower) + collection.insertOne(higher) + collection.insertOne(middle) + collection.insertOne(isNull) + val condition = path().uuidNullable.notNull gte UUID(0L, 2L) + val results = collection.find(condition).toList() + assertContains(results, middle) + assertContains(results, higher) + assertTrue(lower !in results) + assertTrue(isNull !in results) + assertEquals(manualList.filter { condition(it) }.sortedBy { it._id }, results.sortedBy { it._id }) + Unit + } + @Test fun test_UUID_lte_nullable() = runBlocking { + val collection = database.collection("test_UUID_lte_nullable") + val isNull = LargeTestModel(uuidNullable = null) + val lower = LargeTestModel(uuidNullable = UUID(0L, 1L)) + val higher = LargeTestModel(uuidNullable = UUID(0L, 3L)) + val middle = LargeTestModel(uuidNullable = UUID(0L, 2L)) + val manualList = listOf(lower, higher, middle, isNull) + collection.insertOne(lower) + collection.insertOne(higher) + collection.insertOne(middle) + collection.insertOne(isNull) + val condition = path().uuidNullable.notNull lte UUID(0L, 2L) + val results = collection.find(condition).toList() + assertContains(results, middle) + assertContains(results, lower) + assertTrue(higher !in results) + assertTrue(isNull !in results) + assertEquals(manualList.filter { condition(it) }.sortedBy { it._id }, results.sortedBy { it._id }) + Unit + } + + val testDataUuidMin = UUID(0L, 1L) + val testDataUuidMax = UUID(0L, Int.MAX_VALUE.toLong() + 1L) + fun testDataUuid(id: Int) = UUID(0L, id.toLong() + 1L) + val DataClassPath.isTestData: Condition get() = gte(testDataUuidMin) and lte(testDataUuidMax) + @Test fun test_UUID_between() = runBlocking { + val collection = database.collection("test_UUID_between") + val lower = LargeTestModel(uuid = UUID(0L, 1L)) + val higher = LargeTestModel(uuid = UUID(1L, 3L)) + val middle = LargeTestModel(uuid = UUID(0L, 2L)) + collection.insertOne(lower) + collection.insertOne(higher) + collection.insertOne(middle) + val condition = path().uuid.isTestData + val results = collection.find(condition).toList() + assertContains(results, middle) + assertContains(results, lower) + assertTrue(higher !in results) + Unit + } + + + + + + + + } \ No newline at end of file diff --git a/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/models.kt b/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/models.kt index 812c803c..306ec115 100644 --- a/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/models.kt +++ b/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/models.kt @@ -117,6 +117,7 @@ data class LargeTestModel( var double: Double = 0.0, var char: Char = ' ', var string: String = "", + var uuid: UUID = UUID(0L, 0L), var instant: Instant = Instant.fromEpochMilliseconds(0L), var list: List = listOf(), var listEmbedded: List = listOf(), @@ -133,6 +134,7 @@ data class LargeTestModel( var doubleNullable: Double? = null, var charNullable: Char? = null, var stringNullable: String? = null, + var uuidNullable: UUID? = null, var instantNullable: Instant? = null, var listNullable: List? = null, var mapNullable: Map? = null,