Skip to content

Graph (Neo4j)

The API uses Neo4j for graph database operations, handling relationships between entities.

Overview

Neo4j stores:

  • User relationships (follows, friends)
  • Book relationships (similar, recommended)
  • Reading activity graphs
  • Social connections

CypherBuilder

The CypherBuilder class provides type-safe Cypher query construction.

typescript
import { CypherBuilder } from "./CypherBuilder"

const query = new CypherBuilder()
	.match("(u:User {id: $userId})")
	.match("(u)-[:FOLLOWS]->(f:User)")
	.return("f")
	.build()

GraphRepository

Base class for Neo4j operations:

typescript
export class UserGraphRepository extends GraphRepository<User> {
	async findFollowers(userId: string): Promise<User[]> {
		const cypher = new CypherBuilder()
			.match("(u:User {id: $userId})")
			.match("(follower:User)-[:FOLLOWS]->(u)")
			.return("follower")
			.build()

		return this.run(cypher, { userId })
	}

	async follow(followerId: string, followeeId: string): Promise<void> {
		const cypher = new CypherBuilder()
			.match("(follower:User {id: $followerId})")
			.match("(followee:User {id: $followeeId})")
			.merge("(follower)-[:FOLLOWS]->(followee)")
			.build()

		await this.run(cypher, { followerId, followeeId })
	}
}

Common Patterns

Creating Nodes

typescript
async createNode(book: Book): Promise<void> {
	const cypher = new CypherBuilder()
		.create("(b:Book $props)")
		.build()

	await this.run(cypher, {
		props: { id: book.id, title: book.title },
	})
}

Creating Relationships

typescript
async addToShelf(userId: string, bookId: string): Promise<void> {
	const cypher = new CypherBuilder()
		.match("(u:User {id: $userId})")
		.match("(b:Book {id: $bookId})")
		.merge("(u)-[:HAS_ON_SHELF]->(b)")
		.build()

	await this.run(cypher, { userId, bookId })
}

Finding Paths

typescript
async findRecommendations(userId: string): Promise<Book[]> {
	const cypher = new CypherBuilder()
		.match("(u:User {id: $userId})-[:FOLLOWS]->(friend:User)")
		.match("(friend)-[:HAS_ON_SHELF]->(b:Book)")
		.where("NOT (u)-[:HAS_ON_SHELF]->(b)")
		.return("DISTINCT b")
		.limit(10)
		.build()

	return this.run(cypher, { userId })
}

Connection Management

Neo4j connection is managed via dependency injection:

typescript
// bindings.ts
Container.bind(Driver).factory(() => neo4jDriver)

// Repository
constructor(@Inject private driver: Driver) {
	super(driver)
}

Testing

Mock the driver for unit tests:

typescript
const mockSession = {
	run: vi.fn().mockResolvedValue({ records: [] }),
	close: vi.fn(),
}

const mockDriver = {
	session: vi.fn().mockReturnValue(mockSession),
}

Built with VitePress