Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node.js API prototyping #1501

Merged
merged 1 commit into from
Apr 28, 2023
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
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