Theo

master

Open Source Neo4j driver for iOS, tvOS, Linux and macOS
Neo4j-Swift/Neo4j-Swift

Theo

Thomas Anderson is a computer programmer who maintains a double life as "Neo" the hacker. - Combination of Neo and Thomas

Summary

Theo is an open-source Neo4j Swift interface.

Features

  • CRUD operations for Nodes and Relationships
  • Transaction statement execution
  • Supports iOS, tvOS, macOS and Linux

Requirements

  • iOS 12.2 or higher / macOS 10.14 or higher / Ubuntu Linux 16.04 or higher
  • Xcode 10.2.1 or newer for iOS or macOS
  • Swift 5.0

Feedback

Because this framework is open source it is best for most situations to post on Stack Overflow and tag it Theo. If you do find a bug please file an issue or issue a PR for any features or fixes. You are also most welcome to join the conversation in the #neo4j-swift channel in the neo4j-users Slack

Installation

You can install Theo in a number of ways

Swift Package Manager

Add the following line to your Package dependencies array:

.package(url: "https://github.com/Neo4j-Swift/Neo4j-Swift.git", from: "5.0.0")

Run swift build to build your project, now with Theo included and ready to be used from your source

CococaPods

Add the following to your Podfile:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, ‘12.2
use_frameworks!

target '<Your Target Name>' do
  pod ‘Theo’
end

Run pod install to configure your updated workspace. Open the .xcworkspace generated, your project is now ready to use Theo

git submodule

  1. Add it as a submodule to your existing project. git submodule add git@github.com:Neo4j-Swift/Neo4j-Swift.git
  2. Through Terminal, navigate to the submodule directory and run swift package fetch. Theo has other dependencies and they need to be fetched.
  3. Open the Theo folder, and drag Theo.xcodeproj into the file navigator of your Xcode project.
  4. In Xcode, navigate to the target configuration window by clicking on the blue project icon, and selecting the application target under the "Targets" heading in the sidebar.
  5. In the tab bar at the top of that window, open the "Build Phases" panel.
  6. Expand the "Link Binary with Libraries" group, Copy Frameworks and add Theo.framework, Bolt.framework, SSLService.framework, Socket.framework, PacketStream.framework.
  7. Click on the + button at the top left of the panel and select "New Copy Files Phase". Rename this new phase to "Copy Frameworks", set the "Destination" to "Frameworks", and add the frameworks.

Usage

If you prefer just code-examples to get started, check out theo-example that is updated to match the current version of Theo.

Initalization

To get started, you need to set up a BoltClient with the connection information to your Neo4j instance. You could for instance load a JSON into a dictionary, and then pass any values that should overrid the defaults, like this:

let config = ["password": "<passcode>"]
let client = try BoltClient(JSONClientConfiguration(json: config))

Or you can provide your on ClientConfiguration-based class, or even set them all manually:

let client = try BoltClient(hostname: "localhost",
                                port: 6787,
                            username: "neo4j",
                            password: "<passcode>",
                           encrypted: true)

Create and save a node

// Create the node
let node = Node(label: "Character", properties: ["name": "Thomas Anderson", "alias": "Neo" ])

// Save the node
let result = client.createNodeSync(node: node)

// Verify the result of the save
switch result {
case let .failure(error):
  print(error.localizedDescription)
case .success(_):
  print("Node saved successfully")
}

createNodeSync() above has an async sibling createNode(), and they in turn have siblings that return the created node: createAndReturnNode() and creaetAndReturnNodeSync(). Finally, multiple nodes can be created at the same time, giving you the functions createNodes(), createNodesSync(), createAndReturnNodes() and createAndReturnNodesSync() to choose between.

Fetch a node via id

client.nodeBy(id: 42) { result in
  switch result {
  case let .failure(error):
    print(error.localizedDescription)
  case let .success(foundNode):
    if let foundNode = foundNode {
	  print("Successfully found node \(foundNode)")
    } else {
	  print("There was no node with id 42")
	}
  }
}

So, finding the node with id 42 is easy, but there is a little routine work in handling that there could be an error with connecting to the database, or there might not be a node with id 42.

Living dangerously and ignoring both error scenarios would look like this:

client.nodeBy(id: 42) { result in
  let foundNode = result.value!!
  print("Successfully found node \(foundNode)")
}

Updating a node

Given the variable 'node' with an existing node, we might want to update it. Let's add a label:

node.add(label: "AnotherLabel")

or add a few properties:

node["age"] = 42
node["color"] = "white"

and then

let result = client.updateNodeSync(node: node)
switch result {
case let .failure(error):
  print(error.localizedDescription)
case .success(_):
  print("Node updated successfully")
}

Deleting a node

Likewise, given the variable 'node' with an existing node, when we no longer want the data, we might want to delete it all together:

