Thomas Anderson is a computer programmer who maintains a double life as "Neo" the hacker. - Combination of Neo and Thomas
Theo is an open-source Neo4j Swift interface.
- CRUD operations for Nodes and Relationships
- Transaction statement execution
- Supports iOS, tvOS, macOS, watchOS and Linux
- macOS 14+ / iOS 17+ / tvOS 17+ / watchOS 10+ / Linux
- Swift 6.0+
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
Add the following line to your Package dependencies array:
.package(url: "https://github.com/Neo4j-Swift/Neo4j-Swift.git", from: "6.0.0")Run swift build to build your project, now with Theo included and ready to be used from your source
If you prefer just code-examples to get started, check out theo-example that is updated to match the current version of Theo.
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 the node
let node = Node(label: "Character", properties: ["name": "Thomas Anderson", "alias": "Neo" ])
// Save the node
do {
try await client.createNode(node: node)
print("Node saved successfully")
} catch {
print(error.localizedDescription)
}There's also createAndReturnNode() if you need the created node back, and createNodes() / createAndReturnNodes() for creating multiple nodes at once. Sync variants are available for all methods (e.g., createNodeSync()).
do {
if let foundNode = try await client.nodeBy(id: 42) {
print("Successfully found node \(foundNode)")
} else {
print("There was no node with id 42")
}
} catch {
print(error.localizedDescription)
}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
do {
try await client.updateNode(node: node)
print("Node updated successfully")
} catch {
print(error.localizedDescription)
}Likewise, given the variable 'node' with an existing node, when we no longer want the data, we might want to delete it all together:
do {
try await client.deleteNode(node: node)
print("Node deleted successfully")
} catch {
print(error.localizedDescription)
}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
"""
do {
try await client.executeCypher(query, params: ["id": Int64(id)])
print("Node deleted successfully")
} catch {
print("Something went wrong while deleting the node")
}let labels = ["Father", "Husband"]
let properties: [String:PackProtocol] = [
"firstName": "Niklas",
"age": 38
]
let nodes = try await client.nodesWith(labels: labels, andProperties: properties)
print("Found \(nodes.count) nodes")Given two nodes reader and writer, making a relationship with the type "follows" is easy:
try await client.relate(node: reader, to: writer, type: "follows")
print("Relationship successfully created")You can also create a relationship object directly:
let relationship = Relationship(fromNode: from, toNode: to, type: "Married to")
let created = try await client.createAndReturnRelationship(relationship: relationship)
print("Successfully created relationship \(created)")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.
Having fetched a relationship as part of a query, you can edit properties on that relationship:
relationship["someKey"] = "someValue"
relationship["otherKey"] = 42
let updated = try await client.updateAndReturnRelationship(relationship: relationship)
print("Successfully updated relationship \(updated)")And finally, you can remove the relationship altogether:
try await client.deleteRelationship(relationship: relationship)
print("Successfully deleted the relationship")Transactions allow you to run multiple operations atomically and roll back if something goes wrong:
try await client.executeAsTransaction { tx in
try await client.executeCypher("MATCH (n) SET n.abra = 'kadabra'")
try await client.executeCypher("MATCH (n:Person) WHERE n.name = 'Guy' SET n.likeable = true")
let result = try await client.executeCypher("MATCH (n:Person) WHERE n.name = 'Guy' AND n.abra='kadabra' SET n.starRating = 5")
if (result.stats.propertiesSetCount) == 0 {
tx.markAsFailed()
}
}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"]
do {
let result = try await client.executeCypher(query, params: params)
print("Successfully ran query with \(result.rows.count) rows")
} catch {
print("Got an error: \(error)")
}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.
- Select the unit test target
- Hit
CMD-U
- Niklas Saers (@niklassaers) (Theo v3-v6)
- Cory Wiles (@kwylez) (Theo v1-v3)
- Cory Benfield for all the help with with SwiftNIO and Transport Services
- Nigel Small for all the Bolt-related help
- Michael Hunger for help navigating the Neo4j community