Skip to content

Commit

Permalink
Merge pull request #1501 from ynoza/nodeBind
Browse files Browse the repository at this point in the history
Node.js API prototyping
  • Loading branch information
mewim committed Apr 28, 2023
2 parents 175665d + 30bf5f8 commit fdd77ba
Show file tree
Hide file tree
Showing 28 changed files with 1,797 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ scripts/antlr4/antlr4.jar
# macOS
.DS_Store

### Node.js
tools/nodejs_api/node_modules/
tools/nodejs_api/cmake_install.cmake
tools/nodejs_api/package-lock.json
tools/nodejs_api/testDb/

# Archive files
*.zip
*.tar.gz
Expand Down
42 changes: 42 additions & 0 deletions tools/nodejs_api/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
cmake_minimum_required(VERSION 3.25)
project (kuzujs)

set(CMAKE_CXX_STANDARD 20)

add_definitions(-DNAPI_VERSION=5)

if (CMAKE_BUILD_TYPE EQUAL "DEBUG")
find_library(KUZU NAMES kuzu PATHS ../../build/debug/src)
set(THIRD_PARTY_BIN_PATH ../../build/debug/third_party)
else()
find_library(KUZU NAMES kuzu PATHS ../../build/release/src)
set(THIRD_PARTY_BIN_PATH ../../build/release/third_party)
endif()

get_filename_component(NODE_ADDON_API_INCLUDE_PATH ./node_modules/node-addon-api ABSOLUTE)
get_filename_component(KUZU_INCLUDE_PATH ../../src/include ABSOLUTE)
get_filename_component(ANTLR4_CYPHER_INCLUDE_PATH ../../third_party/antlr4_cypher/include ABSOLUTE)
get_filename_component(ANTLR4_RUNTIME_INCLUDE_PATH ../../third_party/antlr4_runtime/src ABSOLUTE)
get_filename_component(SPDLOG_INCLUDE_PATH ../../third_party/spdlog ABSOLUTE)
get_filename_component(NLOHMANN_JSON_INCLUDE_PATH ../../third_party/nlohmann_json ABSOLUTE)
get_filename_component(UTF8PROC_INCLUDE_PATH ../../third_party/utf8proc/include ABSOLUTE)
get_filename_component(CONCURRENT_QUEUE_INCLUDE_PATH ../../third_party/concurrentqueue ABSOLUTE)
get_filename_component(THIRD_PARTY_PATH ../../third_party ABSOLUTE)

include_directories(${CMAKE_JS_INC})
include_directories(${NODE_ADDON_API_INCLUDE_PATH})
include_directories(${KUZU_INCLUDE_PATH})
include_directories(${ANTLR4_CYPHER_INCLUDE_PATH})
include_directories(${ANTLR4_RUNTIME_INCLUDE_PATH})
include_directories(${SPDLOG_INCLUDE_PATH})
include_directories(${NLOHMANN_JSON_INCLUDE_PATH})
include_directories(${UTF8PROC_INCLUDE_PATH})
include_directories(${CONCURRENT_QUEUE_INCLUDE_PATH})

