Cursors

Cursor<T> is the result type for queries. It wraps a Kotlin Flow<T> and provides 29 suspend convenience extensions that mirror stdlib collection operations.

Flow-based iteration

At its core, a Cursor is a Flow<T>. Results are deserialized lazily, chunk by chunk, so you can stream large result sets without loading everything into memory at once:

val cursor: Cursor<Stored<Person>> = repo.findAll()

// Flow-based streaming
cursor.asFlow().collect { stored ->
    println(stored().name)
}

// Metadata
cursor.count      // number of results returned
cursor.fullCount  // total matches (when using pagination)
cursor.timeMs     // query execution time in milliseconds

Convenience extensions

You rarely need to call asFlow() directly. Vault provides 29 suspend extension functions that work just like their counterparts in the Kotlin standard library. The key difference: every operation is a suspend function, so it works safely in coroutine contexts.

Collect

val cursor = repo.findAll()

// Collect all results into a list
val all: List<Stored<Person>> = cursor.toList()

Transform

// map — transform each item
val names: List<String> = cursor.map { it().name }

// mapNotNull — transform, discarding nulls
val emails: List<String> = cursor.mapNotNull { it().email }

// mapIndexed — transform with index
val indexed: List<String> = cursor.mapIndexed { i, it -> it().name }

// flatMap — transform each item into a collection, then flatten
val tags: List<String> = cursor.flatMap { it().tags }

Filter

// filter — keep matching items
val adults: List<Stored<Person>> = cursor.filter { it().age >= 18 }

// filterNot — keep non-matching items
val minors: List<Stored<Person>> = cursor.filterNot { it().age >= 18 }

// filterIsInstance — keep items of a specific type
val dogs: List<Stored<Dog>> = animalCursor.filterIsInstance<Stored<Dog>>()

Element access

// first / firstOrNull
val first: Stored<Person> = cursor.first()
val maybeFirst: Stored<Person>? = cursor.firstOrNull()

// With predicate
val alice: Stored<Person>? = cursor.firstOrNull { it().name == "Alice" }

// find — alias for firstOrNull with predicate
val found: Stored<Person>? = cursor.find { it().age > 30 }

// lastOrNull
val last: Stored<Person>? = cursor.lastOrNull()

Iteration

// forEach
cursor.forEach { stored -> println(stored().name) }

// forEachIndexed
cursor.forEachIndexed { index, stored ->
    println(stored().name)
}

Predicates

// any — true if at least one item matches
val hasAdults: Boolean = cursor.any { it().age >= 18 }

// none — true if no item matches
val noMinors: Boolean = cursor.none { it().age < 18 }

// all — true if every item matches
val allAdults: Boolean = cursor.all { it().age >= 18 }

Aggregation

// fold
val totalAge: Int = cursor.fold(0) { acc, stored -> acc + stored().age }

// groupBy
val byCity: Map<String, List<Stored<Person>>> = cursor.groupBy { it().city }

// associateBy
val byId: Map<String, Stored<Person>> = cursor.associateBy { it._id }

// associate
val nameToAge: Map<String, Int> = cursor.associate { it().name to it().age }

// partition
val (adults, minors) = cursor.partition { it().age >= 18 }

Sorting, distinct, take/drop

// sortedBy / sortedByDescending
val byAge: List<Stored<Person>> = cursor.sortedBy { it().age }
val oldest: List<Stored<Person>> = cursor.sortedByDescending { it().age }

// distinct / distinctBy
val unique: List<Stored<Person>> = cursor.distinct()
val uniqueNames: List<Stored<Person>> = cursor.distinctBy { it().name }

// take / drop
val firstFive: List<Stored<Person>> = cursor.take(5)
val afterFive: List<Stored<Person>> = cursor.drop(5)

Entity caching

For Cursor<Stored<T>>, the cache() extension collects results and registers each entity in the cursor's EntityCache. This is useful when you know you will resolve Ref fields that point back to entities in the same result set:

val persons: List<Stored<Person>> = repo.findAll().cache()
// All persons are now in the EntityCache — Ref resolution can use them

Comparison with stdlib

If you already know Kotlin's standard collection operations, Vault cursors work the same way. The only difference is that each operation is suspend:

Iterable<T> Cursor<T> Difference
list.map { ... } cursor.map { ... } suspend, lambda can be suspend
list.filter { ... } cursor.filter { ... } suspend, lambda can be suspend
list.first() cursor.first() suspend
list.forEach { ... } cursor.forEach { ... } suspend, lambda can be suspend
list.groupBy { ... } cursor.groupBy { ... } suspend, lambda can be suspend
list.toList() cursor.toList() suspend