Skip to content
neode 1.0 alpha

Traversals

neode.traverse() builds multi-hop paths from a starting model, with a hop for each relationship you want to follow.

Hops

Each .hop() takes a relationship type, a direction, and the model on the far side. Pass degrees to follow the same relationship more than once.

const paths = await neode.traverse(Person)
  .hop('ACTED_IN', 'out', Movie)
  .hop('DIRECTED', 'in', Person, { degrees: 2 })
  .where({ name: 'Adam' })
  .run()

Filtering and running

.where() filters the starting nodes, and .run() executes the traversal and returns the paths. Each hydrated row is one path.

See it

The eager-read-models recipe walks the same idea from the write side: a Student, their Courses, and each Course's Lessons, hydrated in one read.

import {
  Direction,
  NodeEntity,
  OneToMany,
  Property,
  RelatedMany,
  RelatedOne,
  RelationshipCount,
  Uuid,
  fromEnv,
} from 'neode'

@NodeEntity()
class Lesson {
  @Uuid({ primary: true })
  id!: string

  @Property({ type: 'string' })
  slug!: string
}

@NodeEntity()
class Course {
  @Uuid({ primary: true })
  id!: string

  @Property({ type: 'string' })
  title!: string

  @Property({ type: 'string' })
  status!: string

  // Eager — hydrated whenever a Course is read, whatever the entry point
  @OneToMany(() => Lesson, 'HAS_LESSON', Direction.OUTGOING, true)
  lessons!: Lesson[]
}

@NodeEntity()
class Student {
  @Uuid({ primary: true })
  id!: string

  @Property({ type: 'string' })
  name!: string

  // Write model: depth-2 eager chain — Student → courses → each course's
  // lessons — and the key relate() resolves for enrolments and completions
  @OneToMany(() => Course, 'IN_COURSE', Direction.OUTGOING, true)
  courses!: Course[]

  @OneToMany(() => Lesson, 'COMPLETED_LESSON', Direction.OUTGOING)
  completed!: Lesson[]

  // Read model over the same relationship types, one query per read.
  // Recommended form: target() checks `fields` against the target, and a
  // Pick annotation keeps the DTO shape from drifting. Two fields → a
  // plain DTO; a single field would project the scalar.
  @RelatedOne({
    type: 'IN_COURSE',
    direction: 'out',
    target: () => Course,
    fields: ['title', 'status'],
  })
  course!: Pick<Course, 'title' | 'status'>

  @RelatedMany({ type: 'COMPLETED_LESSON', target: () => Lesson, fields: ['slug'] })
  completedSlugs!: string[]

  @RelationshipCount({ type: 'COMPLETED_LESSON', direction: 'out' })
  completedCount!: number
}

async function main(): Promise<void> {
  const neode = fromEnv()

  try {
    const course = await neode.create(Course, { title: 'Neo4j 101', status: 'ACTIVE' })
    const [intro, indexes] = await Promise.all([
      neode.create(Lesson, { slug: 'intro' }),
      neode.create(Lesson, { slug: 'indexes' }),
    ])
    const adam = await neode.create(Student, { name: 'Adam' })

    await neode.relate(Course, course, intro, 'lessons')
    await neode.relate(Course, course, indexes, 'lessons')
    await neode.relate(Student, adam, course, 'courses')
    await neode.relate(Student, adam, intro, 'completed')

    // One read — eager chain and projections arrive hydrated together
    const student = await neode.find(Student, adam.id)
    console.log(student?.courses[0]?.lessons.map((lesson) => lesson.slug)) // depth-2
    console.log(student?.course, student?.completedSlugs, student?.completedCount)

    await Promise.all([
      neode.delete(adam),
      neode.delete(course),
      neode.delete(intro),
      neode.delete(indexes),
    ])
  } finally {
    await neode.close()
  }
}

main().catch(console.error)
← Relationships Vector and full-text search →