Code Monkey home page Code Monkey logo

ravendb-nodejs-client's Introduction

Node.js client for RavenDB NoSQL Database

NPM

build status Known Vulnerabilities

Installation

npm install --save ravendb

Releases

  • All client versions 5.4.x are fully compatible with and support RavenDB server releases 5.4 and 6.0.

  • Click here to view all Releases and Changelog.

Documentation

Getting started

  1. Require the DocumentStore class from the ravendb package
const { DocumentStore } = require('ravendb');

or (using ES6 / Typescript imports)

import { DocumentStore } from 'ravendb';
  1. Initialize the document store (you should have a single DocumentStore instance per application)
const store = new DocumentStore('http://live-test.ravendb.net', 'databaseName');
store.initialize();
  1. Open a session
const session = store.openSession();
  1. Call saveChanges() when you're done
session
 .load('users/1-A') // Load document
 .then((user) => {
   user.password = PBKDF2('new password'); // Update data 
 })
 .then(() => session.saveChanges()) // Save changes
 .then(() => {
     // Data is now persisted
     // You can proceed e.g. finish web request
  });

Supported asynchronous call types

Most methods on the session object are asynchronous and return a Promise.
Either use async & await or .then() with callback functions.

  1. async / await
const session = store.openSession();
let user = await session.load('users/1-A');
user.password = PBKDF2('new password');
await session.saveChanges();
  1. .then & callback functions
session.load('Users/1-A')
    .then((user) => {
        user.password = PBKDF2('new password');
    })
    .then(() => session.saveChanges())
    .then(() => {
        // here session is complete
    });
Related tests:

async and await
then and callbacks

CRUD example

Store documents

let product = {
    id: null,
    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/1-A');
console.log(product.id); // products/1-A
await session.saveChanges();
Related tests:

store()
ID generation - session.store()
store document with @metadata
storing docs with same ID in same session should throw

Load documents

const product = await session.load('products/1-A');
console.log(product.title); // iPhone X
console.log(product.id);    // products/1-A
Related tests:

load()

Load documents with include

// users/1
// {
//      "name": "John",
//      "kids": ["users/2", "users/3"]
// }

const session = store.openSession();
const user1 = await session
    .include("kids")
    .load("users/1");
    // Document users/1 and all docs referenced in "kids"
    // will be fetched from the server in 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);
Related tests:

can load with includes
loading data with include
loading data with passing includes

Update 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); // the current date
Related tests:

update document
update document metadata

Delete documents

  1. 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); // null
  1. Using document ID
await session.delete('products/1-A');
Related tests:

delete doc by entity
delete doc by ID
onBeforeDelete is called before delete by ID
cannot delete untracked entity
loading deleted doc returns null

Query documents

  1. Use query() session method:

Query by collection:

const query = session.query({ collection: 'products' });

Query by index name:

const query = session.query({ indexName: 'productsByCategory' });

Query by index:

const query = session.query(Product, Product_ByName);

Query by entity type:

import { User } from "./models";
const query = session.query(User);
  1. Build up the query - apply search conditions, set ordering, etc.
    Query supports chaining calls:
query
    .waitForNonStaleResults()
    .usingDefaultOperator('AND')
    .whereEquals('manufacturer', 'Apple')
    .whereEquals('in_stock', true)
    .whereBetween('last_update', new Date('2022-11-01T00:00:00'), new Date())
    .orderBy('price');
  1. Execute the query to get results:
const results = await query.all(); // get all results
// ...
const firstResult = await query.first(); // gets first result
// ...
const single = await query.single();  // gets single result 

Query methods overview

selectFields() - projections using a single field

// RQL
// from users select name

// Query
const userNames = await session.query({ collection: "users" })
    .selectFields("name")
    .all();

// Sample results
// John, Stefanie, Thomas
Related tests:

projections single field
query single property
retrieve camel case with projection
can_project_id_field

selectFields() - projections using multiple fields

// RQL
// from users select name, age

// Query
await session.query({ collection: "users" })
    .selectFields([ "name", "age" ])
    .all();

// Sample results
// [ { name: 'John', age: 30 },
//   { name: 'Stefanie', age: 25 },
//   { name: 'Thomas', age: 25 } ]
Related tests:

projections multiple fields
query with projection
retrieve camel case with projection
can_project_id_field

distinct()

// RQL
// from users select distinct age

// Query
await session.query({ collection: "users" })
    .selectFields("age")
    .distinct()
    .all();

// Sample results
// [ 30, 25 ]
Related tests:

distinct
query distinct

whereEquals() / whereNotEquals()

// RQL
// from users where age = 30 

// Query
await session.query({ collection: "users" })
    .whereEquals("age", 30)
    .all();

// Saple results
// [ User {
//    name: 'John',
//    age: 30,
//    kids: [...],
//    registeredAt: 2017-11-10T23:00:00.000Z } ]
Related tests:

where equals
where not equals

whereIn()

// RQL
// from users where name in ("John", "Thomas")

// Query
await session.query({ collection: "users" })
    .whereIn("name", ["John", "Thomas"])
    .all();

// Sample results
// [ 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' } ]
Related tests:

where in
query with where in

whereStartsWith() / whereEndsWith()

// RQL
// from users where startsWith(name, 'J')

// Query
await session.query({ collection: "users" })
    .whereStartsWith("name", "J")
    .all();

// Sample results
// [ User {
//    name: 'John',
//    age: 30,
//    kids: [...],
//    registeredAt: 2017-11-10T23:00:00.000Z } ]
Related tests:

query with where clause

whereBetween()

// RQL
// from users where registeredAt between '2016-01-01' and '2017-01-01'

// Query
await session.query({ collection: "users" })
    .whereBetween("registeredAt", new Date(2016, 0, 1), new Date(2017, 0, 1))
    .all();

// Sample results
// [ User {
//     name: 'Thomas',
//     age: 25,
//     registeredAt: 2016-04-24T22:00:00.000Z,
//     id: 'users/3-A' } ]
Related tests:

where between
query with where between

whereGreaterThan() / whereGreaterThanOrEqual() / whereLessThan() / whereLessThanOrEqual()

// RQL
// from users where age > 29

// Query
await session.query({ collection: "users" })
    .whereGreaterThan("age", 29)
    .all();

// Sample results
// [ User {
//   name: 'John',
//   age: 30,
//   registeredAt: 2017-11-10T23:00:00.000Z,
//   kids: [...],
//   id: 'users/1-A' } ]
Related tests:

where greater than
query with where less than
query with where less than or equal
query with where greater than
query with where greater than or equal

whereExists()

Checks if the field exists.

// RQL
// from users where exists("age")

// Query
await session.query({ collection: "users" })
    .whereExists("kids")
    .all();

// Sample results
// [ User {
//   name: 'John',
//   age: 30,
//   registeredAt: 2017-11-10T23:00:00.000Z,
//   kids: [...],
//   id: 'users/1-A' } ]
Related tests:

where exists
query where exists

containsAny() / containsAll()

// RQL
// from users where kids in ('Mara')

// Query
await session.query({ collection: "users" })
    .containsAll("kids", ["Mara", "Dmitri"])
    .all();

// Sample results
// [ User {
//   name: 'John',
//   age: 30,
//   registeredAt: 2017-11-10T23:00:00.000Z,
//   kids: ["Dmitri", "Mara"]
//   id: 'users/1-A' } ]
Related tests:

where contains any
queries with contains

search()

Perform full-text search.

// RQL
// from users where search(kids, 'Mara')

// Query
await session.query({ collection: "users" })
    .search("kids", "Mara Dmitri")
    .all();

// Sample results
// [ User {
//   name: 'John',
//   age: 30,
//   registeredAt: 2017-11-10T23:00:00.000Z,
//   kids: ["Dmitri", "Mara"]
//   id: 'users/1-A' } ]
Related tests:

