in backend/app/services/annotations/Neo4jAnnotations.scala [406:461]
private def move(currentUser: String, workspaceId: String, itemId: String, newWorkspaceId: String, newParentId: String): Attempt[MoveItemResult] = attemptTransaction { tx =>
tx.run(
"""
|MATCH (:WorkspaceNode)<-[oldParentLink:PARENT]-(item: WorkspaceNode {id: {itemId}})-[:PART_OF]->(oldWorkspace: Workspace {id: {workspaceId}})
| WHERE (:User { username: {currentUser} })-[:FOLLOWING]->(oldWorkspace) OR oldWorkspace.isPublic
|
|MATCH (newParent :WorkspaceNode {id: {newParentId}})-[:PART_OF]->(newWorkspace :Workspace {id: {newWorkspaceId}})
| WHERE (:User { username: {currentUser} })-[:FOLLOWING]->(newWorkspace) OR newWorkspace.isPublic
|
|WITH oldParentLink, oldWorkspace, newParent, newWorkspace, item, EXISTS((newParent)-[:PARENT*0..]->(item)) as isNewParentDescendantOfItem
| WHERE isNewParentDescendantOfItem = false
|
| // The zero-length lower bound on the path matches the item itself as well
| // https://neo4j.com/docs/developer-manual/3.3/cypher/clauses/match/#zero-length-paths
| MATCH (itemAndItsDescendants :WorkspaceNode)-[:PARENT*0..]->(item)
|
| // we only want to delete and re-create PART_OF links if we're moving between workspaces
| OPTIONAL MATCH (:Workspace)<-[oldPartOfLinks:PART_OF]-(itemAndItsDescendants)
| WHERE oldWorkspace <> newWorkspace
|
| DELETE oldPartOfLinks
| DELETE oldParentLink
| MERGE (itemAndItsDescendants)-[:PART_OF]->(newWorkspace)
| MERGE (item)-[:PARENT]->(newParent)
| RETURN itemAndItsDescendants
""".stripMargin,
parameters(
"currentUser", currentUser,
"workspaceId", workspaceId,
"itemId", itemId,
"newWorkspaceId", newWorkspaceId,
"newParentId", newParentId
)
).flatMap { statementResult =>
val records = statementResult.list().asScala.toList
val entriesMoved = records.map(_.get("itemAndItsDescendants"))
val resourcesMoved = entriesMoved.flatMap(AffectedResource.fromNeo4jValue)
val relationshipsCreated = statementResult.summary().counters().relationshipsCreated()
val relationshipsDeleted = statementResult.summary().counters().relationshipsDeleted()
if (entriesMoved.isEmpty) {
// checking r.single().get("isNewParentDescendantOfItem").asBoolean() would be helpful for reporting the
// cause of the error, but because of the WITH/WHERE we get an empty result set if it's true
Attempt.Left(NotFoundFailure("Could not find node to move or destination node"))
} else if (relationshipsCreated != relationshipsDeleted) {
Attempt.Left(
IllegalStateFailure(s"Failed to move item. $relationshipsCreated relationships created and $relationshipsDeleted deleted, so change was rolled back.")
)
} else {
entriesMoved.foreach(entryMoved => {
logger.info(s"Moved workspace item ${entryMoved.get("name")} with id ${entryMoved.get("id")} from workspace $workspaceId to workspace $newWorkspaceId under new parent $newParentId. $relationshipsCreated relationships created and $relationshipsDeleted deleted")
})
Attempt.Right(MoveItemResult(resourcesMoved))
}
}
}