Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/target
.idea
Cargo.lock
index.node
node_modules
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
members = [
"grovedb",
"merk",
"node-grove"
]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# GroveDB
4 changes: 2 additions & 2 deletions grovedb/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::{

use merk::{self, rocksdb, Merk};
use rs_merkle::{algorithms::Sha256, MerkleTree};
use subtree::Element;
pub use subtree::Element;

/// Limit of possible indirections
const MAX_REFERENCE_HOPS: usize = 10;
Expand All @@ -27,7 +27,7 @@ pub enum Error {
RocksDBError(#[from] merk::rocksdb::Error),
#[error("unable to open Merk db")]
MerkError(merk::Error),
#[error("invalid path")]
#[error("invalid path: {0}")]
InvalidPath(&'static str),
#[error("unable to decode")]
BincodeError(#[from] bincode::Error),
Expand Down
18 changes: 18 additions & 0 deletions node-grove/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "node-grove"
version = "0.1.0"
description = "GroveDB node.js bindings"
edition = "2021"
license = "MIT"
exclude = ["index.node"]

[lib]
crate-type = ["cdylib"]

[dependencies]
grovedb = { path = "../grovedb" }

[dependencies.neon]
version = "0.9"
default-features = false
features = ["napi-6", "event-queue-api", "try-catch-api"]
84 changes: 84 additions & 0 deletions node-grove/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# node-grove

[![GroveDB npm package](https://img.shields.io/npm/v/@dashevo/node-grove.svg)](https://www.npmjs.com/package/@dashevo/node-grove)

`node-grove` is a GroveDB binding for node.js

`node-grove` is [available on npm](https://www.npmjs.com/package/@dashevo/node-grove)

## Usage

Add the module to your project with `npm install @dashevo/node-grove`.

## Example

```javascript
const GroveDB = require('@dashevo/node-grove');

(async function main() {
const groveDb = GroveDB.open('./test.db');

const tree_key = Buffer.from("test_tree");

const item_key = Buffer.from("test_key");
const item_value = Buffer.from("very nice test value");

const root_tree_path = [];
const item_tree_path = [tree_key];

// Making a subtree to insert items into
await groveDb.insert(
root_tree_path,
tree_key,
{ type: "tree", value: Buffer.alloc(32)
});

// Inserting an item into the subtree
await groveDb.insert(
item_tree_path,
item_key,
{ type: "item", value: item_value }
);

const element = await groveDb.get(item_tree_path, item_key);

// -> "item"
console.log(element.type);
// -> "very nice test value"
console.log(element.value.toString());

// Don't forget to close connection when you no longer need it
await groveDb.close();
})().catch(console.error);
```

## Building and testing

Run `npm run build` to build the package, `npm test` to test it.

## How it works

The main file that is used form the node.js side is `index.js`. It contains
class named `GroveDb`. The actual functions this class makes calls to are
stored in the `./src/lib.rs`. When building the project, it is compiled to
a file called `index.node`, that is imported into the `index.js` file.

Please note that the binding itself contains a lot of code. This is due to
the fact that GroveDB is not thread-safe, and needs to live in its own thread.
It communicates with the main binding thread through messages.

## Contributing

Everyone is welcome to contribute in any way or form! For further details,
please read [CONTRIBUTING.md](./CONTRIBUTING.md) (Which doesn't really exist in
this repo lol)

## Authors
- [Anton Suprunchuk](https://github.com/antouhou) - [Website](https://antouhou.com)

Also, see the list of contributors who participated in this project.

## License

This project is licensed under the MIT License - see the
[LICENSE.md](./LICENSE.md) file for details
72 changes: 72 additions & 0 deletions node-grove/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use strict";

const { promisify } = require("util");

// This file is crated when run `npm run build`. The actual source file that
// exports those functions is ./src/lib.rs
const { groveDbOpen, groveDbGet, groveDbInsert, groveDbProof, groveDbClose } = require("../index.node");

// Convert the DB methods from using callbacks to returning promises
const groveDbGetAsync = promisify(groveDbGet);
const groveDbInsertAsync = promisify(groveDbInsert);
const groveDbProofAsync = promisify(groveDbProof);
const groveDbCloseAsync = promisify(groveDbClose);

// Wrapper class for the boxed `Database` for idiomatic JavaScript usage
class GroveDB {
constructor(db) {
this.db = db;
}

static open(path) {
const db = groveDbOpen(path);
return new GroveDB(db);
}

/**
*
* @param {Buffer[]} path
* @param {Buffer} key
* @returns {Promise<Element>}
*/
async get(path, key) {
return groveDbGetAsync.call(this.db, path, key);
}

/**
*
* @param {Buffer[]} path
* @param {Buffer} key
* @param {Element} value
* @returns {Promise<*>}
*/
async insert(path, key, value) {
return groveDbInsertAsync.call(this.db, path, key, value);
}

/**
* Not implemented in GroveDB yet
*
* @returns {Promise<*>}
*/
async proof() {
return groveDbProofAsync.call(this.db);
}

/**
* Closes connection to the DB
*
* @returns {Promise<void>}
*/
async close() {
return groveDbCloseAsync.call(this.db);
}
}

/**
* @typedef Element
* @property {string} type - element type. Can be "item", "reference" or "tree"
* @property {Buffer|Buffer[]} value - element value
*/

module.exports = GroveDB;
79 changes: 79 additions & 0 deletions node-grove/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const GroveDB = require('./index.js');
const rimraf = require('rimraf');
const { promisify } = require("util");
const removeTestDataFiles = promisify(rimraf);
const { expect } = require('chai');

const testDataPath = './test_data';

describe('GroveDB', () => {
let groveDb;

beforeEach(() => {
groveDb = GroveDB.open(testDataPath);
});

afterEach(async () => {
await groveDb.close();
await removeTestDataFiles(testDataPath);
});

it('should store and retrieve a value', async function createGroveDb() {
const tree_key = Buffer.from("test_tree");

const item_key = Buffer.from("test_key");
const item_value = Buffer.from("very nice test value");

const root_tree_path = [];
const item_tree_path = [tree_key];

// Making a subtree to insert items into
await groveDb.insert(
root_tree_path,
tree_key,
{ type: "tree", value: Buffer.alloc(32)
});

// Inserting an item into the subtree
await groveDb.insert(
item_tree_path,
item_key,
{ type: "item", value: item_value }
);

const element = await groveDb.get(item_tree_path, item_key);

expect(element.type).to.be.equal("item");
expect(element.value.toString()).to.be.equal("very nice test value");
});

describe('#insert', () => {
it('should be able to insert a tree', async () => {
await groveDb.insert([], Buffer.from("test_tree"), { type: "tree", value: Buffer.alloc(32) })
});

it('should throw when trying to insert non-existent element type', async () => {
const path = [];
const key = Buffer.from("test_key");

try {
await groveDb.insert(path, key, { type: "not_a_tree", value: Buffer.alloc(32) })
expect.fail("Expected to throw en error");
} catch (e) {
expect(e.message).to.be.equal("Unexpected element type not_a_tree");
}
});

it('should throw when trying to insert a tree that is not 32 bytes', async () => {
const path = [];
const key = Buffer.from("test_key");

try {
await groveDb.insert(path, key, { type: "tree", value: Buffer.alloc(1) })
expect.fail("Expected to throw en error");
} catch (e) {
expect(e.message).to.be.equal("Tree buffer is expected to be 32 bytes long, but got 1");
}
});
})
});
97 changes: 97 additions & 0 deletions node-grove/src/converter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use grovedb::{Element};
use neon::{prelude::*, borrow::Borrow};

fn element_to_string(element: Element) -> String {
match element {
Element::Item(_) => { "item".to_string() }
Element::Reference(_) => { "reference".to_string() }
Element::Tree(_) => { "tree".to_string() }
}
}

pub fn js_object_to_element<'a, C: Context<'a>>(js_object: Handle<JsObject>, cx: &mut C) -> NeonResult<Element> {
let js_element_string = js_object.get(cx, "type")?.to_string(cx)?;
let value = js_object.get(cx, "value")?;

let element_string: String = js_element_string.value(cx);

match element_string.as_str() {
"item" => {
let js_buffer = value.downcast_or_throw::<JsBuffer, _>(cx)?;
let item = js_buffer_to_vec_u8(js_buffer, cx);
Ok(Element::Item(item))
},
"reference" => {
let js_array = value.downcast_or_throw::<JsArray, _>(cx)?;
let reference = js_array_of_buffers_to_vec(js_array, cx)?;
Ok(Element::Reference(reference))
},
"tree" => {
let js_buffer = value.downcast_or_throw::<JsBuffer, _>(cx)?;
let tree_vec = js_buffer_to_vec_u8(js_buffer, cx);
Ok(Element::Tree(
tree_vec
.try_into()
.or_else(|v: Vec<u8>| {
cx.throw_error(
format!("Tree buffer is expected to be 32 bytes long, but got {}", v.len())
)
})?
))
}
_ => {
cx.throw_error(format!("Unexpected element type {}", element_string))
}
}
}

pub fn element_to_js_object<'a, C: Context<'a>>(element: Element, cx: &mut C) -> NeonResult<Handle<'a, JsValue>> {
let js_object = cx.empty_object();
let js_type_string = cx.string(element_to_string(element.clone()));
js_object.set(cx, "type", js_type_string)?;

let js_value: Handle<JsValue> = match element {
Element::Item(item) => {
let js_buffer = JsBuffer::external(cx, item.clone());
js_buffer.upcast()
}
Element::Reference(reference) => {
let js_array: Handle<JsArray> = cx.empty_array();

for (index, bytes) in reference.iter().enumerate() {
let js_buffer = JsBuffer::external(cx, bytes.clone());
let js_value = js_buffer.as_value(cx);
js_array.set(cx, index as u32, js_value)?;
}

js_array.upcast()
}
Element::Tree(tree) => {
let js_buffer = JsBuffer::external(cx, tree.clone());
js_buffer.upcast()
}
};

js_object.set(cx, "value", js_value)?;
NeonResult::Ok(js_object.upcast())
}

pub fn js_buffer_to_vec_u8<'a, C: Context<'a>>(js_buffer: Handle<JsBuffer>, cx: &mut C) -> Vec<u8> {
let guard = cx.lock();
// let key_buffer = js_buffer.deref();
let key_memory_view = js_buffer.borrow(&guard);
let key_slice = key_memory_view.as_slice::<u8>();
key_slice.to_vec()
}

pub fn js_array_of_buffers_to_vec<'a, C: Context<'a>>(js_array: Handle<JsArray>, cx: &mut C) -> NeonResult<Vec<Vec<u8>>> {
let buf_vec = js_array.to_vec(cx)?;
let mut vec: Vec<Vec<u8>> = Vec::new();

for buf in buf_vec {
let js_buffer_handle = buf.downcast_or_throw::<JsBuffer, _>(cx)?;
vec.push(js_buffer_to_vec_u8(js_buffer_handle, cx));
}

Ok(vec)
}
Loading