let result = client.deleteNodeSync(node: node)
switch result {
case let .failure(error):
  print(error.localizedDescription)
case .success(_):
  print("Node deleted successfully")
}

Note that in Neo4j, to delete a node all relationships this node participates in should be deleted first. However, you can force a delete by calling "DETACH DELETE", and it will then remove all the relationships the node participates in as well. Since this is an exception to the rule, there is no helper function for this. But with Theo, running an arbitrary Cypher statement is easy:

guard let id = node.id else { return }
let query = """
            MATCH (n) WHERE id(n) = {id} DETACH DELETE n
            """
if client.executeCypherSync(query, params: [ "id": Int64(id)] ).isSuccess {
  print("Node deleted successfully")
} else {
  print("Something went wrong while deleting the node")
}

Fetch nodes matching a labels and property values

let labels = ["Father", "Husband"]
let properties: [String:PackProtocol] = [
    "firstName": "Niklas",
    "age": 38
]

client.nodesWith(labels: labels, andProperties: properties) { result in
  print("Found \(result.value?.count ?? 0) nodes")
}

Create a relationship

Given two nodes reader and writer, making a relationship with the type "follows" is easy as

let result = client.relateSync(node: reader, to: writer, type: "follows")
if result.isSuccess {
  print("Relationship successfully created")
}

Again, there is an async version of relateSync() called relate() that takes the same parameters and a callback block with the same result as relateSync returned

You can also make a relationship directly and create that:

let relationship = Relationship(fromNode: from, toNode: to, type: "Married to")
client.createAndReturnRelationship(relationship: relationship) { result in
  switch result {
  case let .failure(error):
    print(error.localizedDescription)
  case let .success(relationship):
    print("Successfully created relationship \(relationship)")
  }
}

Do note that if one or both of the nodes in a relationship have not been created in advance, they will be created together with the relationship

Updating properties on a relationship

Having fetched a relationship as part of a query, you can now edit properties on that relationship:

relationship["someKey"] = "someValue"
relationship["otherKey"] = 42
let result = client.updateAndReturnRelationshipSync(relationship: relationship)
switch result {
case let .failure(error):
  print(error.localizedDescription)
case let .success(relationship):
  print("Successfully updated relationship \(relationship)")
}

Deleting a relationship

And finally, you can remove the relationship alltogether:

let result = client.deleteRelationshipSync(relationship: relationship)
switch result {
case let .failure(error):
  print(error.localizedDescription)
case .success(_):
  print("Successfully deleted the relationship")
}

Execute a transaction

It is easy to make a transaction, and to roll it back if you are not happy with its results. Simply call executeAsTransaction() and pass in a block. This block has a parameter, tx in the example below, where you can invalidate the transaction at any point. If it has not been invalidated, it is considered successful and committed at the end of the transaction block.

try client.executeAsTransaction() { tx in
  client.executeCypherSync("MATCH (n) SET n.abra = \"kadabra\"")
  client.executeCypherSync("MATCH (n:Person) WHERE n.name = 'Guy' SET n.likeable = true")
  let finalResult = client.executeCypherSync("MATCH (n:Person) WHERE n.name = 'Guy' AND n.abra='kadabra' SET n.starRating = 5")
  if (finalResult.value?.stats.propertiesSetCount ?? 0) == 0 {
	tx.markAsFailed()
  }
}

Execute a cypher query

In the example above, we already executed a few cypher queries. In the following example, we execute a longer cypher example with named parameters, where we'll supply the parameters along side the query:

let query = """
            MATCH (u:User {username: {user} }) WITH u 
            MATCH (u)-[:FOLLOWS*0..1]->(f) WITH DISTINCT f,u 
            MATCH (f)-[:LASTPOST]-(lp)-[:NEXTPOST*0..3]-(p) 
            RETURN p.contentId as contentId, p.title as title, p.tagstr as tagstr, p.timestamp as timestamp, p.url as url, f.username as username, f=u as owner
            """
let params: [String:PackProtocol] = ["user": "ajordan"]
let result = client.executeCypherSync(query, params: params)
if result.isSuccess {
  print("Successfully ran query")
} else {
  print("Got an error")
}

Integration Tests

Setup

There is a file called, TheoBoltConfig.json.example which you should copy to TheoBoltConfig.json. You can edit this configuration with connection settings to your Neo4j instance, and the test classes using these instead of having to modify any actual class files. TheoBoltConfig.json is in the .gitignore so you don't have to worry about creds being committed.

Execution

  • Select the unit test target
  • Hit CMD-U

Authors

Special thanks to

Description

  • Swift Tools 5.2.0
View More Packages from this Author

Dependencies

Last updated: Fri Apr 26 2024 09:47:28 GMT-0900 (Hawaii-Aleutian Daylight Time)