search()
query search with or
query_CreateClausesForQueryDynamicallyWithOnBeforeQueryEvent

openSubclause() / closeSubclause()

// RQL
// from users where exists(kids) or (age = 25 and name != Thomas)

// Query
await session.query({ collection: "users" })
    .whereExists("kids")
    .orElse()
    .openSubclause()
        .whereEquals("age", 25)
        .whereNotEquals("name", "Thomas")
    .closeSubclause()
    .all();

// Sample results
// [ 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' } ]
Related tests:

subclause
working with subclause

not()

// RQL
// from users where age != 25

// Query
await session.query({ collection: "users" })
    .not()
    .whereEquals("age", 25)
    .all();

// Sample results
// [ User {
//   name: 'John',
//   age: 30,
//   registeredAt: 2017-11-10T23:00:00.000Z,
//   kids: ["Dmitri", "Mara"]
//   id: 'users/1-A' } ]
Related tests:

not()
query where not

orElse() / andAlso()

// RQL
// from users where exists(kids) or age < 30

// Query
await session.query({ collection: "users" })
    .whereExists("kids")
    .orElse()
    .whereLessThan("age", 30)
    .all();

// Sample results
//  [ 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' } ]
Related tests:

orElse
working with subclause

usingDefaultOperator()

If neither andAlso() nor orElse() is called then the default operator between the query filtering conditions will be AND .
You can override that with usingDefaultOperator which must be called before any other where conditions.

// RQL
// from users where exists(kids) or age < 29

// Query
await session.query({ collection: "users" })
    .usingDefaultOperator("OR") // override the default 'AND' operator
    .whereExists("kids")
    .whereLessThan("age", 29)
    .all();

// Sample results
//  [ 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' } ]
Related tests:

set default operator
AND is used when default operator is not set
set default operator to OR

orderBy() / orderByDesc() / orderByScore() / randomOrdering()

// RQL
// from users order by age

// Query
await session.query({ collection: "users" })
    .orderBy("age")
    .all();

// Sample results
// [ 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' } ]
Related tests:

orderBy()
orderByDesc()
query random order
order by AlphaNumeric
query with boost - order by score

take()

Limit the number of query results.

// RQL
// from users order by age

// Query
await session.query({ collection: "users" })
    .orderBy("age") 
    .take(2) // only the first 2 entries will be returned
    .all();

// Sample results
// [ 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' } ]
Related tests:

take()
query skip take
canUseOffsetWithCollectionQuery

skip()

Skip a specified number of results from the start.

// RQL
// from users order by age

// Query
await session.query({ collection: "users" })
    .orderBy("age") 
    .take(1) // return only 1 result
    .skip(1) // skip the first result, return the second result
    .all();

// Sample results
// [ User {
//     name: 'Thomas',
//     age: 25,
//     registeredAt: 2016-04-24T22:00:00.000Z,
//     id: 'users/3-A' } ]
Related tests:

skip()
query skip take
canUseOffsetWithCollectionQuery

Getting query statistics

Use the statistics() method to obtain query statistics.

// Query
let stats: QueryStatistics;
const results = await session.query({ collection: "users" })
    .whereGreaterThan("age", 29)
    .statistics(s => stats = s)
    .all();

// Sample results
// 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 }
Related tests:

can get stats

all() / first() / single() / count()

all() - returns all results

first() - first result only

single() - first result, throws error if there's more entries

count() - returns the number of entries in the results (not affected by take())

Related tests:

query first and single
query count

Attachments

Store attachments

const doc = new User({ name: "John" });

// Store a dcoument, the entity will be tracked.
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 store attachment using document ID
session.advanced.attachments.store(doc.id, "photo.png", fileStream, "image/png");

// Persist all changes
await session.saveChanges();
Related tests:

store attachment
can put attachments
checkIfHasChangesIsTrueAfterAddingAttachment
store many attachments and docs with bulk insert

Get attachments

// Get an attachment
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. See https://nodejs.org/api/stream.html#class-streamreadable
attachment.data
    .pipe(fs.createWriteStream("photo.png"))
    .on("finish", () => next());
Related tests:

get attachment
can get & delete attachments

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
Related tests:

attachment exists
attachment exists 2

Get attachment names

// Use a loaded entity to determine attachments' names
await session.advanced.attachments.getNames(doc);

// Sample results:
// [ { name: 'photo.png',
//     hash: 'MvUEcrFHSVDts5ZQv2bQ3r9RwtynqnyJzIbNYzu1ZXk=',
//     contentType: 'image/png',
//     size: 4579 } ]
Related tests:

get attachment names
get attachment names 2

TimeSeries

Store time series

const session = store.openSession();

// Create a document with time series
await session.store({ name: "John" }, "users/1");
const tsf = session.timeSeriesFor("users/1", "heartbeat");

// Append a new time series entry
tsf.append(new Date(), 120);

await session.saveChanges();
Related tests:

can use time series
canCreateSimpleTimeSeries
usingDifferentTags
canStoreAndReadMultipleTimestamps
canStoreLargeNumberOfValues
shouldDeleteTimeSeriesUponDocumentDeletion

Get time series for document

const session = store.openSession();

// Get time series for document by time series name
const tsf = session.timeSeriesFor("users/1", "heartbeat");

// Get all time series entries
const heartbeats = await tsf.get();
Related tests:

canCreateSimpleTimeSeries
canStoreLargeNumberOfValues
canRequestNonExistingTimeSeriesRange
canGetTimeSeriesNames2
canSkipAndTakeTimeSeries

Bulk Insert

// Create a bulk insert instance from the DocumentStore
const bulkInsert = store.bulkInsert();

// Store multiple documents
for (const name of ["Anna", "Maria", "Miguel", "Emanuel", "Dayanara", "Aleida"]) {
    const user = new User({ name });
    await bulkInsert.store(user);
    // The data stored in bulkInsert will be streamed to the server in batches 
}

// Sample documents stored:
// 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' }

// Call finish to send all remaining data to the server
await bulkInsert.finish();
Related tests:

bulk insert example
simple bulk insert should work
bulk insert can be aborted
can modify metadata with bulk insert

Changes API

Listen for database changes e.g. document changes.

// Subscribe to change notifications
const changes = store.changes();

// Subscribe for all documents, or for specific collection (or other database items)
const docsChanges = changes.forAllDocuments();

