private def move()

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))
      }
    }
  }