Skip to content

Commit 0832f59

Browse files
committed
feat(model-client): new method IModelClientV2.migrateRoles for replacing names with IDs
1 parent ac16de3 commit 0832f59

File tree

9 files changed

+306
-6
lines changed

9 files changed

+306
-6
lines changed

model-api/src/commonMain/kotlin/org/modelix/model/data/ModelData.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import org.modelix.model.api.IPropertyReference
99
import org.modelix.model.api.IReadableNode
1010
import org.modelix.model.api.ITree
1111
import org.modelix.model.api.IWriteTransaction
12+
import org.modelix.model.api.NullChildLinkReference
1213
import org.modelix.model.api.PNodeReference
14+
import org.modelix.model.api.meta.NullConcept
1315

1416
@Serializable
1517
data class ModelData(
@@ -143,8 +145,8 @@ fun IReadableNode.asData(): NodeData = asLegacyNode().asData()
143145

144146
fun INode.asData(): NodeData = NodeData(
145147
id = reference.serialize(),
146-
concept = getConceptReference()?.getUID(),
147-
role = getContainmentLink()?.toReference()?.getIdOrNameOrNull(),
148+
concept = getConceptReference()?.takeIf { it != NullConcept.getReference() }?.getUID(),
149+
role = getContainmentLink()?.toReference()?.takeIf { !it.matches(NullChildLinkReference) }?.getIdOrNameOrNull(),
148150
properties = getPropertyLinks().associateWithNotNull { getPropertyValue(it) }
149151
.mapKeys { it.key.toReference().getIdOrName() },
150152
references = getReferenceLinks().associateWithNotNull { getReferenceTargetRef(it)?.serialize() }
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package org.modelix.model.client2
2+
3+
import org.modelix.datastructures.model.getDescendants
4+
import org.modelix.datastructures.objects.asObject
5+
import org.modelix.kotlin.utils.DelicateModelixApi
6+
import org.modelix.model.IVersion
7+
import org.modelix.model.ObjectDeltaFilter
8+
import org.modelix.model.TreeType
9+
import org.modelix.model.api.ConceptReference
10+
import org.modelix.model.api.IChildLinkReference
11+
import org.modelix.model.api.INodeReference
12+
import org.modelix.model.api.IPropertyReference
13+
import org.modelix.model.api.IReferenceLinkReference
14+
import org.modelix.model.api.IRoleReference
15+
import org.modelix.model.api.NullChildLinkReference
16+
import org.modelix.model.lazy.BranchReference
17+
import org.modelix.model.lazy.CLVersion
18+
import org.modelix.model.lazy.runWriteOnTree
19+
import org.modelix.model.mutable.DummyIdGenerator
20+
import org.modelix.model.mutable.moveChildren
21+
import org.modelix.model.mutable.setProperty
22+
import org.modelix.model.mutable.setReferenceTarget
23+
import org.modelix.model.persistent.CPTree
24+
import org.modelix.model.persistent.CPVersion
25+
import org.modelix.streams.IStream
26+
import org.modelix.streams.executeBlocking
27+
import org.modelix.streams.forEach
28+
import kotlin.collections.component1
29+
import kotlin.collections.component2
30+
import kotlin.collections.iterator
31+
import kotlin.collections.plus
32+
33+
suspend fun IModelClientV2.migrateRoles(
34+
branch: BranchReference,
35+
roleReplacement: (IRoleReference) -> IRoleReference,
36+
): IVersion = migrateRoles(
37+
branch,
38+
object : IRoleReplacement {
39+
override fun replaceRole(concept: ConceptReference, role: IPropertyReference): IPropertyReference {
40+
return roleReplacement(role) as IPropertyReference
41+
}
42+
43+
override fun replaceRole(concept: ConceptReference, role: IReferenceLinkReference): IReferenceLinkReference {
44+
return roleReplacement(role) as IReferenceLinkReference
45+
}
46+
47+
override fun replaceRole(concept: ConceptReference, role: IChildLinkReference): IChildLinkReference {
48+
return roleReplacement(role) as IChildLinkReference
49+
}
50+
},
51+
)
52+
53+
suspend fun IModelClientV2.migrateRoles(
54+
branch: BranchReference,
55+
roleReplacement: IRoleReplacement,
56+
): IVersion {
57+
val oldVersion = pull(
58+
branch,
59+
lastKnownVersion = null,
60+
filter = ObjectDeltaFilter(
61+
knownVersions = emptySet(),
62+
includeOperations = false,
63+
includeHistory = false,
64+
includeTrees = true,
65+
),
66+
)
67+
68+
val newVersion = oldVersion.runWriteOnTree(nodeIdGenerator = DummyIdGenerator<INodeReference>(), getUserId()) { newTree ->
69+
val oldTree = newTree.getTransaction().tree
70+
oldTree.getDescendants(oldTree.getRootNodeId(), true)
71+
.flatMap { IStream.of(it).zipWith(oldTree.getConceptReference(it)) { nodeId, concept -> nodeId to concept } }
72+
.flatMap { (nodeId, concept) ->
73+
oldTree.getChildren(nodeId).flatMap { childId ->
74+
IStream.of(childId)
75+
.zipWith(oldTree.getRoleInParent(childId).firstOrDefault { NullChildLinkReference }) { id, role -> id to role }
76+
}.toList().forEach { children ->
77+
val childrenByRole = children.groupBy { it.second }
78+
for ((oldRole, childrenInOldRole) in childrenByRole) {
79+
val newRole = if (oldRole.matches(NullChildLinkReference)) {
80+
NullChildLinkReference
81+
} else {
82+
roleReplacement.replaceRole(concept, oldRole)
83+
}
84+
if (oldRole == newRole) continue
85+
newTree.getWriteTransaction().moveChildren(nodeId, newRole, -1, childrenInOldRole.map { it.first })
86+
}
87+
}.andThen(
88+
oldTree.getProperties(nodeId).forEach { (oldRole, value) ->
89+
val newRole = roleReplacement.replaceRole(concept, oldRole)
90+
if (oldRole == newRole) return@forEach
91+
newTree.setProperty(nodeId, oldRole, null)
92+
newTree.setProperty(nodeId, newRole, value)
93+
}.andThen(
94+
oldTree.getReferenceTargets(nodeId).forEach { (oldRole, value) ->
95+
val newRole = roleReplacement.replaceRole(concept, oldRole)
96+
newTree.setReferenceTarget(nodeId, oldRole, null)
97+
newTree.setReferenceTarget(nodeId, newRole, value)
98+
},
99+
),
100+
).andThenUnit()
101+
}.drainAll().executeBlocking(oldTree)
102+
}.replaceMainTree { it.copy(usesRoleIds = true) }
103+
104+
push(branch, newVersion, oldVersion)
105+
return newVersion
106+
}
107+
108+
interface IRoleReplacement {
109+
fun replaceRole(concept: ConceptReference, role: IPropertyReference): IPropertyReference
110+
fun replaceRole(concept: ConceptReference, role: IReferenceLinkReference): IReferenceLinkReference
111+
fun replaceRole(concept: ConceptReference, role: IChildLinkReference): IChildLinkReference
112+
}
113+
114+
private fun IVersion.replaceMainTree(modifier: (CPTree) -> CPTree): CLVersion {
115+
this as CLVersion
116+
@OptIn(DelicateModelixApi::class)
117+
return CLVersion(data.replaceMainTree(modifier).asObject(obj.graph))
118+
}
119+
120+
private fun CPVersion.replaceMainTree(modifier: (CPTree) -> CPTree): CPVersion {
121+
val mainTreeRef = treeRefs[TreeType.MAIN]!!.resolveNow()
122+
val newData = modifier(mainTreeRef.data)
123+
val newRef = mainTreeRef.graph.fromCreated(newData)
124+
return copy(treeRefs = treeRefs + (TreeType.MAIN to newRef))
125+
}

model-client/src/jvmMain/kotlin/org/modelix/model/client/RestWebModelClient.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ typealias ConnectionStatusListener = (oldValue: RestWebModelClient.ConnectionSta
7979
/**
8080
* We need to specify the connection listeners right into the constructor because connection is started in the constructor.
8181
*/
82+
@Deprecated("Use ModelClientV2 instead")
8283
class RestWebModelClient @JvmOverloads constructor(
8384
var baseUrl: String = defaultUrl,
8485
private val authTokenProvider: (() -> String?)? = null,

model-datastructure/src/commonMain/kotlin/org/modelix/model/mutable/IGenericMutableModelTree.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ fun <NodeId> IGenericMutableModelTree.WriteTransaction<NodeId>.addNewChild(
5353
),
5454
)
5555

56+
fun <NodeId> IGenericMutableModelTree.WriteTransaction<NodeId>.moveChildren(
57+
parentId: NodeId,
58+
role: IChildLinkReference,
59+
index: Int,
60+
childIds: List<NodeId>,
61+
) = mutate(
62+
MutationParameters.Move(
63+
nodeId = parentId,
64+
role = role,
65+
index = index,
66+
existingChildIds = childIds,
67+
),
68+
)
69+
5670
fun <NodeId> IGenericMutableModelTree.WriteTransaction<NodeId>.moveChild(
5771
parentId: NodeId,
5872
role: IChildLinkReference,
@@ -67,6 +81,12 @@ fun <NodeId> IGenericMutableModelTree.WriteTransaction<NodeId>.moveChild(
6781
),
6882
)
6983

84+
fun <NodeId> IGenericMutableModelTree<NodeId>.setProperty(
85+
nodeId: NodeId,
86+
role: IPropertyReference,
87+
value: String?,
88+
) = getWriteTransaction().setProperty(nodeId, role, value)
89+
7090
fun <NodeId> IGenericMutableModelTree.WriteTransaction<NodeId>.setProperty(
7191
nodeId: NodeId,
7292
role: IPropertyReference,
@@ -79,6 +99,12 @@ fun <NodeId> IGenericMutableModelTree.WriteTransaction<NodeId>.setProperty(
7999
),
80100
)
81101

102+
fun <NodeId> IGenericMutableModelTree<NodeId>.setReferenceTarget(
103+
nodeId: NodeId,
104+
role: IReferenceLinkReference,
105+
target: INodeReference?,
106+
) = getWriteTransaction().setReferenceTarget(nodeId, role, target)
107+
82108
fun <NodeId> IGenericMutableModelTree.WriteTransaction<NodeId>.setReferenceTarget(
83109
nodeId: NodeId,
84110
role: IReferenceLinkReference,

model-datastructure/src/commonMain/kotlin/org/modelix/model/persistent/CPTree.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import org.modelix.streams.plus
2929

3030
typealias ModelTreeRef = ObjectReference<CPTree>
3131

32-
class CPTree(
32+
data class CPTree(
3333
val id: TreeId,
3434
val int64Hamt: ObjectReference<IPersistentMapRootData<Long, ObjectReference<NodeObjectData<Long>>>>?,
3535
val trieWithNodeRefIds: ObjectReference<IPersistentMapRootData<INodeReference, ObjectReference<NodeObjectData<INodeReference>>>>?,

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import kotlinx.coroutines.test.runTest
55
import org.modelix.model.ObjectDeltaFilter
66
import org.modelix.model.api.ITree
77
import org.modelix.model.api.PNodeReference
8-
import org.modelix.model.api.meta.NullConcept
98
import org.modelix.model.client.IdGenerator
109
import org.modelix.model.client2.ModelClientV2
1110
import org.modelix.model.client2.runWrite
@@ -36,7 +35,6 @@ class RepositoryMigrationTest {
3635
val modelData = ModelData(
3736
root = NodeData(
3837
id = PNodeReference(config.modelId, ITree.ROOT_ID).serialize(),
39-
concept = NullConcept.getReference().getUID(),
4038
children = listOf(
4139
NodeData(
4240
id = "id1",
@@ -61,7 +59,6 @@ class RepositoryMigrationTest {
6159
"""
6260
{
6361
"id": "modelix:d9330ca8-2145-4d1f-9b50-8f5aed1804cf/1",
64-
"concept": "null",
6562
"children": [
6663
{
6764
"id": "modelix:d9330ca8-2145-4d1f-9b50-8f5aed1804cf/1c800000001",
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package org.modelix.model.server
2+
3+
import io.ktor.server.testing.ApplicationTestBuilder
4+
import io.ktor.server.testing.testApplication
5+
import mu.KotlinLogging
6+
import org.modelix.model.ObjectDeltaFilter
7+
import org.modelix.model.api.IChildLinkReference
8+
import org.modelix.model.api.IPropertyReference
9+
import org.modelix.model.api.IReferenceLinkReference
10+
import org.modelix.model.client2.migrateRoles
11+
import org.modelix.model.client2.runWriteOnTree
12+
import org.modelix.model.data.ModelData
13+
import org.modelix.model.data.asData
14+
import org.modelix.model.lazy.RepositoryId
15+
import org.modelix.model.mutable.asModelSingleThreaded
16+
import org.modelix.model.mutable.load
17+
import org.modelix.model.server.handlers.IdsApiImpl
18+
import org.modelix.model.server.handlers.ModelReplicationServer
19+
import org.modelix.model.server.handlers.RepositoriesManager
20+
import org.modelix.model.server.store.InMemoryStoreClient
21+
import kotlin.test.Test
22+
import kotlin.test.assertEquals
23+
24+
private val LOG = KotlinLogging.logger { }
25+
26+
class RolesMigrationTest {
27+
28+
private fun runTest(block: suspend ApplicationTestBuilder.() -> Unit) = testApplication {
29+
application {
30+
try {
31+
installDefaultServerPlugins()
32+
val repoManager = RepositoriesManager(InMemoryStoreClient())
33+
ModelReplicationServer(repoManager).init(this)
34+
IdsApiImpl(repoManager).init(this)
35+
} catch (ex: Throwable) {
36+
LOG.error("", ex)
37+
}
38+
}
39+
block()
40+
}
41+
42+
@Test
43+
fun `migrate role names to role IDs`() = runTest {
44+
val client = createModelClient()
45+
46+
val repositoryId = RepositoryId("repo1")
47+
val branchRef = repositoryId.getBranchReference()
48+
val initialVersion = client.initRepository(repositoryId, useRoleIds = false)
49+
50+
val allRoles = listOf(
51+
IPropertyReference.fromIdAndName("p1", "myProperty"),
52+
IReferenceLinkReference.fromIdAndName("r1", "myReference"),
53+
IChildLinkReference.fromIdAndName("c1", "myChild"),
54+
)
55+
val rolesById = allRoles.associateBy { it.getUID()!! }
56+
val rolesByName = allRoles.associateBy { it.getSimpleName()!! }
57+
58+
// language=json
59+
val modelData: ModelData = ModelData.fromJson(
60+
"""
61+
{
62+
"root": {
63+
"id": "n001",
64+
"children": [
65+
{
66+
"id": "n002",
67+
"concept": "MyConcept1",
68+
"role": "myChild",
69+
"children": [
70+
{
71+
"id": "n003",
72+
"references": {
73+
"myReference": "n004"
74+
},
75+
"properties": {
76+
"myProperty": "myPropertyValue"
77+
}
78+
}
79+
]
80+
},
81+
{
82+
"id": "n004"
83+
}
84+
]
85+
}
86+
}
87+
""".trimIndent(),
88+
)
89+
90+
client.runWriteOnTree(branchRef) { modelData.load(it) }
91+
client.migrateRoles(branchRef) { oldRole ->
92+
rolesByName.getValue(oldRole.getNameOrId())
93+
}
94+
95+
val versionWithIds = client.pull(
96+
branchRef,
97+
lastKnownVersion = null,
98+
filter = ObjectDeltaFilter(
99+
knownVersions = emptySet(),
100+
includeOperations = false,
101+
includeHistory = false,
102+
includeTrees = true,
103+
),
104+
)
105+
106+
val rootNodeWithIds = versionWithIds.getModelTree().asModelSingleThreaded().getRootNode()
107+
val actualJsonWithIds = rootNodeWithIds.asData().toJson()
108+
// language=json
109+
val expectedJsonWithIds = """
110+
{
111+
"root": {
112+
"id": "modelix:00000000-0000-0000-0000-000000000000/1",
113+
"children": [
114+
{
115+
"id": "n004"
116+
},
117+
{
118+
"id": "n002",
119+
"concept": "MyConcept1",
120+
"role": "c1",
121+
"children": [
122+
{
123+
"id": "n003",
124+
"properties": {
125+
"p1": "myPropertyValue"
126+
},
127+
"references": {
128+
"r1": "n004"
129+
}
130+
}
131+
]
132+
}
133+
]
134+
}
135+
}
136+
""".let { ModelData.fromJson(it).root.copy(id = rootNodeWithIds.getNodeReference().serialize()).toJson() }
137+
138+
assertEquals(expectedJsonWithIds, actualJsonWithIds)
139+
}
140+
}

streams/src/commonMain/kotlin/org/modelix/streams/IStream.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,9 @@ fun <A, B, R> IStream.Many<Pair<A, B>>.mapSecond(mapper: (B) -> R) = map { it.fi
135135

136136
fun IStream.Many<*>.isNotEmpty() = isEmpty().map { !it }
137137
fun <T> IStream.Many<T>.contains(element: T): IStream.One<Boolean> = this.filter { it == element }.isNotEmpty()
138+
139+
fun <T> IStream.Many<T>.forEach(action: (T) -> Unit): IStream.Completable = map(action).drainAll()
140+
fun <T> IStream.Many<T>.onEach(action: (T) -> Unit): IStream.Many<T> = map {
141+
action(it)
142+
it
143+
}

streams/src/commonMain/kotlin/org/modelix/streams/IStreamExecutor.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,8 @@ suspend fun <T> IStream.ZeroOrOne<T>.getSuspending(executor: IStreamExecutor): T
106106
fun <T> IStream.Many<T>.iterateBlocking(executor: IStreamExecutorProvider, visitor: (T) -> Unit): Unit = iterateBlocking(executor.getStreamExecutor(), visitor)
107107
fun <T> IStream.Many<T>.iterateBlocking(executor: IStreamExecutor, visitor: (T) -> Unit): Unit = executor.iterate({ this }, visitor)
108108

109+
fun IStream.Completable.executeBlocking(executor: IStreamExecutorProvider): Unit = executor.execute { this }
110+
suspend fun IStream.Completable.executeSuspending(executor: IStreamExecutorProvider): Unit = executor.executeSuspending { this }
111+
109112
suspend fun <T> IStream.Many<T>.iterateSuspending(executor: IStreamExecutorProvider, visitor: suspend (T) -> Unit): Unit = iterateSuspending(executor.getStreamExecutor(), visitor)
110113
suspend fun <T> IStream.Many<T>.iterateSuspending(executor: IStreamExecutor, visitor: suspend (T) -> Unit): Unit = executor.iterateSuspending({ this }, visitor)

0 commit comments

Comments
 (0)