// Handle changes events 
docsChanges.on("data", change => {
    // A sample change data recieved:
    // { 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 the changes instance when you're done
changes.dispose();
Related tests:

listen to changes
can obtain single document changes
can obtain all documents changes
can obtain notification about documents starting with
can obtain notification about documents in collection

Streaming

Stream documents by ID prefix

// Filter streamed results by passing an ID prefix
// The stream() method returns a Node.js ReadableStream
const userStream = await session.advanced.stream("users/");

// Handle stream events with callback functions
userStream.on("data", user => {
    // Get only documents with ID that starts with 'users/' 
    // i.e.: User { name: 'John', id: 'users/1-A' }
});

userStream.on("error", err => {
    // handle errors
})
Related tests:

can stream users by prefix
can stream documents starting with

Stream documents by query

// Define a query
const query = session.query({ collection: "users" }).whereGreaterThan("age", 29);

let streamQueryStats;
// Call stream() to execute the query, it returns a Node.js ReadableStream.
// Can get query stats by passing a stats callback to stream() method
const queryStream = await session.advanced.stream(query, _ => streamQueryStats = _);

// Handle stream events with callback functions
queryStream.on("data", user => {
    // Only documents matching the query are received
    // These entities are Not tracked by the session
});

// Can get query stats by using an event listener
queryStream.once("stats", queryStats => {
    // Sample stats:
    // { resultEtag: 7464021133404493000,
    //   isStale: false,
    //   indexName: 'Auto/users/Byage',
    //   totalResults: 1,
    //   indexTimestamp: 2018-10-01T09:04:07.145Z }
});

// Stream emits an 'end' event when there is no more data to read
queryStream.on("end", () => {
   // Get info from 'streamQueryStats', the stats object
   const totalResults = streamQueryStats.totalResults;
   const indexUsed = streamQueryStats.indexName;
});

queryStream.on("error", err => {
    // handle errors
});
Related tests:

can stream query and get stats
can stream query results
can stream query results with query statistics
can stream raw query results

Revisions

NOTE: Please make sure revisions are enabled before trying the below.

const session = store.openSession();
const user = {
    name: "Marcin",
    age: 30,
    pet: "Cat"
};

// Store a document
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");

// Sample results:
// [ { name: 'Roman',
//     age: 40,
//     pet: 'Cat',
//     '@metadata': [Object],
//     id: 'users/1' },
//   { name: 'Marcin',
//     age: 30,
//     pet: 'Cat',
//     '@metadata': [Object],
//     id: 'users/1' } ]
Related tests:

can get revisions
canGetRevisionsByDate
can handle revisions
canGetRevisionsByChangeVectors

Suggestions

Suggest options for similar/misspelled terms

// Some documents in users collection with misspelled name term
// [ User {
//     name: 'Johne',
//     age: 30,
//     ...
//     id: 'users/1-A' },
//   User {
//     name: 'Johm',
//     age: 31,
//     ...
//     id: 'users/2-A' },
//   User {
//     name: 'Jon',
//     age: 32,
//     ...
//     id: 'users/3-A' },
// ]

// Static index definition
class UsersIndex extends AbstractJavaScriptIndexCreationTask {
    constructor() {
        super();
        this.map(User, doc => {
            return {
                name: doc.name
            }
        });
        
        // Enable the suggestion feature on index-field 'name'
        this.suggestion("name"); 
    }
}

// ...
const session = store.openSession();

// Query for similar terms to 'John'
// Note: the term 'John' itself will Not be part of the results

const suggestedNameTerms = await session.query(User, UsersIndex)
    .suggestUsing(x => x.byField("name", "John")) 
    .execute();

// Sample results:
// { name: { name: 'name', suggestions: [ 'johne', 'johm', 'jon' ] } }
Related tests:

can suggest
canChainSuggestions
canUseAliasInSuggestions
canUseSuggestionsWithAutoIndex
can suggest using linq
can suggest using multiple words
can get suggestions with options

Advanced patching

// Increment 'age' field by 1
session.advanced.increment("users/1", "age", 1);

// Set 'underAge' field to false
session.advanced.patch("users/1", "underAge", false);

await session.saveChanges();
Related tests:

can use advanced.patch
can patch
can patch complex
can add to array
can increment
patchWillUpdateTrackedDocumentAfterSaveChanges
can patch single document
can patch multiple documents

Subscriptions

// Create a subscription task on the server
// Documents that match the query will be send to the client worker upon opening a connection

const subscriptionName = await store.subscriptions.create({
    query: "from users where age >= 30"
});

// Open a connection
// Create a subscription worker that will consume document batches sent from the server
// Documents are sent from the last document that was processed for this subscription

const subscriptionWorker = store.subscriptions.getSubscriptionWorker({ subscriptionName });

// Worker handles incoming batches
subscriptionWorker.on("batch", (batch, callback) => {
    try {
        // Process the incoming batch items
        // Sample 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
        // The worker will send an acknowledgement to the server, so that server can send next batch
        callback();
        
    } catch(err) {
        // If processing fails for a particular batch then pass the error to the callback
        callback(err);
    }
});

subscriptionWorker.on("error", err => {
   // handle errors
});

// Subscription event types: 
'batch', 'error', 'end', 'unexpectedSubscriptionError', 'afterAcknowledgment', 'connectionRetry'
Related tests:

can subscribe
should stream all documents
should send all new and modified docs
should respect max doc count in batch
can disable subscription
can delete subscription

Using object literals for entities

To comfortably use object literals as entities,
configure the collection name that will be used in the store conventions.

This must be done before calling initialize() on the DocumentStore instance,
else, your entities will be created in the @empty collection.

const store = new DocumentStore(urls, database);

// Configure the collection name that will be used
store.conventions.findCollectionNameForObjectLiteral = entity => entity["collection"];
// ...
store.initialize();

// Sample object literal
const user = {
   collection: "Users",
   name: "John"
};

session = store.openSession();
await session.store(user);
await session.saveChanges();

// The document will be stored in the 'Users' collection
Related tests:

using object literals for entities
using object literals
handle custom entity naming conventions + object literals

Using classes for entities

  1. 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()
        });
      }
}
  1. To store a document pass its instance to store().
    The collection name will automatically be detected from the 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();
  1. Loading a document
const product = await session.load('products/1-A');
console.log(product instanceof Product); // true
console.log(product.id);                 // products/1-A
  1. Querying for documents
const products = await session.query({  collection: 'products' }).all();

products.forEach((product) => {
  console.log(product instanceof Product);       // true
  console.log(product.id.includes('products/')); // true
});
Related tests:

using classes

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');
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 a secure server

  1. Fill auth options object.
    Pass the 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 a 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  
};
  1. 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, set the following environment variables:
# 
# - Location of RavenDB server binary:
# RAVENDB_TEST_SERVER_PATH="C:\\work\\test\\Server\\Raven.Server.exe" 
#
# - Certificate path for tests requiring a 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 

ravendb-nodejs-client's People

Contributors

a-serdukov-mobilunity avatar alonronin avatar ayende avatar cabelitos avatar configurator avatar cybercats-team avatar danielle9897 avatar dependabot[bot] avatar gregolsky avatar ml054 avatar mobilunity-user avatar mrinc avatar nemanja-tosic avatar nickchampion avatar poissoncorp avatar ppekrol avatar victor-accarini avatar vxern avatar wallaceturner avatar yj7o5 avatar yuramijs avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ravendb-nodejs-client's Issues

error TS2507: Type 'typeof FilterBase' is not a constructor function type.

I'm using ravendb within my pnpm monorepo.
When I try to build my project using typescript, I have a sad error reporting:

error TS2507: Type 'typeof FilterBase' is not a constructor function type.

 6 export declare class TransformKeysJsonStream extends FilterBase {

Found 1 error in ../../node_modules/.pnpm/[email protected]/node_modules/ravendb/dist/Mapping/Json/Streams/TransformKeysJsonStream.d.ts:6

My configuration is:

  • A pnpm monorepo
  • Typescript project ver 4.7.2
  • Node 16.12.0

I installed all the types for:

  • bluebird
  • readable-stream
  • stream-json
  • ws

I solved all the problems installing the types but for the above is quite impossible to solve it.
Any idea?

Thanks in advance

Dependency on deprecated punycode module

Running a project using ravendb on the newest version of node (v21) yields a deprecation warning.

Minimal example

package.json

{
  "dependencies": {
    "ravendb": "5.4.1"
  },
  "scripts": {
    "start": "node main.js"
  }
}

main.js

const { DocumentStore } = require("ravendb");

const store = new DocumentStore(/* Your DB connection info */).initialize();

After running npm i, we get the following behaviour:

$ npm run start

> start
> node main.js

(node:78223) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)

On older versions of node, the same warning can be seen by adding the --pending-deprecations flag.

The dependency on punycode seems to enter through node-fetch v2. Simply updating to v3 might do the trick.

Specific version information

  • OS: Ubuntu 22.04
  • Node: 21.0.0
  • npm: 10.2.0

Map/Reduce TypeError: Object has no method 'flat'

In the reduce we I want use some extensions like flat, flatMap, or something new in typescript, I get this error :
TypeError: Object has no method 'flat'


