RavenDB Client for Node.js
Installation
npm install --save ravendb
click here
Releases and Changelog -Documentation
Please find the official documentation on RavenDB Documentation page.
Getting started
- Require
DocumentStore
class from package
const { DocumentStore } = require('ravendb');
or (using ES6 / Typescript imports)
import { DocumentStore } from 'ravendb';
- Initialize document store (you should have one DocumentStore instance per application)
const store = new DocumentStore('http://live-test.ravendb.net', 'databaseName');
store.initialize();
- Open a session
const session = store.openSession();
- Call
saveChanges()
once you're done:
session
.load('users/1-A')
.then((user) => {
user.password = PBKDF2('new password');
})
.then(() => session.saveChanges())
.then(() => {
// data is persisted
// you can proceed e.g. finish web request
});
Supported asynchronous call types
- async / await
const session = store.openSession();
let user = await session.load('users/1-A');
user.password = PBKDF2('new password');
await session.saveChanges();
- Promises
session.load('Users/1-A')
.then((user) => {
user.password = PBKDF2('new password');
})
.then(() => session.saveChanges())
.then(() => {
// here session is complete
});
CRUD example
Storing documents
let product = {
title: 'iPhone X',
price: 999.99,
currency: 'USD',
storage: 64,
manufacturer: 'Apple',
in_stock: true,
last_update: new Date('2017-10-01T00:00:00')
};
await session.store(product, 'products/');
console.log(product.id); // Products/1-A
await session.saveChanges();
Loading documents
const product = await session.load('products/1-A');
console.log(product.title); // iPhone X
console.log(product.id); // Products/1-A
Loading documents with includes
const session = store.openSession();
// users/1
// {
// "name": "John",
// "kids": ["users/2", "users/3"]
// }
const user1 = await session
.include("kids")
.load("users/1");
// Document users/1 is going to be pulled along
// with docs referenced in "kids" field within a single request
const user2 = await session.load("users/2"); // this won't call server again
assert.ok(user1);
assert.ok(user2);
assert.equal(session.advanced.numberOfRequests, 1);
Updating documents
let product = await session.load('products/1-A');
product.in_stock = false;
product.last_update = new Date();
await session.saveChanges();
// ...
product = await session.load('products/1-A');
console.log(product.in_stock); // false
console.log(product.last_update); // 2018-06-05T13:52:31.633Z
Deleting documents
- Using entity
let product = await session.load('products/1-A');
await session.delete(product);
await session.saveChanges();
product = await session.load('products/1-A');
console.log(product); // undefined
- Using document ID
await session.delete('products/1-A');
Querying documents
- Use
query()
session method:
By collection:
const query = session.query({ collection: 'products' });
By index name:
const query = session.query({ indexName: 'productsByCategory' });
By index:
const query = session.query(Product, Product_ByName);
Using entity type:
import { User } from "./models";
const query = session.query(User);
- Build up the query - apply conditions, set ordering etc. Query supports chaining calls:
query
.waitForNonStaleResults()
.usingDefaultOperator('AND')
.whereEquals('manufacturer', 'Apple')
.whereEquals('in_stock', true)
.whereBetween('last_update', new Date('2017-10-01T00:00:00'), new Date())
.orderBy('price');
- Finally, you may get query results:
const results = await query.all();
// ...
const firstOne = await query.first(); // gets first result
// ...
const single = await query.single(); // gets single result
DocumentQuery methods overview
selectFields() - projections using a single field
// RQL
// from users select name
const userNames = await session.query({ collection: "users" })
.selectFields("name")
.all();
// John,Stefanie,Thomas
selectFields() - projections using multiple fields
// RQL
// from users select name, age
await session.query({ collection: "users" })
.selectFields([ "name", "age" ])
.all();
// [ { name: 'John', age: 30, id: 'users/1-A' },
// { name: 'Stefanie', age: 25, id: 'users/2-A' },
// { name: 'Thomas', age: 25, id: 'users/3-A' } ]
distinct()
// RQL
// from users select distinct age
await session.query({ collection: "users" })
.selectFields("age")
.distinct()
.all(); // [ 25, 30 ]
whereEquals() / whereNotEquals()
// RQL
// from users where age = 30
await session.query({ collection: "users" })
.whereEquals("age", 30)
.all();
// [ User {
// name: 'John',
// age: 30,
// kids: [...],
// registeredAt: 2017-11-10T23:00:00.000Z } ]
whereIn()
// RQL
// from users where name in ("John", "Thomas")
await session.query({ collection: "users" })
.whereIn("name", ["John", "Thomas"])
.all();
// [ User {
// name: 'John',
// age: 30,
// registeredAt: 2017-11-10T23:00:00.000Z,
// kids: [...],
// id: 'users/1-A' },
// User {
// name: 'Thomas',
// age: 25,
// registeredAt: 2016-04-24T22:00:00.000Z,
// id: 'users/3-A' } ]
whereStartsWith() / whereEndsWith()
// RQL
// from users where startsWith(name, 'J')
await session.query({ collection: "users" })
.whereStartsWith("name", "J")
.all();
// [ User {
// name: 'John',
// age: 30,
// kids: [...],
// registeredAt: 2017-11-10T23:00:00.000Z } ]
whereBetween()
// RQL
// from users where registeredAt between '2016-01-01' and '2017-01-01'
await session.query({ collection: "users" })
.whereBetween("registeredAt", new Date(2016, 0, 1), new Date(2017, 0, 1))
.all();
// [ User {
// name: 'Thomas',
// age: 25,
// registeredAt: 2016-04-24T22:00:00.000Z,
// id: 'users/3-A' } ]
whereGreaterThan() / whereGreaterThanOrEqual() / whereLessThan() / whereLessThanOrEqual()
// RQL
// from users where age > 29
await session.query({ collection: "users" })
.whereGreaterThan("age", 29);
.all();
// [ User {
// name: 'John',
// age: 30,
// registeredAt: 2017-11-10T23:00:00.000Z,
// kids: [...],
// id: 'users/1-A' } ]
whereExists()
Checks if the field exists.
// RQL
// from users where exists("age")
await session.query({ collection: "users" })
.whereExists("kids");
.all();
// [ User {
// name: 'John',
// age: 30,
// registeredAt: 2017-11-10T23:00:00.000Z,
// kids: [...],
// id: 'users/1-A' } ]
containsAny() / containsAll()
// RQL
// from users where kids in ('Mara')
await session.query({ collection: "users" })
.containsAll("kids", ["Mara", "Dmitri"]);
.all();
// [ User {
// name: 'John',
// age: 30,
// registeredAt: 2017-11-10T23:00:00.000Z,
// kids: ["Dmitri", "Mara"]
// id: 'users/1-A' } ]
search()
Performs full-text search.
// RQL
// from users where search(kids, 'Mara')
await session.query({ collection: "users" })
.search("kids", "Mara Dmitri");
.all();
// [ User {
// name: 'John',
// age: 30,
// registeredAt: 2017-11-10T23:00:00.000Z,
// kids: ["Dmitri", "Mara"]
// id: 'users/1-A' } ]
openSubclause() / closeSubclause()
// RQL
// from users where exists(kids) or (age = $p0 and name != $p1)
await session.query({ collection: "users" })
.whereExists("kids")
.orElse()
.openSubclause()
.whereEquals("age", 25)
.whereNotEquals("name", "Thomas")
.closeSubclause();
.all();
// [ User {
// name: 'John',
// age: 30,
// registeredAt: 2017-11-10T23:00:00.000Z,
// kids: ["Dmitri", "Mara"]
// id: 'users/1-A' },
// User {
// name: 'Stefanie',
// age: 25,
// registeredAt: 2015-07-29T22:00:00.000Z,
// id: 'users/2-A' } ]
not()
// RQL
// from users where age != 25
await session.query({ collection: "users" })
.not()
.whereEquals("age", 25)
.all();
// [ User {
// name: 'John',
// age: 30,
// registeredAt: 2017-11-10T23:00:00.000Z,
// kids: ["Dmitri", "Mara"]
// id: 'users/1-A' } ]
andAlso() / orElse()
// RQL
// from users where age != 25
await session.query({ collection: "users" })
.whereExists("kids")
.orElse()
.whereLessThan("age", 30)
.all();
// [ User {
// name: 'John',
// age: 30,
// registeredAt: 2017-11-10T23:00:00.000Z,
// kids: [ 'Dmitri', 'Mara' ],
// id: 'users/1-A' },
// User {
// name: 'Thomas',
// age: 25,
// registeredAt: 2016-04-24T22:00:00.000Z,
// id: 'users/3-A' },
// User {
// name: 'Stefanie',
// age: 25,
// registeredAt: 2015-07-29T22:00:00.000Z,
// id: 'users/2-A' } ]
usingDefaultOperator()
Sets default operator (which will be used if no andAlso()
/ orElse()
was called. Just after query instantiation, OR
is used as default operator. Default operator can be changed only adding any conditions.
orderBy() / randomOrdering()
// RQL
// from users order by age
await session.query({ collection: "users" })
.orderBy("age") // .randomOrdering()
.all();
// [ User {
// name: 'Stefanie',
// age: 25,
// registeredAt: 2015-07-29T22:00:00.000Z,
// id: 'users/2-A' },
// User {
// name: 'Thomas',
// age: 25,
// registeredAt: 2016-04-24T22:00:00.000Z,
// id: 'users/3-A' },
// User {
// name: 'John',
// age: 30,
// registeredAt: 2017-11-10T23:00:00.000Z,
// kids: [ 'Dmitri', 'Mara' ],
// id: 'users/1-A' } ]
take()
Limits the number of result entries to count
.
// RQL
// from users order by age
await session.query({ collection: "users" })
.orderBy("age")
.take(2)
.all();
// [ User {
// name: 'Stefanie',
// age: 25,
// registeredAt: 2015-07-29T22:00:00.000Z,
// id: 'users/2-A' },
// User {
// name: 'Thomas',
// age: 25,
// registeredAt: 2016-04-24T22:00:00.000Z,
// id: 'users/3-A' } ]
skip()
Skips first count
results.
// RQL
// from users order by age
await session.query({ collection: "users" })
.orderBy("age")
.take(1)
.skip(1)
.all();
// [ User {
// name: 'Thomas',
// age: 25,
// registeredAt: 2016-04-24T22:00:00.000Z,
// id: 'users/3-A' } ]
Getting query statistics
To obtain query statistics use statistics()
method.
let stats: QueryStatistics;
const results = await session.query({ collection: "users" })
.whereGreaterThan("age", 29)
.statistics(s => stats = s)
.all();
// QueryStatistics {
// isStale: false,
// durationInMs: 744,
// totalResults: 1,
// skippedResults: 0,
// timestamp: 2018-09-24T05:34:15.260Z,
// indexName: 'Auto/users/Byage',
// indexTimestamp: 2018-09-24T05:34:15.260Z,
// lastQueryTime: 2018-09-24T05:34:15.260Z,
// resultEtag: 8426908718162809000 }
all() / first() / single() / count()
all()
- returns all results
first()
- first result
single()
- first result, throws error if there's more entries
count()
- returns the count of the results (not affected by take()
)
Attachments
Store attachments
const doc = new User({ name: "John" });
// track entity
await session.store(doc);
// get read stream or buffer to store
const fileStream = fs.createReadStream("../photo.png"));
// store attachment using entity
session.advanced.attachments.store(doc, "photo.png", fileStream, "image/png");
// OR using document ID
session.advanced.attachments.store(doc.id, "photo.png", fileStream, "image/png");
await session.saveChanges();
Get attachments
const attachment = await session.advanced.attachments.get(documentId, "photo.png")
// attachment.details contains information about the attachment:
// {
// name: 'photo.png',
// documentId: 'users/1-A',
// contentType: 'image/png',
// hash: 'MvUEcrFHSVDts5ZQv2bQ3r9RwtynqnyJzIbNYzu1ZXk=',
// changeVector: '"A:3-K5TR36dafUC98AItzIa6ow"',
// size: 4579
// }
// attachment.data is a Readable https://nodejs.org/api/stream.html#stream_class_stream_readable
attachment.data
.pipe(fs.createWriteStream("photo.png"))
.on("finish", () => next());
Check if attachment exists
await session.advanced.attachments.exists(doc.id, "photo.png"));
// true
await session.advanced.attachments.exists(doc.id, "not_there.avi"));
// false
Get attachment names
// use a loaded entity to determine attachments' names
await session.advanced.attachments.getNames(doc);
// [ { name: 'photo.png',
// hash: 'MvUEcrFHSVDts5ZQv2bQ3r9RwtynqnyJzIbNYzu1ZXk=',
// contentType: 'image/png',
// size: 4579 } ]
TimeSeries
Store time series
// create document with time series
const session = store.openSession();
await session.store({ name: "John" }, "users/1");
const tsf = session.timeSeriesFor("users/1", "heartbeat");
tsf.append(new Date(), 120);
await session.saveChanges();
Get time series for document
// load time series by document by given name
const session = store.openSession();
const tsf = session.timeSeriesFor("users/1", "heartbeat");
const heartbeats = await tsf.get();
Bulk Insert
// create bulk insert instance using DocumentStore instance
const bulkInsert = store.bulkInsert();
// insert your documents
for (const name of ["Anna", "Maria", "Miguel", "Emanuel", "Dayanara", "Aleida"]) {
const user = new User({ name });
await bulkInsert.store(user);
}
// User { name: 'Anna', id: 'users/1-A' }
// User { name: 'Maria', id: 'users/2-A' }
// User { name: 'Miguel', id: 'users/3-A' }
// User { name: 'Emanuel', id: 'users/4-A' }
// User { name: 'Dayanara', id: 'users/5-A' }
// User { name: 'Aleida', id: 'users/6-A' }
// flush data and finish
await bulkInsert.finish();
Changes API
Listen for database changes e.g. document changes.
const changes = store.changes();
const docsChanges = changes.forAllDocuments();
docsChanges.on("data", change => {
// { type: 'Put',
// id: 'users/1-A',
// collectionName: 'Users',
// changeVector: 'A:2-QCawZTDbuEa4HUBORhsWYA' }
});
docsChanges.on("error", err => {
// handle errors
})
{
const session = store.openSession();
await session.store(new User({ name: "Starlord" }));
await session.saveChanges();
}
// ...
// dispose changes instance once you're done
changes.dispose();
Streaming
Stream documents with ID prefix
const userStream = await session.advanced.stream("users/");
// stream() method returns a Readable
userStream.on("data", user => {
// User { name: 'Anna', id: 'users/1-A' }
});
userStream.on("error", err => {
// handle errors
})
Stream query results
// create a query
const query = session.query({ collection: "users" }).whereGreaterThan("age", 29);
let stats;
// stream() returns a Readable
// get query stats passing a stats callback to stream() method
const queryStream = await session.advanced.stream(query, _ => stats = _);
queryStream.on("data", user => {
// User { name: 'Anna', id: 'users/1-A' }
});
// get stats using an event listener
queryStream.once("stats", stats => {
// { resultEtag: 7464021133404493000,
// isStale: false,
// indexName: 'Auto/users/Byage',
// totalResults: 1,
// indexTimestamp: 2018-10-01T09:04:07.145Z }
});
queryStream.on("error", err => {
// handle errors
});
Revisions
NOTE: Please make sure revisions are enabled before trying one of the below.
const session = store.openSession();
const user = {
name: "Marcin",
age: 30,
pet: "users/4"
};
await session.store(user, "users/1");
await session.saveChanges();
// modify the document to create a new revision
user.name = "Roman";
user.age = 40;
await session.saveChanges();
// get revisions
const revisions = await session.advanced.revisions.getFor("users/1");
// [ { name: 'Roman',
// age: 40,
// pet: 'users/4',
// '@metadata': [Object],
// id: 'users/1' },
// { name: 'Marcin',
// age: 30,
// pet: 'users/4',
// '@metadata': [Object],
// id: 'users/1' } ]
Suggestions
// users collection
// [ User {
// name: 'John',
// age: 30,
// registeredAt: 2017-11-10T23:00:00.000Z,
// kids: [Array],
// id: 'users/1-A' },
// and a static index like:
class UsersIndex extends AbstractJavaScriptIndexCreationTask {
constructor() {
super();
this.map(User, doc => {
return {
name: doc.name
}
});
this.suggestion("name");
}
}
// ...
const session = store.openSession();
const suggestionQueryResult = await session.query(User, UsersIndex)
.suggestUsing(x => x.byField("name", "Jon"))
.execute();
// { name: { name: 'name', suggestions: [ 'john' ] } }
Advanced patching
session.advanced.increment("users/1", "age", 1);
// increments *age* field by 1
session.advanced.patch("users/1", "underAge", false);
// sets *underAge* field to *false*
await session.saveChanges();
Subscriptions
// create a subscription
const subscriptionName = await store.subscriptions.create({
query: "from users where age >= 30"
});
// get subscription worker for your subscription
const subscription = store.subscriptions.getSubscriptionWorker({ subscriptionName });
subscription.on("error", err => {
// handle errors
});
subscription.on("batch", (batch, callback) => {
try {
// do batch processing on batch.items
// batch.items:
// [ Item {
// changeVector: 'A:2-r6nkF5nZtUKhcPEk6/LL+Q',
// id: 'users/1-A',
// rawResult:
// { name: 'John',
// age: 30,
// registeredAt: '2017-11-11T00:00:00.0000000',
// kids: [Array],
// '@metadata': [Object],
// id: 'users/1-A' },
// rawMetadata:
// { '@collection': 'Users',
// '@nested-object-types': [Object],
// 'Raven-Node-Type': 'User',
// '@change-vector': 'A:2-r6nkF5nZtUKhcPEk6/LL+Q',
// '@id': 'users/1-A',
// '@last-modified': '2018-10-18T11:15:51.4882011Z' },
// exceptionMessage: undefined } ]
// ...
// call the callback, once you're done
callback();
} catch(err) {
// if processing fails for a particular batch
// pass the error to the callback
callback(err);
}
});
Using object literals for entities
In order to comfortably use object literals as entities set the function getting collection name based on the content of the object - store.conventions.findCollectionNameForObjectLiteral()
.
const store = new DocumentStore(urls, database);
store.conventions.findCollectionNameForObjectLiteral = entity => entity["collection"];
// ...
store.initialize();
This needs to be done before an initialize()
call on DocumentStore
instance. If you fail to do so, your entites will land up in @empty collection having an UUID for an ID. E.g.
Using classes for entities
- Define your model as class. Attributes should be just public properties:
export class Product {
constructor(
id = null,
title = '',
price = 0,
currency = 'USD',
storage = 0,
manufacturer = '',
in_stock = false,
last_update = null
) {
Object.assign(this, {
title,
price,
currency,
storage,
manufacturer,
in_stock,
last_update: last_update || new Date()
});
}
}
- To store a document pass its instance to
store()
. Collection name will be detected automatically using entity's class name.
import { Product } from "./models";
let product = new Product(
null, 'iPhone X', 999.99, 'USD', 64, 'Apple', true, new Date('2017-10-01T00:00:00'));
product = await session.store(product);
console.log(product instanceof Product); // true
console.log(product.id.includes('products/')); // true
await session.saveChanges();
- When loading document, you can use
session.load()
. Pass class constructor as a second argument:
let product = await session.load('products/1-A', Product);
console.log(product instanceof Product); // true
console.log(product.id); // Products/1-A
NOTE: To limit passing class constructors around, register the type in document store's conventions like so:
import { Product } from "./models";
const store = new DocumentStore(url, dbName);
store.conventions.registerEntityType(Product);
// ...
let product = await session.load('products/1-A');
console.log(product instanceof Product); // true
console.log(product.id); // Products/1-A
- When querying documents, you pass class constructor as
documentType
option ofsession.query({ ... })
:
let products = await session.query({
collection: 'products',
documentType: Product // you can drop this if type has been registered in conventions using store.conventions.registerEntityType()
}).all();
products.forEach((product) => {
console.log(product instanceof Product); // true
console.log(product.id.includes('Products/')); // true
});
Usage with TypeScript
TypeScript typings are embedded into the package (see types
property in package.json
).
// file models/product.ts
export class Product {
constructor(
public id: string = null,
public title: string = '',
public price: number = 0,
public currency: string = 'USD',
public storage: number = 0,
public manufacturer: string = '',
public in_stock: boolean = false,
public last_update: Date = null
) {}
}
// file app.ts
import {Product} from "models/product";
import {DocumentStore, IDocumentStore, IDocumentSession, IDocumentQuery, DocumentConstructor, QueryOperators} from 'ravendb';
const store: IDocumentStore = new DocumentStore('url', 'database name');
store.conventions.registerEntityType(Product);
let session: IDocumentSession;
store.initialize();
(async (): Promise<void> => {
let product = new Product(
null, 'iPhone X', 999.99, 'USD', 64, 'Apple', true, new Date('2017-10-01T00:00:00')
);
await session.store<Product>(product);
await session.saveChanges();
console.log(product instanceof Product); // true
console.log(product.id.includes('products/')); // true
product = await session.load<Product>('products/1-A');
console.log(product instanceof Product); // true
console.log(product.id); // products/1-A
let products: Product[] = await session
.query<Product>({ collection: 'Products' })
.waitForNonStaleResults()
.whereEquals('manufacturer', 'Apple')
.whereEquals('in_stock', true)
.whereBetween('last_update', new Date('2017-10-01T00:00:00'), new Date())
.whereGreaterThanOrEqual('storage', 64)
.all();
products.forEach((product: Product): void => {
console.log(product instanceof Product); // true
console.log(product.id.includes('products/')); // true
});
})();
Working with secured server
- Fill auth options object. Pass contents of the pem/pfx certificate, specify its type and (optionally) a passphrase:
const {DocumentStore, Certificate} = require('ravendb');
const certificate = `
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
`;
let authOptions = {
certificate,
type: "pem",
password: "my passphrase" // optional
};
PFX certificates content should be passed as Buffer
object:
const {DocumentStore} = require('ravendb');
const fs = require('fs');
const certificate = './cert.pfx';
let authOptions = {
certificate: fs.readFileSync(certificate),
type: "pfx",
password: 'my passphrase' // optional
};
- Pass auth options as third argument to
DocumentStore
constructor:
let store = new DocumentStore('url', 'databaseName', authOptions);
store.initialize();
Building
npm install
npm run build
Running tests
# To run the suite one needs to set the following environment variables:
#
# - the location of RavenDB server binary:
# RAVENDB_TEST_SERVER_PATH="C:\\work\\test\\Server\\Raven.Server.exe"
#
# - certificate path for tests requiring secure server
# RAVENDB_TEST_SERVER_CERTIFICATE_PATH="C:\\work\\test\\cluster.server.certificate.pfx"
#
# - certificate hostname
# RAVENDB_TEST_SERVER_HOSTNAME="a.nodejstest.development.run"
#
npm test