file(GLOB SOURCE_FILES ./src_cpp/*)
file(GLOB CMAKE_JS_SRC ./src_nodejs/*)
file(COPY ${CMAKE_JS_SRC} DESTINATION ./kuzu)

add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
target_link_libraries(${PROJECT_NAME} PRIVATE ${CMAKE_JS_LIB} ${KUZU})
25 changes: 25 additions & 0 deletions tools/nodejs_api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## Install dependency
```
npm i
```

## Build
```
npm run build
```

## Clean
```
npm run clean
```

## Run test
```
npm run test
```

## Run sample
```
node sample.js
```

28 changes: 28 additions & 0 deletions tools/nodejs_api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "kuzu",
"version": "0.0.1",
"description": "Node.js API for Kùzu graph database management system",
"main": "index.js",
"homepage": "https://kuzudb.com/",
"repository": {
"type": "git",
"url": "https://github.com/kuzudb/kuzu.git"
},
"scripts": {
"build": "(cd ../..; make release) && cmake-js compile && npm run js",
"clean": "cmake-js clean && rm -rf cmake_install.cmake Makefile",
"js": "cp src_nodejs/* build/kuzu/",
"all": "npm run clean && npm run build && node sample.js",
"test": "mocha",
"buildtest": "npm run build && mocha"
},
"author": "Kùzu Team",
"license": "MIT",
"dependencies": {
"chai": "^4.3.7",
"cmake-js": "^7.1.1",
"mocha": "^10.2.0",
"node-addon-api": "^6.0.0",
"tmp": "^0.2.1"
}
}
120 changes: 120 additions & 0 deletions tools/nodejs_api/sample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Make sure the test directory is removed as it will be recreated
const fs = require("fs");
const {Database, Connection} = require("./build/kuzu");

try {
fs.rmSync("./testDb", { recursive: true });
} catch (e) {
// ignore
}

async function executeAllCallback(err, queryResult) {
if (err) {
console.log(err);
} else {
await queryResult.all({"callback": (err, result) => {
if (err) {
console.log("All result with Callback failed");
console.log(err);
} else {
console.log(result);
console.log("All result received Callback");
}
}});
}
}

async function executeAllPromise(err, queryResult) {
if (err) {
console.log(err);
} else {
await queryResult.all().then(result => {
console.log(result);
console.log("All result received Promise");
}).catch(error => {
console.log("All with Promise failed");
console.log(error);
});
}
}

// Basic Case with all callback
const database = new Database("testDb", 1000000000);
console.log("The database looks like: ", database);
const connection = new Connection(database);
console.log ("The connection looks like: ", connection);
connection.execute("create node table person (ID INt64, fName StRING, gender INT64, isStudent BoOLEAN, isWorker BOOLEAN, age INT64, eyeSight DOUBLE, birthdate DATE, registerTime TIMESTAMP, lastJobDuration interval, workedHours INT64[], usedNames STRING[], courseScoresPerTerm INT64[][], grades INT64[], height DOUBLE, PRIMARY KEY (ID));", {"callback": executeAllCallback}).then(async r => {
await connection.execute("COPY person FROM \"../../dataset/tinysnb/vPerson.csv\" (HEADER=true);", {"callback": executeAllPromise})
const executeQuery = "MATCH (a:person) RETURN a.fName, a.age, a.eyeSight, a.isStudent;";
const parameterizedExecuteQuery = "MATCH (a:person) WHERE a.age > $1 and a.isStudent = $2 and a.fName < $3 RETURN a.fName, a.age, a.eyeSight, a.isStudent;";

connection.execute(executeQuery, {"callback": executeAllPromise});
connection.execute(parameterizedExecuteQuery, {
"callback": executeAllPromise,
"params": [["1", 29], ["2", true], ["3", "B"]]
});

// Extensive Case
connection.setMaxNumThreadForExec(2);
connection.execute(executeQuery, {"callback": executeAllCallback});

// Execute with each callback
connection.execute(executeQuery, {
"callback": async (err, result) => {
if (err) {
console.log(err);
} else {
await result.each(
(err, rowResult) => {
if (err) {
console.log(err)
} else {
console.log(rowResult);
}
},
() => {
console.log("all of the each's are done callback");
}
);
}
}
});

// Execute with promise + await
connection.execute(executeQuery).then(async queryResult => {
await queryResult.all({
"callback": (err, result) => {
if (err) {
console.log(err);
} else {
console.log(result);
console.log("All result received for execution with a promise");
}
}
});
}).catch(error => {
console.log("Execution with a promise failed");
console.log(error);
});

async function asyncAwaitExecute(executeQuery) {
const queryResult = await connection.execute(executeQuery);
return queryResult;
}

await asyncAwaitExecute(executeQuery).then(async queryResult => {
await queryResult.all({
"callback": (err, result) => {
if (err) {
console.log(err);
} else {
console.log(result);
console.log("All result received for execution with await");
}
}
});
}).catch(error => {
console.log("Execution with await failed");
console.log(error);
});
});
37 changes: 37 additions & 0 deletions tools/nodejs_api/src_cpp/execute_async_worker.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#include "include/execute_async_worker.h"

#include <chrono>
#include <thread>

#include "include/node_query_result.h"

using namespace Napi;

ExecuteAsyncWorker::ExecuteAsyncWorker(Function& callback, shared_ptr<kuzu::main::Connection>& connection,
std::string query, NodeQueryResult * nodeQueryResult, unordered_map<std::string, shared_ptr<kuzu::common::Value>>& params)
: AsyncWorker(callback), connection(connection), query(query), nodeQueryResult(nodeQueryResult)
, params(params) {};

void ExecuteAsyncWorker::Execute() {
try {
shared_ptr<kuzu::main::QueryResult> queryResult;
if (!params.empty()) {
auto preparedStatement = std::move(connection->prepare(query));
queryResult = connection->executeWithParams(preparedStatement.get(), params);
} else {
queryResult = connection->query(query);
}

if (!queryResult->isSuccess()) {
SetError("Query async execute unsuccessful: " + queryResult->getErrorMessage());
}
nodeQueryResult->SetQueryResult(queryResult);
}
catch(const std::exception &exc) {
SetError("Unsuccessful async execute: " + std::string(exc.what()));
}
};

void ExecuteAsyncWorker::OnOK() {
Callback().Call({Env().Null()});
};
23 changes: 23 additions & 0 deletions tools/nodejs_api/src_cpp/include/execute_async_worker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once
#include <napi.h>
#include "main/kuzu.h"
#include "node_query_result.h"

using namespace std;

class ExecuteAsyncWorker : public Napi::AsyncWorker {

public:
ExecuteAsyncWorker(Napi::Function& callback, shared_ptr<kuzu::main::Connection>& connection,
std::string query, NodeQueryResult * nodeQueryResult, unordered_map<std::string, shared_ptr<kuzu::common::Value>> & params);
virtual ~ExecuteAsyncWorker() {};

void Execute();
void OnOK();

private:
NodeQueryResult * nodeQueryResult;
std::string query;
shared_ptr<kuzu::main::Connection> connection;
unordered_map<std::string, shared_ptr<kuzu::common::Value>> params;
};
50 changes: 50 additions & 0 deletions tools/nodejs_api/src_cpp/include/node_connection.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#include <napi.h>
#include <iostream>
#include "main/kuzu.h"

using namespace std;

class NodeConnection : public Napi::ObjectWrap<NodeConnection> {
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports);
NodeConnection(const Napi::CallbackInfo& info);
~NodeConnection() = default;

private:
Napi::Value GetConnection(const Napi::CallbackInfo& info);
Napi::Value TransferConnection(const Napi::CallbackInfo& info);
Napi::Value Execute(const Napi::CallbackInfo& info);
void SetMaxNumThreadForExec(const Napi::CallbackInfo& info);
Napi::Value GetNodePropertyNames(const Napi::CallbackInfo& info);
shared_ptr<kuzu::main::Database> database;
shared_ptr<kuzu::main::Connection> connection;
uint64_t numThreads = 0;

struct ThreadSafeConnectionContext {
ThreadSafeConnectionContext(Napi::Env env, shared_ptr<kuzu::main::Connection> & connection,
shared_ptr<kuzu::main::Database> & database, uint64_t numThreads) :
deferred(Napi::Promise::Deferred::New(env)),
connection(connection),
database(database),
numThreads(numThreads){};

// Native Promise returned to JavaScript
Napi::Promise::Deferred deferred;

uint64_t numThreads = 0;

bool passed = true;

shared_ptr<kuzu::main::Connection> connection;
kuzu::main::Connection * connection2;
shared_ptr<kuzu::main::Database> database;

// Native thread
std::thread nativeThread;

Napi::ThreadSafeFunction tsfn;
};
ThreadSafeConnectionContext * context;
static void threadEntry(ThreadSafeConnectionContext * context);
static void FinalizerCallback(Napi::Env env, void* finalizeData, ThreadSafeConnectionContext* context);
};
16 changes: 16 additions & 0 deletions tools/nodejs_api/src_cpp/include/node_database.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#include <napi.h>
#include <iostream>
#include "main/kuzu.h"

using namespace std;

class NodeDatabase : public Napi::ObjectWrap<NodeDatabase> {
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports);
NodeDatabase(const Napi::CallbackInfo& info);
~NodeDatabase() = default;
friend class NodeConnection;

private:
shared_ptr<kuzu::main::Database> database;
};
30 changes: 30 additions & 0 deletions tools/nodejs_api/src_cpp/include/node_query_result.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#ifndef KUZU_NODE_QUERY_RESULT_H
#define KUZU_NODE_QUERY_RESULT_H

#include <napi.h>
#include <iostream>
#include "main/kuzu.h"
#include "binder/bound_statement_result.h"
#include "planner/logical_plan/logical_plan.h"
#include "processor/result/factorized_table.h"

using namespace std;

class NodeQueryResult: public Napi::ObjectWrap<NodeQueryResult> {
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports);
void SetQueryResult(shared_ptr<kuzu::main::QueryResult> & inputQueryResult);
NodeQueryResult(const Napi::CallbackInfo& info);
~NodeQueryResult() = default;

private:
void Close(const Napi::CallbackInfo& info);
Napi::Value All(const Napi::CallbackInfo& info);
Napi::Value Each(const Napi::CallbackInfo& info);
Napi::Value GetColumnDataTypes(const Napi::CallbackInfo& info);
Napi::Value GetColumnNames(const Napi::CallbackInfo& info);
shared_ptr<kuzu::main::QueryResult> queryResult;
};


#endif
Loading

0 comments on commit fdd77ba

Please sign in to comment.