this.reduce((result) =>
            result
                .groupBy((c) => c.mobileNumber)
                .aggregate((groupResult) => {
                    return {
                       employerOrganisationDocumentIds: groupResult.values
                            .map((x) => x.employerOrganisationDocumentIds)
                            .flat()
                            .filter((v, i, a) => a.indexOf(v) === i),

image

if I change my code to avoid using the flat then I can create my index


this.reduce((result) =>
            result
                .groupBy((c) => c.mobileNumber)
                .aggregate((groupResult) => {
                    return {
                       employerOrganisationDocumentIds: groupResult.values
                                .map((x) => x.employerOrganisationDocumentIds)
                               .reduce((acc, val, i) => acc.concat(val), []).filter((v, i, a) => a.indexOf(v) === i),

my typescript version is 4.7.4 like the ravendb

Error "Max topology update tries reached" with trailing slash in url

This is not so much a bug, but improper usage by the consumer which might be resolved by informing the user.

In short, when adding a slash to the urlOrUrls property during connect, I'm getting the following error:

[04/21/2018 13:02:45] System.Private.CoreLib: Exception while executing function: Functions.event-grid-listener. System.Private.CoreLib: Result:
[04/21/2018 13:02:45] Exception: ue: Max topology update tries reached
[04/21/2018 13:02:45] Stack: ue: Max topology update tries reached
[04/21/2018 13:02:45]     at _withoutTopology._awaitFirstTopologyLock.acquire.then.e (/Users/.../node_modules/ravendb/lib/ravendb-node.js:2:55187)
[04/21/2018 13:02:45]     at tryCatcher (/Users/.../node_modules/bluebird/js/release/util.js:16:23)
[04/21/2018 13:02:45]     at Promise._settlePromiseFromHandler (/Users/.../node_modules/bluebird/js/release/promise.js:512:31)
[04/21/2018 13:02:45]     at Promise._settlePromise (/Users/.../node_modules/bluebird/js/release/promise.js:569:18)
[04/21/2018 13:02:45]     at Promise._settlePromise0 (/Users/.../node_modules/bluebird/js/release/promise.js:614:10)
[04/21/2018 13:02:45]     at Promise._settlePromises (/Users/.../node_modules/bluebird/js/release/promise.js:693:18)
[04/21/2018 13:02:45]     at Async._drainQueue (/Users/.../node_modules/bluebird/js/release/async.js:133:16)
[04/21/2018 13:02:45]     at Async._drainQueues (/Users/.../node_modules/bluebird/js/release/async.js:143:10)
[04/21/2018 13:02:45]     at Immediate.Async.drainQueues (/Users/.../node_modules/bluebird/js/release/async.js:17:14)
[04/21/2018 13:02:45]     at runCallback (timers.js:789:20)
[04/21/2018 13:02:45]     at tryOnImmediate (timers.js:751:5)
[04/21/2018 13:02:45]     at processImmediate [as _immediateCallback] (timers.js:722:5).

Steps to reproduce:

const endpoint = 'http://127.0.0.1:8888/';

const store = DocumentStore.create(endpoint, 'smurfs');
store.initialize();

const session = store.openSession();

const response = await session.query({
        collection: 'Smurfs'
    }).all();

In order to fix it, simply remove the trailing slash from the endpoint:

const endpoint = 'http://127.0.0.1:8888/';

My suggestion would be to add a console.warn() to the UriUtility.parseUrls() function if that's the case. As an alternative solution, the trailing slash could also be removed by the parseUrls() function. However, I can imagine you might consider this to obfuscate that there is a problem.

I'll be happy to submit a PR for either solution, if this would be considered useful?

Activating `queryCustomization.randomOrdering` ignores Collection parameter and returns from all Collections

Hi guys,
I am using RavenDB as part of my backend and I am really enjoying it.
The ability to save attachments as part of Documents is awesome and has proven a handy feature.

I am experiencing a weird behavior though. As part of a request, I am trying to get a random set of Documents from a Collection.
I tried to use queryCustomization.randomOrdering for this but it turns out, that it ignores the given Collection parameter and just returns from every Collection where whereEquals matches.

Version:
[email protected]
node v16.13.2

This is the part of my code where I encountered this behavior.

async function generateReviewQueue(notebookId: string): Promise<ReviewQueueEntry[]> {
    const session = getSession();

    session.advanced.on("beforeQuery", (event) => event.queryCustomization.randomOrdering());
    const notes = await session.query<Note>(Note.collectionName).whereEquals("notebookId", notebookId).take(5).all();

    console.log("GENERATED REVIEW QUEUE");
    notes.forEach((note) => {
        console.log(note);
    });

    return notes.map((note) => new ReviewQueueEntry(note.id, 3));
}

export class Note {
    public static collectionName: string = "Notes";
    ...

Here is a log message that shows this:

GENERATED REVIEW QUEUE
Note {id: '20f86a25-bad1-49d7-995e-7553497145f0', notebookId: '210b69b6-5483-4496-942a-cbf1f8d499d9', name: 'Price', content: '값', sortIndex: 0, …}
Note {id: 'f1ff344e-4230-4b91-82bc-288d69a92424', notebookId: '210b69b6-5483-4496-942a-cbf1f8d499d9', name: 'Family', content: '가족', sortIndex: 0, …}
ReviewSession {id: 'ae5f381a-221c-438f-a491-ac0f3533d39b', type: 'audio-cue', notebookId: '210b69b6-5483-4496-942a-cbf1f8d499d9', reviewQueue: Array(0), isFinished: true, …}
Note {id: '15d4e8eb-8bef-4ca5-956a-ccbc588ddb5b', notebookId: '210b69b6-5483-4496-942a-cbf1f8d499d9', name: 'Worry', content: '걱정', sortIndex: 0, …}
ReviewSession {id: '2af0d8e3-b84d-4b65-9784-a03a00cc7a19', type: 'text-input', notebookId: '210b69b6-5483-4496-942a-cbf1f8d499d9', reviewQueue: Array(0), isFinished: true, …}

Please notice the two ReviewSession entries, even though the collection is given as Note.collectionName.

Can I use the as clause?

RQL can use as clause, but can I use it in js sdk?
Example
const data = await session.query({ collection: 'Users'}).as('id', 'user_id')

// { user_id: 50465465 }

This is what it looks like.
I want to achieve this without using indexes or RQL.

Streaming not Working

Hello, with v5.2.7 streaming works, with v5.4.2 streaming immediately responds as done with 0 results.

This is the code:

    const entities: Draw[] = [];
    const reader = await session.advanced.stream<Draw>(session.query<Draw>({ collection: 'Draws' }));

    reader.on('data', (data) => {
      entities.push(data.document);
    });

    await StreamUtil.finishedAsync(reader);

Also, I had to import StreamUtil like, because Typescript cannot seem to find it and don't believe it is exported as StreamUtil:
import * as StreamUtil from 'ravendb/dist/Utility/StreamUtil';

I am using RavenDB Server v5.4.2

Since my code worked with v.5.2.7 ... it seems there was a change. Is it because of my Server version?

Remove @Metadata when querying

I am trying out NextJS 13 async queries directly in Server Components and it complains that Warning: Only plain objects can be passed to Client Components from Server Components. Classes or other objects with methods are not supported.

The actual issue is there are values in the @metadata that appear to not be serializable or at least when I manually remove the @metadata all works fine. I normally use GraphQL every since I have been working with RavenDB when back in version 1 and didn't even realize the metadata comes with a query as I also had to load the metadata manually when I wanted to say add a expires date.

I could not find a store convention or anything like that to strip the @metadata when loading stuff. Did I miss something?

Thanks!

Document is stored but always added to the `@empty` collection

Summary:
Using the v4.1.5 Node.js client, and RavenDB server 4.1.5-patch-41012 (Docker), storing a document is stored but always added to the @empty collection, instead of the actual collection. See the two examples below.

I've looked at the Storing Entities section of the docs. Note this behaviour occurs both when using a class, like Foo in the example below, or when using an object literal and specifying the collection name.

This behaviour occurs both on a new, non-existing collection as well as when the collection already exists.

Lastly, I've tried several overloads of the session.store() function; with/without ID, different ID strategies, providing a collection name etc. Just can't seem to find the correct way.


Expected behaviour:

  • New collection is created, if not exists, and document is added to it
  • Or, document is added to existing collection

Actual behaviour:

  • Document is added to @empty collection

Example:

	export class Foo {
	    public foo: number | undefined;
	    public bar: string | undefined;
	}

    test('example 1 - store should save new collection, for class', async () => {
        const raw = `{
            "foo": 42,
            "bar": "lorem ipsum"
        }`;

        const store = new DocumentStore(settings.endpoint, settings.database);
        store.initialize();
        const session = await store.openSession();
        const document: Foo = JSON.parse(raw);

        // Note: Tried several options here:
        // await session.store(document);
        // await session.store(document, 'foo/');
        // await session.store(document, 'foo/', { documentType: Foo });
        await session.store(document, 'foo/', 'Foo');
        await session.saveChanges();

        /* Result:
            {
                "foo": 42,
                "bar": "lorem ipsum",
                "@metadata": {
                    "@nested-object-types": {},
                    "Raven-Node-Type": null
                }
            }
        */
    });

    test('example 2 - store should save new collection, for object literal', async () => {
        const raw = `{
            "foo": 42,
            "bar": "lorem ipsum"
        }`;

        const store = new DocumentStore(settings.endpoint, settings.database);
        store.conventions.findCollectionNameForObjectLiteral = (entity: any) => entity['collection'];
        store.initialize();
        const session = await store.openSession();
        const document = JSON.parse(raw);

        // Note: Also tried several options, mentioned in 'example 1'
        await session.store(document, 'foo/');
        await session.saveChanges();
    });

Support for attachments

Hi!

In 4.0.0-rc3 there was support for attachments via PutAttachmentOperation, although the session.advanced was lacking the same api as it's C# counterpart. In the RTM, PutAttachmentOperation has been removed but we don't have support yet in the session.advanced api. Any plans to bring the support for attachments back any time soon?

Great to have support for ravendb in nodejs!!

Thanks!

Germán

Documentation - Loading nested documents

Not exactly an issue, but I can't find any example or implementation on the internet how to load nested documents in Javascript or Typescript; Though on C# and Java it is documented. Submitted examples will be greatly appreciated. Thanks!

Next version release?

Hello!

The company I work for is attempting to utilize this client in TypeScript. We ran into the issue that #212 fixes, but we noticed the build hadn't triggered a publish to npm. Just wondering if there's a release schedule for this package and when the next version will be published.

Thanks!

Query with search and then were uses OR instead of AND

The following code:

async function query(queryString: QueryString): Promise<Question[]> {
  const store = asyncLocalStorage.getStore() as alStoreType;
  const loggedinUserId = store?.loggedinUserId;
  const { language, level, searchTerm, isMarkedToBeRevised, limit, page, isRevised } = queryString;
  const isMarkedToBeRevisedBoolean = convertQueryParamsToBoolean(isMarkedToBeRevised);
  const isRevisedBoolean = convertQueryParamsToBoolean(isRevised);
  const session = ravenStore.openSession();
  const query = session.query<Question>({ collection: COLLECTION_NAME });

  if (limit) query.take(Number(limit));
  if (searchTerm) query.search("question", searchTerm);
  const skip = Number(page) * Number(limit);
  if (skip) query.skip(skip);

  if (!loggedinUserId) query.randomOrdering();
  else {
    const userCorrectAnswersIds = await session
      .query<UserCorrectAnswer>({
        collection: "UserCorrectAnswers",
      })
      .selectFields(["questionId"])
      .whereEquals("userId", loggedinUserId)
      .all();

    const docIdsToFilter = userCorrectAnswersIds.map(id =>
      setIdToCollectionName(COLLECTION_NAME, id as unknown as string)
    );
    query.not().whereIn("id()", docIdsToFilter);
  }
  

  const questions = await query.all();
  for (const question of questions) question.id = trimCollectionNameFromId(question.id);
  return questions;
}

Will generate a query with search("question", $p0) or not where("id()", ...)

Should be and not...

@nested-object-types doesn’t store type information for newly added fields on update

Exposition

We’re loading existing documents and setting “new” fields to Date values. (e.g. fields that didn’t exist on the document when it was created)

When those documents are saved, their metadata is not updated with the type information for the newly added fields.

On subsequent loads, Date values are not cast, and instead returned as stings.

This seems like an oversight, as type information is correctly extracted and stored on create.

Failing test case

    it("sets correct nested-object-types", async () => {
        class DateDoc {
            public firstDate: Date
            public secondDate: Date
        }
        {
            const session = store.openSession();
            const dateDoc = new DateDoc();
            dateDoc.firstDate = new Date();

            await session.store(dateDoc, "date/1");
            await session.saveChanges();
        }
        const session = store.openSession()
        const doc = await session.load<DateDoc>("date/1")
        doc.secondDate = new Date()
        await session.saveChanges()
        const metadata = session.advanced.getMetadataFor(doc)
        assert.strictEqual(doc.firstDate instanceof Date, true)
        assert.strictEqual(doc.secondDate instanceof Date, true)
        assert.strictEqual(metadata["@nested-object-types"]["firstDate"], "date")
        assert.strictEqual(metadata["@nested-object-types"]["secondDate"], "date")

        const session2 = store.openSession()
        const loaded = await session2.load<DateDoc>("date/1")
        const loadedMetadata = session2.advanced.getMetadataFor(loaded)
        assert.strictEqual(loaded.firstDate instanceof Date, true)
        assert.strictEqual(loaded.secondDate instanceof Date, true)
        assert.strictEqual(loadedMetadata["@nested-object-types"]["firstDate"], "date")
        assert.strictEqual(loadedMetadata["@nested-object-types"]["secondDate"], "date")
    });

MetadataInternal on @metadata makes it problematic in Next.js

When I load a document I get an object looking similar to this:

{
…
“@metadata” : MetadataInternal {…}
}

This seem to cause problems when using for instance Next.js, getting the data in getServerSideProps and when the data is transferred to the frontend components it throws an error saying something like, it only supports plain object. Maybe you should consider to remove MetadataInternal from the result and return a plain JSON object instead?

Support for Counters

Hello! I was wondering if you consider implementing counters API in node client?
Maybe I am missing something from the official docs, but I have failed to locate the ability to interact with counters.

Thank you in advance!

Issue with 5.0.1

Switched to 5.0.1, and when building, the following errors occured:

node_modules/ravendb/dist/Types/index.d.ts:49:28 - error TS1005: ']' expected.

49 [K in keyof T & string as ${Capitalize<K>}]: T[K] extends Array ? R extends string ? R[] : ServerCasing[] : T[K] extends object ? ServerCasing<T[K]> : T[K];
~~

node_modules/ravendb/dist/Types/index.d.ts:49:47 - error TS1005: '(' expected.

49 [K in keyof T & string as ${Capitalize<K>}]: T[K] extends Array ? R extends string ? R[] : ServerCasing[] : T[K] extends object ? ServerCasing<T[K]> : T[K];
~

node_modules/ravendb/dist/Types/index.d.ts:51:1 - error TS1160: Unterminated template literal.

51

Cannot find type definition file for 'stream-json'

When including this in my Typescript project, I get the following error when building it:

node_modules/ravendb/dist/Mapping/Json/Streams/TransformKeysJsonStream.d.ts:1:23 - error TS2688: Cannot find type definition file for 'stream-json'.

1 ///

Documents are saved into @empty collection

I am trying to build an adapter for Lucia but the DatabaseSession object is saved in the @empty collection.

public async setSession(databaseSession: DatabaseSession): Promise<void> {
      const session = documentStore.openSession();
      await session.store(databaseSession);
      await session.saveChanges();
}

I also saw this issue but i am already passing the object directly without explicitly defining an id. It is worth noting that the DatabaseSession interface is part of an external library and i have no control over it.

export interface DatabaseSession {
    userId: string;
    expiresAt: Date;
    id: string;
    attributes: RegisteredDatabaseSessionAttributes;
}

At first i though this might be due to the id being generated by Lucia instead of being null, but when i tried this:

public async setSession(databaseSession: DatabaseSession): Promise<void> {
    (databaseSession as any).id = null;
    const session = documentStore.openSession();
    await session.store<DatabaseSession>(databaseSession);
    await session.saveChanges();
}

I still received the same result (I also checked in the debugger to see if the id is null, and it was.).

EDIT: This Worked. The session is saved in RavenDbDatabaseSession collection :

public async setSession(databaseSession: DatabaseSession): Promise<void> {
      var newSession = new RavenDbDatabaseSession(null, databaseSession.userId, databaseSession.expiresAt, databaseSession.attributes);
      const session = documentStore.openSession();
      await session.store(newSession);
      await session.saveChanges();
}
	
class RavenDbDatabaseSession {
    constructor(
          id: string | null,
          userId: string,
          expiresAt: Date,
          attributes: RegisteredDatabaseSessionAttributes
) {
      Object.assign(this, {
	      id,
	      userId,
	      expiresAt,
	      attributes
      });
    }
}

EDIT 2: That seemed to work:

public async setSession(databaseSession: DatabaseSession): Promise<void> {
      const session = documentStore.openSession();
      (databaseSession as any)['@metadata'] = { "@collection": 'Sessions' };
      await session.store<DatabaseSession>(databaseSession);
      await session.saveChanges();
}

XRegExp is not a function

I'm using Next.js 14 with node 20.9.0 and when I import the ravendb client I get the following error.

TypeError: XRegExp is not a function
at eval (webpack-internal:///(rsc)/./node_modules/ravendb/dist/Utility/StringUtil.js:122:23)
at (rsc)/./node_modules/ravendb/dist/Utility/StringUtil.js (/Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/.next/server/vendor-chunks/ravendb.js:7631:1)
at webpack_require (/Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/.next/server/webpack-runtime.js:33:42)
at eval (webpack-internal:///(rsc)/./node_modules/ravendb/dist/Mapping/ObjectMapper.js:9:22)
at (rsc)/./node_modules/ravendb/dist/Mapping/ObjectMapper.js (/Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/.next/server/vendor-chunks/ravendb.js:6554:1)
at webpack_require (/Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/.next/server/webpack-runtime.js:33:42)
at eval (webpack-internal:///(rsc)/./node_modules/ravendb/dist/Documents/Conventions/DocumentConventions.js:6:24)
at (rsc)/./node_modules/ravendb/dist/Documents/Conventions/DocumentConventions.js (/Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/.next/server/vendor-chunks/ravendb.js:735:1)
at webpack_require (/Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/.next/server/webpack-runtime.js:33:42)
at eval (webpack-internal:///(rsc)/./node_modules/ravendb/dist/index.js:28:29)
at (rsc)/./node_modules/ravendb/dist/index.js (/Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/.next/server/vendor-chunks/ravendb.js:7686:1)
at webpack_require (/Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/.next/server/webpack-runtime.js:33:42)
at eval (webpack-internal:///(rsc)/./src/app/page.js:10:65)
at webpack_require.a (/Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/.next/server/webpack-runtime.js:100:13)
at eval (webpack-internal:///(rsc)/./src/app/page.js:1:21)
at (rsc)/./src/app/page.js (/Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/.next/server/app/page.js:369:1)
at Function.webpack_require (/Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/.next/server/webpack-runtime.js:33:42)
at async ez (/Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:35:395774)
at async t7 (/Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:38:5732)
at async /Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:38:7840
at async Promise.all (index 0)
at async t7 (/Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:38:7607)
at async /Users/marcus/Library/Mobile Documents/comappleCloudDocs/Projects.nosync/unsorted/nextjs14/my-app/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:38:24472 {
digest: '942695289'
}

To reproduce:

  1. Install Next.js 14
  2. Use app directory
  3. Use JavaScript
  4. Install ravendb
  5. import DocumentStore
  6. Run the app using npm run dev

Changes Observable uneven connection state count prematurely closes changes connection

Note lines 36 and 62 - the inc() method only increments on data event, while the dec() method decrements on both data and error. This causes the connection state to close the connection, even though there are clients still listening to data.

public on(event: "data", handler: (value: T) => void);
public on(event: "error", handler: (error: Error) => void);
public on(event: "data" | "error", handler: ((value: T) => void) | ((error: Error) => void)) {
switch (event) {
case "data":
// since allow multiple subscriptions on single object we cant register it multiple times
// to avoid duplicates in notification
if (!this._sendHandler) {
// register shared handler
this._sendHandler = (payload: T) => this.send(payload);
this._connectionState.addOnChangeNotification(this._type, this._sendHandler);
}
this._subscribers.add(handler as (value: T) => void);
this._connectionState.inc();
break;
case "error":
if (!this._errorHandler) {
// register shared error handler
this._errorHandler = (ex: Error) => this.error(ex);
this._connectionState.addOnError(this._errorHandler);
}
this._errorSubscribers.add(handler as (error: Error) => void);
break;
}
return this;
}
public removeListener(event: "data", handler: (value: T) => void);
public removeListener(event: "error", handler: (error: Error) => void);
public removeListener(event: "data" | "error", handler: ((value: T) => void) | ((error: Error) => void)) {
return this.off(event as any, handler as any);
}
public off(event: "data", handler: (value: T) => void);
public off(event: "error", handler: (error: Error) => void);
public off(event: "data" | "error", handler: ((value: T) => void) | ((error: Error) => void)) {
this._connectionState.dec();
switch (event) {
case "data":
this._subscribers.delete(handler as (value: T) => void);
if (!this._subscribers.size) {
// no more subscribers left - remove from parent
this._connectionState.removeOnChangeNotification(this._type, this._sendHandler);
this._sendHandler = undefined;
}
break;
case "error":
this._errorSubscribers.delete(handler as (error: Error) => void);
if (!this._errorSubscribers.size) {
this._connectionState.removeOnError(this._errorHandler);
this._errorHandler = undefined;
}
break;
}
return this;
}

Deleting does not work in bulk

Tried several variations of the docs
https://www.npmjs.com/package/ravendb#crud-example

Nothing works on deletion

export async function deleteCollection(collectionName: string): Promise<void> {
    const session = store.openSession();
    try {
        const documents = (await session
            .query({ collection: collectionName })
            .all()) as RavenDocument[];
            // data is fetched properly here
        for (const document of documents) {
            await session.delete(document);
        }
        await session.saveChanges();
    } catch (error) {
        console.error('Error deleting documents:', error);
        throw error;
    } finally {
        session.dispose();
    }
}

Question: check for unsaved changes in session

This is just a question, not a real issue: while using this library I came up with the idea of calling session.saveChanges() automatically before sending the server response (we're talking API here) whenever the session has unsaved/pending changes, but I couldn't find any way for checking for this (I inspected some session objects at different stages).

So, is there any publicly accessible way to check if a session has currently unsaved changes?

Incorrect ModifiedDocument returned in afterSaveChanges session event

Exposition

We’re running the NodeJS client against documents with keys that aren't camel-cased. In some cases, the keys (e.g. SIPCall) cannot be (easily) round-trip converted between camel and pascal case without losing the original key name. So we’re using the original key names, and have opted not to use entityFieldNameConvention or remoteEntityFieldNameConvention.

As part of our application, we load documents containing keys that are not camel-cased into a session. When we patch those documents (with a deferred session command) and save the session changes, the resulting modified document (as accessed through an afterSaveChanges event) contains an object with both the original (non camel-cased) keys (with the old values), and camel-cased keys with the new (modified) values.

This is an example of the kind of entity that's returned:

{
  Name: "old name",
  SIPCall: "old value",
  name: "new name",
  sipCall: "new value"
}

How it happens…

SingleNodeBatchCommand requests that its result keys be transformed…

this.result = await RavenCommandResponsePipeline.create<BatchCommandResult>()
.collectBody(_ => body = _)
.parseJsonSync() // TODO: consider parseJsonAsync()
.objectKeysTransform({
defaultTransform: "camel",
ignoreKeys: [/^@/],
})
.process(bodyStream);

…which sets streamKeyCaseTransform in the RavenCommandResponsePipeline

public objectKeysTransform(
optsOrTransform: CasingConvention | ObjectKeyCaseTransformStreamOptions,
profile?: ObjectKeyCaseTransformProfile): this {
if (!this._opts.jsonAsync && !this._opts.jsonSync) {
throwError("InvalidOperationException",
"Cannot use key case transform without doing parseJson() or parseJsonAsync() first.");
}
if (!optsOrTransform || typeof optsOrTransform === "string") {
this._opts.streamKeyCaseTransform =
getObjectKeyCaseTransformProfile(optsOrTransform as CasingConvention, profile);
} else {
this._opts.streamKeyCaseTransform = optsOrTransform;
}
if (this._opts.jsonAsync) {
this._opts.streamKeyCaseTransform.handleKeyValue = true;
}
return this;
}

…which adds ObjectKeyCaseTransformStream to the stream when processing the results…

if (opts.streamKeyCaseTransform) {
const handlePath = !!opts.jsonAsync;
const keyCaseOpts = Object.assign({}, opts.streamKeyCaseTransform, { handlePath });
streams.push(new ObjectKeyCaseTransformStream(keyCaseOpts));
}

…which, by default, is recursive, descending into ModifiedDocument and irreversibly altering the returned document object keys.

const DEFAULT_OBJECT_KEY_CASE_TRANSFORM_OPTS = {
arrayRecursive: true,
recursive: true
};

Failing test case

it("returns correct modified document after patch", async () => {
        class PascalDoc {
            public SIPCall: string
        }
        {
            const session = store.openSession();
            const pascalDoc = new PascalDoc();
            pascalDoc.SIPCall = "RavenDB";

            await session.store(pascalDoc, "pascal/1");
            await session.saveChanges();
        }
        let modifiedDocument
        store.addSessionListener("afterSaveChanges", event => {
            modifiedDocument = event.entity
        });

        const session = store.openSession()
        await session.load("pascal/1")
        const patchRequest = PatchRequest.forScript("this.SIPCall = \"Patched\"");
        session.advanced.defer(new PatchCommandData("pascal/1", null, patchRequest))
        await session.saveChanges();

        assert.strictEqual(modifiedDocument.SIPCall, "Patched")
        assert.ok(!("sipCall" in modifiedDocument))
});

Potential fix

We worked around the issue by adding ignorePaths to the options passed to objectKeysTransform.

this.result = await RavenCommandResponsePipeline.create<BatchCommandResult>()
    .collectBody(_ => body = _)
    .parseJsonSync() // TODO: consider parseJsonAsync()
    .objectKeysTransform({
        defaultTransform: "camel",
        ignoreKeys: [/^@/],
        ignorePaths: [/results\.\[\]\.modifiedDocument\./i],
    })
    .process(bodyStream);

Original:

this.result = await RavenCommandResponsePipeline.create<BatchCommandResult>()
.collectBody(_ => body = _)
.parseJsonSync() // TODO: consider parseJsonAsync()
.objectKeysTransform({
defaultTransform: "camel",
ignoreKeys: [/^@/],
})
.process(bodyStream);

This works for us, but we’re not sure if this fix is broadly applicable, as maintaining compatibility with the existing case-conversion conventions wasn’t a priority for us.

Getting started error

Following the getting started on the README, I'm hitting:

An exception occurred while contacting https://[redacted[.ravendb.cloud/databases/[redacted]/bulk_docs? . \n' +
    'TypeError [ERR_INVALID_PROTOCOL]: Protocol "https:" not supported. Expected "http:"\n' +

I'm running your client in a lambda using node 14.

Better DX for defining AbstractJavaScriptIndexCreationTask

TimeSeriesRangeAggregation.asTypedEntry not producing expected results

Given the following RQL I am using to retrieve trading time series data:

const aggregatedHistoryQueryResult = await session
    .query(MarketSymbol)
    .whereEquals("id", symbolId)
    .selectTimeSeries(
      (builder) =>
        builder.raw(
          `from history 
          between $start and $end 
          group by $groupBy
          select first(), last(), min(), max()`
        ),
      TimeSeriesAggregationResult
    )
    .addParameter("start", fromDate)
    .addParameter("end", dayjs.utc().toDate())
    .addParameter("groupBy", groupingAction)
    .firstOrNull();

And this is what RavenDB is returning as the aggregation entry:

TimeSeriesRangeAggregation {
  key: null,
  count: [ 1416, 1416, 1416, 1416 ],
  first: [ 39147.8, 39141.6, 39185.5, 39107.4 ],
  last: [ 38404.5, 38415, 38428.2, 38388.9 ],
  min: [ 37780.2, 37781.5, 37860.7, 37653.6 ],
  max: [ 39751.9, 39751.9, 39774, 39678.2 ],
  from: 2021-08-02T03:00:00.000Z,
  to: 2021-08-03T03:00:00.000Z
}

When I call asTypedEntry and pass the following class:

class SymbolPrice {
  open: number;
  close: number;
  high: number;
  low: number;

  static TIME_SERIES_VALUES: TimeSeriesValue<SymbolPrice> = [
    { field: "open", name: "Open" },
    { field: "close", name: "Close" },
    { field: "high", name: "High" },
    { field: "low", name: "Low" },
  ];
}

aggregatedHistoryQueryResult.results[0].asTypedEntry(SymbolPrice);

It is producing SymbolPrice instances that only have an open value set, and undefined set for the rest of the values:

TypedTimeSeriesRangeAggregation {
  from: 2021-08-02T03:00:00.000Z,
  to: 2021-08-03T03:00:00.000Z,
  min: SymbolPrice {
    open: 37780.2,
    close: undefined,
    high: undefined,
    low: undefined
  },
  max: SymbolPrice {
    open: 39751.9,
    close: undefined,
    high: undefined,
    low: undefined
  },
  first: SymbolPrice {
    open: 39147.8,
    close: undefined,
    high: undefined,
    low: undefined
  },
  last: SymbolPrice {
    open: 38404.5,
    close: undefined,
    high: undefined,
    low: undefined
  },
  sum: null,
  count: SymbolPrice {
    open: 1416,
    close: undefined,
    high: undefined,
    low: undefined
  },
  average: null
}

What I expect is that all the values are filled in.

I believe it has to do with this logic with asRollup (I am not exactly sure what it's purpose is):

If it was passed as false, I think it would work like I expect (and how the C# code works, since I ported this logic from .NET).

I can workaround this by not using asTypedEntry for now.

Map/Reduce in NodeJs

how can i create an index on nodejs that returns besides the desired fields also the id of the document? using linq from c# notation returns the id easily, but in nodejs using javascript notation I did not get it yet

Cannot query with where after moreLikeThis

A query such as:

async function getDuplicates(question: Question): Promise<Question[]> {
  const session = ravenStore.openSession();
  const query = session
    .query<Question>({ indexName: "Questions/Search" })
    .whereEquals("isArchived", false)
    .whereNotEquals("id", question.id)
    .whereEquals("level", question.level)
    .moreLikeThis(q =>
      q.usingDocument(
        JSON.stringify({
          question: question.question,
        })
      )
    )
    .take(5);

  if(question.language)   query.whereEquals("level", question.level)
  

  const similarQuestions = await query.all();

  return similarQuestions;
}

This fails:

  if(question.language)   query.whereEquals("level", question.level)

Because the previous operation in not a where, which is what this expects

GetDatabaseOperation throws when response is "successfully" a 404

Using this code snippet:

async function createDbIfNotExists(store: IDocumentStore) {
	const getDbOp = new GetDatabaseRecordOperation(store.database);
	const dbRecord: DatabaseRecord = await store.maintenance.send(getDbOp);

	console.log('DatabaseRecord does not exist for: ', store.database);

	if (!dbRecord) {
		const createResult = await store.maintenance.send(
			new CreateDatabaseOperation(
				{
					databaseName: store.database,
				}
			)
		);

		if (createResult?.name) {
			console.log('Automatically created database that did not exist: ' + createResult.name);
		}
	}
}

The code will fail on await store.maintenance.send(getDbOp) with an InvalidOperationException: Response is invalid error.

The reason is because this code path goes through RavenCommand._executeOnSpecificNode and the code path that handles the DatabaseNotFound is unreachable on a 404 response.

image

image

I was modeling this after the equivalent .NET SDK code where I can check for nullish if the DB does not exist and was hoping to do the same.

As a workaround, I believe I can simply have it try to create the DB anyway and let it fail silently.

I also had to use conventions.disableTopologyUpdates = true; since that was failing on initialize due to the DB missing.


SDK Version: 5.2.8
Platform: Windows
Node.js: 16.8.1

How to create multi-map index using this library?

I tried to do the following in the constructor of the index but did not work

this.map = `map(xxx);map(yyy);`;

Just one map like below works:

this.map = `map(xxx)`;

This multi-map index supported by this library at all?

Class 'AbstractDocumentQuery<T, TSelf>' incorrectly implements interface 'QueryEventsEmitter'

I'm getting an error trying to implement with Angular.

Here is the error:
ERROR in node_modules/ravendb/dist/Documents/Session/AbstractDocumentQuery.d.ts:44:31 - error TS2420: Class 'AbstractDocumentQuery<T, TSelf>' incorrectly implements interface 'QueryEventsEmitter'.
Type 'AbstractDocumentQuery<T, TSelf>' is missing the following properties from type 'QueryEventsEmitter': off, rawListeners

44 export declare abstract class AbstractDocumentQuery<T extends object, TSelf extends AbstractDocumentQuery<T, TSelf>> extends EventEmitter implements QueryEventsEmitter, IAbstractDocumentQuery {
~~~~~~~~~~~~~~~~~~~~~
node_modules/ravendb/dist/Documents/Session/DocumentQuery.d.ts:36:22 - error TS2420: Class 'DocumentQuery' incorrectly implements interface 'IDocumentQuery'.
Type 'DocumentQuery' is missing the following properties from type 'IDocumentQuery': off, rawListeners

36 export declare class DocumentQuery extends AbstractDocumentQuery<T, DocumentQuery> implements IDocumentQuery {
~~~~~~~~~~~~~
node_modules/ravendb/dist/Documents/Session/RawDocumentQuery.d.ts:9:22 - error TS2420: Class 'RawDocumentQuery' incorrectly implements interface 'IRawDocumentQuery'.
Type 'RawDocumentQuery' is missing the following properties from type 'IRawDocumentQuery': off, rawListeners

9 export declare class RawDocumentQuery extends AbstractDocumentQuery<T, RawDocumentQuery> implements IRawDocumentQuery {

my implementation:

raven-db.service.ts:

export class RavenDBService {
  private store = new DocumentStore(
    ["http://127.0.0.1:8080/"], "PES"
  );
  constructor() { 
    this.store.initialize();    
  }

  getSession() : any {
    return this.store.openSession();
  }
}

form-axample.component.ts:

...
constructor(private fb: FormBuilder, private store: RavenDBService) { }

  async onSubmit() {
    var session = this.store.getSession();
    await session.store(this.addressForm);
    await session.saveChanges();

    alert('Thanks!');
  }
}

Serialisation issue

When storing a value of a model, the SDK crashes under the following code:

const nullObj = Object.create(null)

ravenModel.someProp = nullObj
await session.saveChanges()

The reason is that nullObj doesn't have a prototype, which is used to determine the type of data (in Utility/TypeUtil)

Using load with includes returns null for documents that were previously stored

If we store and load within the same session without invoking saveChanges and with includes, like so:

await session.store({ id: '1', path: [] });
const doc = await session.load('1', {
  includes: ['path']
});

// expect document, actual null
console.log(doc);

the doc is null. Same example without includes or with invoking saveChanges works fine.

Reproducible in tests, namely ReadmeSamples => "loading data with include()".

Type clash with @types/node

There seems to be a type clash with the newest minor version of @types/node (18.16.x).

Minimal example

package.json (pinned versions for definiteness):

{
  "dependencies": {
    "ravendb": "5.2.10"
  },
  "devDependencies": {
    "@types/node": "18.16.0",
    "typescript": "5.1.3"
  },
  "scripts": {
    "build": "tsc"
  }
}

tsconfig.json:

{
  "compilerOptions": {
    "target": "es2019",
    "module": "commonjs",
    "esModuleInterop": true,
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "./dist",
    "noImplicitAny": true,
    "noFallthroughCasesInSwitch": true,
    "strictNullChecks": true,
    "alwaysStrict": true,
    "pretty": true,
  },
  "types": ["node"],
  "include": ["index.ts"],
  "exclude": ["node_modules", "dist"],
}

index.ts:

import { IDocumentStore } from "ravendb";

Expected behaviour

First, install the dependencies (npm i), then run npm run build. The project should build without problems.

Actual behaviour

We get the following error message:

node_modules/ravendb/dist/Documents/Session/DocumentResultStream.d.ts:11:18 - error TS2320: Interface 'DocumentResultStream<T>' cannot simultaneously extend types 'TypedEventEmitter<DocumentStreamResultEvents<T>>' and 'ReadableStream'.
  Named property 'listenerCount' of types 'TypedEventEmitter<DocumentStreamResultEvents<T>>' and 'ReadableStream' are not identical.

11 export interface DocumentResultStream<T extends object> extends TypedEventEmitter<DocumentStreamResultEvents<T>>, NodeJS.ReadableStream {
                    ~~~~~~~~~~~~~~~~~~~~


Found 1 error in node_modules/ravendb/dist/Documents/Session/DocumentResultStream.d.ts:11

The same problem does not arise with @types/node 18.15.x or lower.

Additional information

  • OS: Ubuntu 22.04
  • node version: 16.20.0
  • npm version: 9.7.1

Provide a way for closing sessions manually

While trying to use this library I found two quirks when handling sessions:

  1. There's no way (at least none that I've found) to manually and forcibly close a session.
  2. Opening a session leaves the app running forever.

Running this code finishes ok (node process exits with code 0):

import { DocumentStore } from 'ravendb'

const ds = new DocumentStore('http://localhost:8080', 'test').initialize()

Running this doesn't finish:

import { DocumentStore } from 'ravendb'

const ds = new DocumentStore('http://localhost:8080', 'test').initialize()
ds.openSession()

Running this doesn't finish either:

import { DocumentStore } from 'ravendb'

const ds = new DocumentStore('http://localhost:8080', 'test').initialize()
ds.openSession()
ds.dispose()

It would be nice to check why the app continues to run even when it should exit (idk, maybe a stray setInterval?), and to provide a way for closing the sessions at will (pretty much like the Go and the beta PHP clients do).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.