Skip to content

Commit f9b40d6

Browse files
authored
Merge pull request #295 from modelix/MODELIX-577
MODELIX-577 Add ModelQL v2 support to model-server
2 parents b0c607f + 31864fc commit f9b40d6

File tree

6 files changed

+105
-52
lines changed

6 files changed

+105
-52
lines changed

model-client/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ kotlin {
4343
api(project(":model-api"))
4444
api(project(":model-datastructure"))
4545
api(project(":model-server-api"))
46+
implementation(project(":modelql-client"))
47+
api(project(":modelql-core"))
4648
implementation(kotlin("stdlib-common"))
4749
implementation(libs.kotlin.collections.immutable)
4850
implementation(libs.kotlin.coroutines.core)

model-client/src/commonMain/kotlin/org/modelix/model/client2/IModelClientV2.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313
*/
1414
package org.modelix.model.client2
1515

16+
import org.modelix.kotlin.utils.DeprecationInfo
1617
import org.modelix.model.IVersion
1718
import org.modelix.model.api.IIdGenerator
19+
import org.modelix.model.api.INode
1820
import org.modelix.model.lazy.BranchReference
1921
import org.modelix.model.lazy.RepositoryId
22+
import org.modelix.modelql.core.IMonoStep
2023

2124
/**
2225
* This interface is meant exclusively for model client usage.
@@ -41,8 +44,12 @@ interface IModelClientV2 {
4144

4245
suspend fun listBranches(repository: RepositoryId): List<BranchReference>
4346

47+
@Deprecated("repository ID is required for permission checks")
48+
@DeprecationInfo("3.7.0", "May be removed with the next major release. Also remove the endpoint from the model-server.")
4449
suspend fun loadVersion(versionHash: String, baseVersion: IVersion?): IVersion
4550

51+
suspend fun loadVersion(repositoryId: RepositoryId, versionHash: String, baseVersion: IVersion?): IVersion
52+
4653
/**
4754
* The pushed version is merged automatically by the server with the current head.
4855
* The merge result is returned.
@@ -62,4 +69,8 @@ interface IModelClientV2 {
6269
suspend fun poll(branch: BranchReference, lastKnownVersion: IVersion?): IVersion
6370

6471
suspend fun pollHash(branch: BranchReference, lastKnownVersion: IVersion?): String
72+
73+
suspend fun <R> query(branch: BranchReference, body: (IMonoStep<INode>) -> IMonoStep<R>): R
74+
75+
suspend fun <R> query(repositoryId: RepositoryId, versionHash: String, body: (IMonoStep<INode>) -> IMonoStep<R>): R
6576
}

model-client/src/commonMain/kotlin/org/modelix/model/client2/ModelClientV2.kt

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import io.ktor.serialization.kotlinx.json.json
3232
import kotlinx.coroutines.CoroutineScope
3333
import kotlinx.coroutines.launch
3434
import kotlinx.serialization.json.Json
35+
import org.modelix.kotlin.utils.DeprecationInfo
3536
import org.modelix.model.IVersion
3637
import org.modelix.model.api.IIdGenerator
3738
import org.modelix.model.api.INode
@@ -49,6 +50,8 @@ import org.modelix.model.operations.OTBranch
4950
import org.modelix.model.persistent.HashUtil
5051
import org.modelix.model.persistent.MapBasedStore
5152
import org.modelix.model.server.api.v2.VersionDelta
53+
import org.modelix.modelql.client.ModelQLClient
54+
import org.modelix.modelql.core.IMonoStep
5255
import kotlin.time.Duration.Companion.seconds
5356

5457
class ModelClientV2(
@@ -120,6 +123,8 @@ class ModelClientV2(
120123
}.bodyAsText().lines().map { repository.getBranchReference(it) }
121124
}
122125

126+
@Deprecated("repository ID is required for permission checks")
127+
@DeprecationInfo("3.7.0", "May be removed with the next major release. Also remove the endpoint from the model-server.")
123128
override suspend fun loadVersion(versionHash: String, baseVersion: IVersion?): IVersion {
124129
val response = httpClient.post {
125130
url {
@@ -131,7 +136,25 @@ class ModelClientV2(
131136
}
132137
}
133138
val delta = Json.decodeFromString<VersionDelta>(response.bodyAsText())
134-
return createVersion(null, delta)
139+
return createVersion(baseVersion as CLVersion?, delta)
140+
}
141+
142+
override suspend fun loadVersion(
143+
repositoryId: RepositoryId,
144+
versionHash: String,
145+
baseVersion: IVersion?,
146+
): IVersion {
147+
val response = httpClient.post {
148+
url {
149+
takeFrom(baseUrl)
150+
appendPathSegments("repositories", repositoryId.id, "versions", versionHash)
151+
if (baseVersion != null) {
152+
parameters["lastKnown"] = (baseVersion as CLVersion).getContentHash()
153+
}
154+
}
155+
}
156+
val delta = Json.decodeFromString<VersionDelta>(response.bodyAsText())
157+
return createVersion(baseVersion as CLVersion?, delta)
135158
}
136159

137160
override suspend fun push(branch: BranchReference, version: IVersion, baseVersion: IVersion?): IVersion {
@@ -212,6 +235,22 @@ class ModelClientV2(
212235
return receivedVersion
213236
}
214237

238+
override suspend fun <R> query(branch: BranchReference, body: (IMonoStep<INode>) -> IMonoStep<R>): R {
239+
val url = URLBuilder().apply {
240+
takeFrom(baseUrl)
241+
appendPathSegmentsEncodingSlash("repositories", branch.repositoryId.id, "branches", branch.branchName, "query")
242+
}
243+
return ModelQLClient.builder().httpClient(httpClient).url(url.buildString()).build().query(body)
244+
}
245+
246+
override suspend fun <R> query(repository: RepositoryId, versionHash: String, body: (IMonoStep<INode>) -> IMonoStep<R>): R {
247+
val url = URLBuilder().apply {
248+
takeFrom(baseUrl)
249+
appendPathSegmentsEncodingSlash("repositories", repository.id, "versions", versionHash, "query")
250+
}
251+
return ModelQLClient.builder().httpClient(httpClient).url(url.buildString()).build().query(body)
252+
}
253+
215254
override fun close() {
216255
httpClient.close()
217256
}

model-server/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ dependencies {
6363
testImplementation(libs.cucumber.java)
6464
testImplementation(libs.ktor.server.test.host)
6565
testImplementation(kotlin("test"))
66+
testImplementation(project(":modelql-untyped"))
6667
}
6768

6869
tasks.test {

model-server/src/main/kotlin/org/modelix/model/server/handlers/ModelReplicationServer.kt

Lines changed: 34 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,17 @@
1414

1515
package org.modelix.model.server.handlers
1616

17-
import io.ktor.http.ContentType
1817
import io.ktor.http.HttpStatusCode
1918
import io.ktor.server.application.Application
2019
import io.ktor.server.application.ApplicationCall
2120
import io.ktor.server.application.call
2221
import io.ktor.server.plugins.origin
2322
import io.ktor.server.request.receive
24-
import io.ktor.server.request.receiveParameters
2523
import io.ktor.server.response.respond
2624
import io.ktor.server.response.respondText
2725
import io.ktor.server.routing.Route
2826
import io.ktor.server.routing.get
2927
import io.ktor.server.routing.post
30-
import io.ktor.server.routing.put
3128
import io.ktor.server.routing.route
3229
import io.ktor.server.routing.routing
3330
import io.ktor.server.websocket.webSocket
@@ -38,6 +35,7 @@ import kotlinx.serialization.encodeToString
3835
import kotlinx.serialization.json.Json
3936
import org.modelix.authorization.getUserName
4037
import org.modelix.model.api.PBranch
38+
import org.modelix.model.api.TreePointer
4139
import org.modelix.model.api.getRootNode
4240
import org.modelix.model.area.getArea
4341
import org.modelix.model.client2.checkObjectHashes
@@ -46,14 +44,11 @@ import org.modelix.model.lazy.CLTree
4644
import org.modelix.model.lazy.CLVersion
4745
import org.modelix.model.lazy.RepositoryId
4846
import org.modelix.model.operations.OTBranch
49-
import org.modelix.model.persistent.HashUtil
50-
import org.modelix.model.server.api.ModelQuery
5147
import org.modelix.model.server.api.v2.VersionDelta
5248
import org.modelix.model.server.store.IStoreClient
5349
import org.modelix.model.server.store.LocalModelClient
5450
import org.modelix.modelql.server.ModelQLServer
5551
import org.slf4j.LoggerFactory
56-
import java.util.UUID
5752

5853
/**
5954
* Implements the endpoints used by the 'model-client', but compared to KeyValueLikeModelServer also understands what
@@ -65,10 +60,6 @@ class ModelReplicationServer(val repositoriesManager: RepositoriesManager) {
6560

6661
companion object {
6762
private val LOG = LoggerFactory.getLogger(ModelReplicationServer::class.java)
68-
69-
private fun randomUUID(): String {
70-
return UUID.randomUUID().toString().replace("[^a-zA-Z0-9]".toRegex(), "")
71-
}
7263
}
7364

7465
private val modelClient: LocalModelClient get() = repositoriesManager.client
@@ -191,10 +182,43 @@ class ModelReplicationServer(val repositoriesManager: RepositoriesManager) {
191182
}
192183
}
193184
}
185+
route("versions") {
186+
route("{versionHash}") {
187+
get {
188+
// TODO permission check on the repository ID is not sufficient, because the client could
189+
// provide any repository ID to access a version inside a different repository.
190+
// A check if the version belongs to the repository is required.
191+
val baseVersionHash = call.request.queryParameters["lastKnown"]
192+
val versionHash = call.parameters["versionHash"]!!
193+
if (storeClient[versionHash] == null) {
194+
call.respondText(
195+
"Version '$versionHash' doesn't exist",
196+
status = HttpStatusCode.NotFound,
197+
)
198+
return@get
199+
}
200+
call.respondDelta(versionHash, baseVersionHash)
201+
}
202+
get("history/{oldestVersionHash}") {
203+
TODO()
204+
}
205+
post("query") {
206+
val versionHash = call.parameters["versionHash"]!!
207+
val version = CLVersion.loadFromHash(versionHash, repositoriesManager.client.storeCache)
208+
val initialTree = version.getTree()
209+
val branch = TreePointer(initialTree)
210+
ModelQLServer.handleCall(call, branch.getRootNode(), branch.getArea())
211+
}
212+
}
213+
}
194214
}
195215
}
196216
route("versions") {
197217
get("{versionHash}") {
218+
// TODO versions should be stored inside a repository with permission checks.
219+
// Knowing a version hash should not give you access to the content.
220+
// This handler was already moved to the 'repositories' route. Removing it here would be a breaking
221+
// change, but should be done in some future version.
198222
val baseVersionHash = call.request.queryParameters["lastKnown"]
199223
val versionHash = call.parameters["versionHash"]!!
200224
if (storeClient[versionHash] == null) {
@@ -210,47 +234,6 @@ class ModelReplicationServer(val repositoriesManager: RepositoriesManager) {
210234
TODO()
211235
}
212236
}
213-
route("objects") {
214-
post {
215-
val values = call.receive<List<String>>()
216-
storeClient.putAll(values.associateBy { HashUtil.sha256(it) }, true)
217-
call.respondText("OK")
218-
}
219-
get("{hash}") {
220-
val key = call.parameters["hash"]!!
221-
val value = storeClient[key]
222-
if (value == null) {
223-
call.respondText("object '$key' not found", status = HttpStatusCode.NotFound)
224-
} else {
225-
call.respondText(value)
226-
}
227-
}
228-
}
229-
route("modelql") {
230-
put {
231-
val params = call.receiveParameters()
232-
val queryFromClient = params["query"]
233-
if (queryFromClient == null) {
234-
call.respondText(text = "'query' is missing", status = HttpStatusCode.BadRequest)
235-
return@put
236-
}
237-
val query = ModelQuery.fromJson(queryFromClient)
238-
val json = query.toJson()
239-
val hash = HashUtil.sha256(json)
240-
storeClient.put(hash, json)
241-
call.respondText(text = hash)
242-
}
243-
get("{hash}") {
244-
val hash = call.parameters["hash"]!!
245-
val json = storeClient[hash]
246-
if (json == null) {
247-
call.respondText(status = HttpStatusCode.NotFound, text = "ModelQL with hash '$hash' doesn't exist")
248-
return@get
249-
}
250-
ModelQuery.fromJson(json) // ensure it's a valid ModelQuery
251-
call.respondText(json, ContentType.Application.Json)
252-
}
253-
}
254237
}
255238

256239
private suspend fun ApplicationCall.respondDelta(versionHash: String, baseVersionHash: String?) {

model-server/src/test/kotlin/org/modelix/model/server/ModelClientV2Test.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import org.modelix.model.lazy.RepositoryId
3232
import org.modelix.model.operations.OTBranch
3333
import org.modelix.model.server.handlers.ModelReplicationServer
3434
import org.modelix.model.server.store.InMemoryStoreClient
35+
import org.modelix.modelql.core.count
36+
import org.modelix.modelql.untyped.allChildren
3537
import kotlin.test.Test
3638
import kotlin.test.assertEquals
3739

@@ -87,6 +89,21 @@ class ModelClientV2Test {
8789
)
8890
}
8991

92+
@Test
93+
fun modelqlSmokeTest() = runTest {
94+
val url = "http://localhost/v2"
95+
val client = ModelClientV2.builder().url(url).client(client).build().also { it.init() }
96+
97+
val repositoryId = RepositoryId("repo1")
98+
val branchRef = repositoryId.getBranchReference()
99+
val initialVersion = client.initRepository(repositoryId)
100+
val size = client.query(branchRef) { it.allChildren().count() }
101+
assertEquals(0, size)
102+
103+
val size2 = client.query(repositoryId, initialVersion.getContentHash()) { it.allChildren().count() }
104+
assertEquals(0, size2)
105+
}
106+
90107
@Test
91108
fun testSlashesInPathSegmentsFromRepositoryIdAndBranchId() = runTest {
92109
val url = "http://localhost/v2"

0 commit comments

Comments
 (0)