npm install --save ravendb
- Require
DocumentStore
class from package
const {DocumentStore} = require('ravendb');
or
const DocumentStore = require('ravendb').default;
or (using ES6 / Typescript imports)
import DocumentStore from 'ravendb';
- Initialize document store (this can be kept as a global object)
const store = DocumentStore.create('database url', 'default database name');
store.initialize();
- Open a session
const session = store.openSession();
- Call
saveChanges()
when you'll finish working with a session
session
.load('Users/1-A')
.then((user) => {
user.password = PBKDF2('new password');
return session.store(user);
})
.then(() => session.saveChanges())
.then(() => {
// here you can finish request
});
- You can use callbacks
session
.load('Users/1-A', (user) => {
user.password = PBKDF2('new password');
session.store(user, null, () => {
session.saveChanges(() => {
// here session is complete
});
});
})
- You can use promises as well
session
.load('Users/1-A')
.then((user) => {
user.password = PBKDF2('new password');
return session.store(user);
})
.then(() => session.saveChanges())
.then(() => {
// here session is complete
});
- With
co
library or frameworks using it (such asAdonisJS
) you canyield
calls
const co = require('co');
// ...
co(function * () {
session = store.openSession();
let user = yield store.load('Users/1-A');
user.password = PBKDF2('new password');
yield session.store(user);
yield session.saveChanges();
// here session is complete
});
- Also client is supporting
async
/await
stuff
async () => {
session = store.openSession();
let user = await store.load('Users/1-A');
user.password = PBKDF2('new password');
await session.store(user);
await session.saveChanges();
// here session is complete
})
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')
};
product = await session.store(product, 'Products/');
console.log(product.id); // will output Products/<some number>-<some letter (server node tag)> e.g. Products/1-A
await session.saveChanges();
product = await session.load('Products/1-A');
console.log(product.title); // iPhone X
console.log(product.id); // Products/1-A
product = await session.load('Products/1-A');
product.in_stock = false;
product.last_update = new Date();
await session.store(product);
await session.saveChanges();
product = await session.load('Products/1-A');
console.log(product.in_stock); // false
console.log(product.last_update); // outputs current date
product = await session.load('Products/1-A');
await session.delete(product);
// or you can just do
// await session.delete('Products/1-A');
await session.saveChanges();
product = await session.load('Products/1-A');
console.log(product); // undefined
- Create
DocumentQuery
instance usingquery()
method of session:
query = session.query({
collection: 'Products', // specify which collection you'd like to query
// optionally you may specify an index name for querying
// indexName: 'PopularProductsWithViewsCount'
});
- Apply conditions, ordering etc. Query supports chaining calls:
const {DocumentStore, QueryOperators} = require('ravendb');
// ...
query
.waitForNonStaleResults()
.usingDefaultOperator(QueryOperators.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:
let documents = await query.all();
Method | RQL / description |
---|---|
selectFields(fields: string[],
projections?: string[]): IDocumentQuery<T>; |
SELECT field1 [AS projection1], ... |
distinct(): IDocumentQuery<T>; |
SELECT DISTINCT |
whereEquals
<V extends ConditionValue>(
fieldName: string, value: V,
exact?: boolean)
: IDocumentQuery<T>; |
WHERE fieldName = <value> |
whereNotEquals
<V extends ConditionValue>(
fieldName: string, value: V,
exact?: boolean)
: IDocumentQuery<T>; |
WHERE fieldName != <value> |
whereIn
<V extends ConditionValue>(
fieldName: string, values: V[],
exact?: boolean)
: IDocumentQuery<T>; |
WHERE fieldName IN (<value1>, <value2>, ...) |
whereStartsWith
<V extends ConditionValue>(
fieldName: string, value: V)
: IDocumentQuery<T>; |
WHERE startsWith(fieldName, '<value>') |
whereEndsWith
<V extends ConditionValue>(
fieldName: string, value: V)
: IDocumentQuery<T>; |
WHERE endsWith(fieldName, '<value>') |
whereBetween
<V extends ConditionValue>(
fieldName: string, start: V, end: V,
exact?: boolean) : IDocumentQuery<T>; |
WHERE fieldName BETWEEN <start> AND <end> |
whereGreaterThan
<V extends ConditionValue>(
fieldName: string, value: V,
exact?: boolean)
: IDocumentQuery<T>; |
WHERE fieldName > <value> |
whereGreaterThanOrEqual
<V extends ConditionValue>(
fieldName: string, value: V,
exact?: boolean)
: IDocumentQuery<T>; |
WHERE fieldName >= <value> |
whereLessThan
<V extends ConditionValue>(
fieldName: string, value: V,
exact?: boolean)
: IDocumentQuery<T>; |
WHERE fieldName < <value> |
whereLessThanOrEqual
<V extends ConditionValue>(
fieldName: string, value: V,
exact?: boolean)
: IDocumentQuery<T>; |
WHERE fieldName <= <value> |
whereExists(fieldName: string)
: IDocumentQuery<T>; |
WHERE exists(fieldName) |
containsAny
<V extends ConditionValue>(
fieldName: string, values: V[])
: IDocumentQuery<T>; |
WHERE fieldName IN (<value1>, <value2>, ...) |
containsAll
<V extends ConditionValue>(
fieldName: string, values: V[])
: IDocumentQuery<T>; |
WHERE fieldName ALL IN (<value1>, <value2>, ...) |
search(fieldName: string,
searchTerms: string,
operator?: SearchOperator)
: IDocumentQuery<T>; |
Performs full-text search |
openSubclause(): IDocumentQuery<T>; |
Opens subclause ( |
closeSubclause(): IDocumentQuery<T>; |
Closes subclause ) |
negateNext(): IDocumentQuery<T>; |
Adds NOT before next condition |
andAlso(): IDocumentQuery<T>; |
Adds AND before next condition |
orElse(): IDocumentQuery<T>; |
Adds OR before next condition |
usingDefaultOperator
(operator: QueryOperator)
: IDocumentQuery<T>; |
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(field: string,
ordering?: OrderingType)
: IDocumentQuery<T>; |
ORDER BY field [DESC] |
randomOrdering(seed?: string)
: IDocumentQuery<T>; |
ORDER BY random() |
take(count: number)
: IDocumentQuery<T>; |
Limits the number of result entries to count |
skip(count: number)
: IDocumentQuery<T>; |
Skips first count results |
async first(callback?
: EntityCallback): Promise; |
Returns first document from result set |
async single(callback?
: EntityCallback): Promise; |
Returns single document matching query criteria. If there are no such document or more then one - throws an InvalidOperationException |
async all(callback?
: QueryResultsCallback): Promise; |
Returns all documents from result set (considering take() / skip() options) |
async count(callback?
: EntitiesCountCallback): Promise; |
Returns count of all documents matching query criteria (non-considering take() / skip() options) |
Condition value can be a string, number, boolean, null value or Date
object:
type ConditionValue = string | number | boolean | Date | null;
- Define your model as class. Attributes should be just public properties:
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()
});
}
}
- For store model just pass it's instance without specifying collection prefix (e.g.
Products/
). Collection name will be detected automatically by model's class name
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 need to use
session.load()
secondoptions
param. Pass class constructor asdocumentType
option:
let product = await session.load('Products/1-A', {documentType: Product});
console.log(product instanceof Product); // true
console.log(product.id); // Products/1-A
- When querying documents, pass class constructor to
documentType
option ofsession.query({ ... })
:
let products = await session.query({
collection: 'Products',
documentType: Product
}).all();
products.forEach((product) => {
console.log(product instanceof Product); // true
console.log(product.id.includes('Products/')); // true
});
Also you can set global models class resolver (something like class autoloader in PHP). It should be a callback function accepting a name of the model class and returning its constructor:
store.conventions.addDocumentInfoResolver({
resolveConstructor: (className) =>
require(`./relative/path/to/models/${className}`)[className]
});
session = store.openSession();
let product = await session.load('Products/1-A');
console.log(product instanceof Product); // true
console.log(product.id); // Products/1-A
let products = await session.query({ collection: 'Products' }).all();
products.forEach((product) => {
console.log(product instanceof Product); // true
console.log(product.id.includes('Products/')); // true
});
All datatype definitions you can find in lib/ravendb-node.d.ts
. An example of CRUD operations and querying documents you may find below:
// 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
) {}
}
export default Product;
// file app.ts
import {DocumentStore, IDocumentStore, IDocumentSession, IDocumentQuery, DocumentConstructor, QueryOperators} from 'ravendb';
const store: IDocumentStore = DocumentStore.create('database url', 'database name');
let session: IDocumentSession;
store.initialize();
store.conventions.addDocumentInfoResolver({
resolveConstructor: (typeName: string): DocumentConstructor =>
<DocumentConstructor>require(`./models/${typeName}`)[typeName]
});
(async (): Promise<void> => {
let product: Product = new Product(
null, 'iPhone X', 999.99, 'USD', 64, 'Apple', true, new Date('2017-10-01T00:00:00')
);
product = await session.store<Product>(product);
console.log(product instanceof Product); // true
console.log(product.id.includes('Products/')); // true
await session.saveChanges();
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()
.usingDefaultOperator(QueryOperators.And)
.whereEquals<string>('manufacturer', 'Apple')
.whereEquals<boolean>('in_stock', true)
.whereBetween<Date>('last_update', new Date('2017-10-01T00:00:00'), new Date())
.whereGreaterThanOrEqual<number>('storage', 64)
.all();
products.forEach((product: Product): void => {
console.log(product instanceof Product); // true
console.log(product.id.includes('Products/')); // true
});
})();
- Fill auth options object. Pass contents of the pem/pfx certificate, specify its type and (optionally) passphrase:
const {DocumentStore, Certificate} = require('ravendb');
const certificate = `
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
`;
let authOptions = {
certificate: certificate,
type: Certificate.Pem,
password: 'my passphrase' // optional
};
Pfx certificates content should be passed as Buffer
object:
const {DocumentStore, Certificate} = require('ravendb');
const fs = require('fs');
const certificate = './cert.pfx';
let authOptions = {
certificate: fs.readFileSync(certificate),
type: Certificate.Pfx,
password: 'my passphrase' // optional
};
- Pass auth options as third argument of
DocumentStore.create
:
let store = DocumentStore.create(
'database url',
'default database name',
authOptions
);
store.initialize();
- if no auth options was provided and you're trying to work with secured server, an
NotSupportedException
will be thrown during store initialization - if certificate is invalid or doesn't have permissions for specific operations, an
AuthorizationException
will be thrown during working with sessions/querying/sending operations
You can attach binary files to documents.
- For attach a file, use
PutAttachmentOperation
. Pass document id, attachment name (it can be just a file name), content type and file contents as anBuffer
object:
const {DocumentStore, PutAttachmentOperation} = require('ravendb');
const path = require('path');
const fs = require('fs');
// ...
const fileName = './iphone-x.png';
await store.operations.send(
new PutAttachmentOperation(
'Products/1-A',
path.basename(fileName),
fs.readFileSync(fileName),
'image/png'
)
);
- For read an attachment, use
GetAttachmentOperation
. Pass document id and attachment name. File contents will be stored as anBuffer
object insidestream
property of response:
const {DocumentStore, PutAttachmentOperation, AttachmentTypes} = require('ravendb');
const fs = require('fs');
// ...
const fileName = 'iphone-x.png';
let attachmentResult = await store.operations.send(
new GetAttachmentOperation(
'Products/1-A',
fileName,
AttachmentTypes.Document
)
);
fs.writeFileSync(`./${fileName}`, attachmentResult.stream);
- For delete an attachment, use
DeleteAttachmentOperation
. Pass document id and attachment name.
const {DocumentStore, DeletAttachmentOperation} = require('ravendb');
// ...
const fileName = 'iphone-x.png';
await store.operations.send(
new DeleteAttachmentOperation(
'Products/1-A',
fileName
)
);
By default document id is stored onto id
property of document. But you can define which name of id property should be for specific document types.
- Define custom document id property name resolver as callback function. It accepts document type and should return id property name:
// models.item.js
class Item {
constructor(Id = null, Title = "", Options = []) {
this.Id = Id;
this.Title = Title;
this.Options = Options;
}
}
exports.Item = Item;
// index.js
const {DocumentStore} = require('ravendb');
const {Item} = require('./models/item');
const resolveIdProperty = (typeName) => {
switch (typeName) {
case Item.name:
return 'Id';
// ...
}
};
- Pass this callback to
resolveIdProperty
option ofconventions.addDocumentInfoResolver
:
store.conventions.addDocumentInfoResolver({ resolveIdProperty });
- Now client will read/fill
Id
property with document id while doing CRUD operations:
let session = store.openSession();
await session.store(new Item(null, 'First Item', [1, 2, 3]));
await session.saveChanges();
console.log(item.Id); // Items/1-A
session = store.openSession();
let item = await session.load('Items/1-A');
console.log(item.Id); // Items/1-A
console.log(item.Title); // First Item
console.log(item.Options); // [1, 2, 3]
You can define custom serializers if you need to implement your own logic for convert attributes names/values for specific document types.
- Define your serializer as object with
onSerialized
/onUnserialized
methods:
const serializer = {
onSerialized: (serialized) => {
},
onUnserialized: (serialized) => {
}
};
Where serialized
attribute has the following structure:
interface ISerialized<T extends Object = IRavenObject> {
source: object | T;
target?: object | T;
originalAttribute: string;
serializedAttribute: string;
originalValue: any;
serializedValue: any;
attributePath: string;
metadata?: object;
nestedObjectTypes?: IRavenObject<DocumentConstructor>;
}
- Store target attribute name/value into the
serializedAttribute
/serializedValue
properties ofserialized
parameter:
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.substring(1);
}
function uncapitalize(string) {
return string.charAt(0).toLowerCase() + string.substring(1);
}
const serializer = {
onSerialized: (serialized) => {
switch (serialized.metadata['Raven-Node-Type']) {
case Item.name:
serialized.serializedAttribute = uncapitalize(serialized.originalAttribute);
if ('Items' === serialized.originalAttribute) {
serialized.serializedValue = serialized.originalValue.join(",");
}
break;
// ...
}
},
onUnserialized: (serialized) => {
switch (serialized.metadata['Raven-Node-Type']) {
case Item.name:
serialized.serializedAttribute = capitalize(serialized.originalAttribute);
if ('items' === serialized.originalAttribute) {
serialized.serializedValue = serialized.originalValue.split(",").map(parseInt);
}
break;
// ...
}
}
};
- Pass your serializer object to
conventions.addAttributeSerializer
:
const {DocumentStore, GetDocumentCommand} = require('ravendb');
store.conventions.addAttributeSerializer(serializer);
let sesssion = store.openSession();
await session.store(new Item(null, 'First Item', [1, 2, 3]));
await session.saveChanges();
session = store.openSession();
let item = await session.load('Items/1-A');
console.log(item.Id); // Items/1-A
console.log(item.Title); // First Item
console.log(item.Options); // [1, 2, 3]
let response = await store.getRequestExecutor().execute(new GetDocumentCommand('Items/1-A'));
let rawDocument = response.Results[0];
console.log(rawDocument['@metadata']['@id']); // Items/1-A
console.log(rawDocument.title); // First Item
console.log(rawDocument.options); // "1,2,3"
npm run build
npm test -- -h 192.168.5.44 [-p 8080] [-c path/to/certificate.pem(pfx)] [-t DocumentSerializing [-f]]
Option | Description |
---|---|
-h or --ravendb-host= |
Database host |
-p or --ravendb-port= |
Database port. 8080 by default |
-c or --ravendb-certificate= |
Path to .pem or .pfx certificate. If specified, test runner will use https protocol |
-t or --test= |
Test name. For run multiple test, specify each test in separate --test= option. By default runs all tests |
-f or --no-fixtures |
Skip executing database fixtures (create test database, put test indexes etc). Can be usable for tests which doesn't executes raven commands (e.g. DocumentSerializing) |