Migration to v2
GraphDB v2 is a ground-up rewrite. This guide covers every breaking change with migration examples.
Timestamp field rename
The updateAt field has been renamed to updatedAt (with the “d”).
// v1doc.updateAt; // Date object
// v2doc.updatedAt; // number (epoch ms)Timestamps are numbers
Timestamps are now epoch milliseconds (number), not Date objects. This removes the date-fns dependency.
// v1doc.createdAt; // Datedoc.updateAt; // Date
// v2doc.createdAt; // 1700000000000doc.updatedAt; // 1700000000000
// Convert to Date if needednew Date(doc.createdAt);query() always returns an array
In v1, query() could return a single document or null. In v2, it always returns Doc<T>[].
// v1const result = col.query({ name: 'Alex' }); // Doc<T> | Doc<T>[] | null
// v2const result = col.query({ name: 'Alex' }); // Doc<T>[] (always)Use the new findOne() method to get a single document:
// v2 — get a single documentconst user = col.findOne({ name: 'Alex' }); // Doc<T> | nullZero runtime dependencies
GraphDB v2 has no runtime dependencies:
- Removed
uuid: Usescrypto.randomUUID()(Node 18+, all Bun versions) - Removed
date-fns: UsesDate.now()for epoch milliseconds
Listener payloads changed
Event payloads are now typed objects instead of positional arguments.
// v1col.on('create', (doc) => { ... });col.on('update', (before, after) => { ... });col.on('remove', (doc) => { ... });
// v2col.on('create', ({ doc }) => { ... });col.on('update', ({ before, after, patch }) => { ... });col.on('remove', ({ doc }) => { ... });col.on('populate', ({ count }) => { ... }); // newcol.on('syncError', ({ op, error, docId }) => { ... }); // newSkip edge cases fixed
// v1 — skip: 0 might have been treated as falsycol.query({}, { skip: 0 }); // inconsistent
// v2 — skip: 0 is valid, returns all resultscol.query({}, { skip: 0 }); // returns all docs
// v2 — skip >= length returns empty arraycol.query({}, { skip: 100 }); // [] (not undefined/error)Query pipeline order fixed
The query pipeline is now consistently: filter -> sort -> skip -> limit.
// v2 — sort happens before skip/limitcol.query({}, { orderBy: { age: 'ASC' }, skip: 1, limit: 2,});// Filters first, sorts by age, then skips 1 and takes 2Multi-field sort fixed
Multi-field sort now correctly evaluates keys in order. The first non-zero comparison decides the order.
col.query({}, { orderBy: { lastName: 'ASC', age: 'ASC' },});// Sorts by lastName first, then by age within same lastNameTop-level RegExp in where clause
RegExp now works at the top level of where clauses:
// v2 — both forms workcol.query({ name: /^al/i }); // top-level RegExpcol.query({ name: { match: /^al/i } }); // operator formAsync/sync error handling
Sync errors are no longer swallowed. Failed syncs properly revert the optimistic write and throw.
// v2 — sync failures throw and reverttry { await col.create({ name: 'Alex', ... });} catch (err) { // Document was NOT persisted (reverted) // syncError event was also emitted}Populate validates _id
populate() now validates that every document has an _id field. Duplicates overwrite (last wins).
// v2 — throws if any doc is missing _idcol.populate([ { _id: '1', name: 'Alex', ... }, // ok { name: 'No ID', ... }, // throws!]);Map/Set for listeners
Listeners use Map and Set internally for O(1) unsubscribe performance instead of arrays.
// v2 — cancel function removes listener in O(1)const cancel = col.on('create', handler);cancel(); // O(1) removal, no array spliceNew APIs
These methods are new in v2:
| Method | Description |
|---|---|
findOne(where) | Returns Doc<T> | null |
count(where?) | Returns number of matching documents |
exists(id) | Returns boolean |
clear() | Removes all documents from collection |
updateMany(where, patch) | Updates all matching documents |
removeMany(where) | Removes all matching documents |
on('populate', fn) | Listen for bulk populate events |
on('syncError', fn) | Listen for sync errors |