├── .gitmodules ├── package.json ├── LICENSE ├── Dockerfile ├── .github └── workflows │ └── build-and-deploy.yml ├── index.js ├── .gitignore └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "explorer"] 2 | path = explorer 3 | url = https://github.com/kuzudb/explorer 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kuzu-api-server", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "scripts": { 6 | "serve": "NODE_ENV=production node index.js", 7 | "init-submodule": "git submodule update --init", 8 | "minify": "mv explorer/src/server . && rm -rf explorer && mkdir -p explorer/src && cp package.json explorer && mv server explorer/src/server", 9 | "clean": "rm -rf node_modules && rm -rf explorer" 10 | }, 11 | "license": "MIT", 12 | "dependencies": { 13 | "cors": "^2.8.5", 14 | "express": "^4.18.2", 15 | "kuzu": "0.11.2", 16 | "pino": "^8.16.1", 17 | "pino-pretty": "^10.2.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-2025 Kùzu Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-bookworm-slim 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | # Copy app 5 | COPY . /home/node/app 6 | 7 | # Make data and database directories 8 | RUN mkdir -p /database 9 | RUN mkdir -p /data 10 | RUN chown -R node:node /database 11 | RUN chown -R node:node /data 12 | 13 | # Install git, init submodules, minify, and remove git 14 | RUN cd /home/node/app &&\ 15 | apt-get update &&\ 16 | apt-get install -y git &&\ 17 | npm run clean &&\ 18 | npm run init-submodule &&\ 19 | npm run minify &&\ 20 | apt-get remove -y git &&\ 21 | apt-get autoremove -y &&\ 22 | apt-get clean &&\ 23 | chown -R node:node /home/node/app 24 | 25 | # Switch to node user 26 | USER node 27 | 28 | # Set working directory 29 | WORKDIR /home/node/app 30 | 31 | # Install dependencies and reduce size of kuzu node module 32 | RUN npm install &&\ 33 | rm -rf node_modules/kuzu/prebuilt node_modules/kuzu/kuzu-source 34 | 35 | # Expose port 36 | EXPOSE 8000 37 | 38 | # Set environment variables 39 | ENV NODE_ENV=production 40 | ENV PORT=8000 41 | ENV KUZU_DIR=/database 42 | ENV CROSS_ORIGIN=true 43 | 44 | # Run app 45 | ENTRYPOINT ["node", "index.js"] 46 | -------------------------------------------------------------------------------- /.github/workflows/build-and-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build-And-Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | docker: 11 | name: Build and push Docker image 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Prebuild - get version 17 | shell: bash 18 | run: | 19 | VERSION=$(node -e 'fs=require("fs");console.log(JSON.parse(fs.readFileSync("package.json")).dependencies.kuzu)') 20 | echo "VERSION=$VERSION" >> $GITHUB_ENV 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v3 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v3 25 | - name: Login to Docker Hub 26 | uses: docker/login-action@v3 27 | with: 28 | username: ${{ secrets.DOCKERHUB_USERNAME }} 29 | password: ${{ secrets.DOCKERHUB_TOKEN }} 30 | - name: Login to GitHub Container Registry 31 | uses: docker/login-action@v3 32 | with: 33 | registry: ghcr.io 34 | username: ${{ github.actor }} 35 | password: ${{ secrets.GITHUB_TOKEN }} 36 | - name: Build and push 37 | uses: docker/build-push-action@v5 38 | with: 39 | context: . 40 | platforms: linux/amd64,linux/arm64 41 | push: true 42 | tags: kuzudb/api-server:latest, kuzudb/api-server:${{ env.VERSION }}, ghcr.io/${{ github.repository_owner }}/api-server:latest, ghcr.io/${{ github.repository_owner }}/api-server:${{ env.VERSION }} 43 | build-args: | 44 | SKIP_GRAMMAR=true 45 | SKIP_BUILD_APP=true 46 | SKIP_DATASETS=true 47 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const cors = require("cors"); 3 | const process = require("process"); 4 | const database = require("./explorer/src/server/utils/Database"); 5 | const logger = require("./explorer/src/server/utils/Logger"); 6 | 7 | const schema = require("./explorer/src/server/Schema"); 8 | const cypher = require("./explorer/src/server/Cypher"); 9 | const state = require("./explorer/src/server/State"); 10 | 11 | const CROSS_ORIGIN = process.env.CROSS_ORIGIN 12 | ? process.env.CROSS_ORIGIN.toLowerCase() === "true" 13 | : false; 14 | 15 | process.on("SIGINT", () => { 16 | logger.info("SIGINT received, exiting"); 17 | process.exit(0); 18 | }); 19 | 20 | process.on("SIGTERM", () => { 21 | logger.info("SIGTERM received, exiting"); 22 | process.exit(0); 23 | }); 24 | 25 | const app = express(); 26 | 27 | if (CROSS_ORIGIN) { 28 | app.use(cors()); 29 | logger.info("CORS enabled for all origins"); 30 | } 31 | 32 | let PORT = parseInt(process.env.PORT); 33 | if (isNaN(PORT)) { 34 | PORT = 8000; 35 | } 36 | const MAX_PAYLOAD_SIZE = process.env.MAX_PAYLOAD_SIZE 37 | ? process.env.MAX_PAYLOAD_SIZE 38 | : "128mb"; 39 | 40 | const api = express.Router(); 41 | api.use("/schema", schema); 42 | api.use("/cypher", cypher); 43 | api.use("/", state); 44 | app.use(express.json({ limit: MAX_PAYLOAD_SIZE })); 45 | app.use("/", api); 46 | 47 | database 48 | .getDbVersion() 49 | .then((res) => { 50 | const version = res.version; 51 | const storageVersion = res.storageVersion; 52 | logger.info("Version of Kùzu: " + version); 53 | logger.info("Storage version of Kùzu: " + storageVersion); 54 | app.listen(PORT, () => { 55 | logger.info("Deployed server started on port: " + PORT); 56 | }); 57 | }) 58 | .catch((err) => { 59 | logger.error("Error getting version of Kùzu: " + err); 60 | }); 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kùzu API Server 2 | 3 | REST-style API server for the Kùzu graph database powered by Express.js. 4 | 5 | ## Get started 6 | 7 | Kùzu API Server is launched as a Docker container. Please refer to the [Docker documentation](https://docs.docker.com/get-docker/) for details on how to install and use Docker. 8 | 9 | To access an existing Kùzu database, you can mount its path to the `/database` directory as follows: 10 | 11 | ```bash 12 | docker run -p 8000:8000 \ 13 | -v {path to the directory containing the database file}:/database \ 14 | -e KUZU_FILE={database file name} \ 15 | --rm kuzudb/api-server:latest 16 | ``` 17 | 18 | By mounting local database files to Docker via `-v {path to the directory containing the database file}:/database` and `-e KUZU_FILE={database file name}`, 19 | the changes done through the API server will persist to the local database files after the server is shutdown. If the directory is mounted but the `KUZU_FILE` environment variable is not set, the API server will look for a file named `database.kz` in the mounted directory or create a new database file named `database.kz` in the mounted directory if it does not exist. 20 | 21 | The `--rm` flag tells docker that the container should automatically be removed after we close docker. 22 | 23 | If the launching is successful, you should see the logs similar to the following in your shell: 24 | 25 | ``` 26 | [00:46:50.833] INFO (1): Access mode: READ_WRITE 27 | [00:46:50.834] INFO (1): CORS enabled for all origins 28 | [00:46:50.853] INFO (1): Version of Kùzu: 0.3.1 29 | [00:46:50.854] INFO (1): Deployed server started on port: 8000 30 | ``` 31 | 32 | ### Additional launch configurations 33 | 34 | #### Access mode 35 | 36 | By default, the API server is launched in read-write mode, which means that you can modify the database. If you want to launch it in read-only mode, you can do so by setting the `MODE` environment variable to `READ_ONLY` as follows. 37 | 38 | ```bash 39 | docker run -p 8000:8000 \ 40 | -v {path to the directory containing the database file}:/database \ 41 | -e KUZU_FILE={database file name} \ 42 | -e MODE=READ_ONLY \ 43 | --rm kuzudb/api-server:latest 44 | ``` 45 | 46 | The API server will then be launched in read-only mode, and you will see the following log message: 47 | 48 | ``` 49 | [00:46:50.833] INFO (1): Access mode: READ_ONLY 50 | ``` 51 | 52 | In read-only mode, you can still issue read queries, but you cannot run write queries or modify the schema. 53 | 54 | #### Buffer pool size 55 | 56 | By default, the API server is launched with a maximum buffer pool size of 80% of the available memory. If you want to launch API server with a different buffer pool size, you can do so by setting the `KUZU_BUFFER_POOL_SIZE` environment variable to the desired value in bytes as follows. 57 | 58 | For example, to launch the API server with a buffer pool size of 1GB, you can run the following command. 59 | 60 | ```bash 61 | docker run -p 8000:8000 \ 62 | -v {path to the directory containing the database file}:/database \ 63 | -e KUZU_FILE={database file name} \ 64 | -e KUZU_BUFFER_POOL_SIZE=1073741824 \ 65 | --rm kuzudb/api-server:latest 66 | ``` 67 | 68 | #### Cross-Origin Resource Sharing (CORS) 69 | 70 | By default, the API server is launched with CORS enabled for all origins. If you want to disable CORS, you can do so by setting the `CROSS_ORIGIN` environment variable to `false` as follows. 71 | 72 | ```bash 73 | docker run -p 8000:8000 \ 74 | -v {path to the directory containing the database file}:/database \ 75 | -e KUZU_FILE={database file name} \ 76 | -e CROSS_ORIGIN=false \ 77 | --rm kuzudb/api-server:latest 78 | ``` 79 | 80 | ### Launch with Podman 81 | 82 | If you are using [Podman](https://podman.io/) instead of Docker, you can launch the API server by replacing `docker` with `podman` in the commands above. However, note that by default Podman maps the default user account to the `root` user in the container. This may cause permission issues when mounting local database files to the container. To avoid this, you can use the `--userns=keep-id` flag to keep the user ID of the current user inside the container, or enable `:U` option for each volume to change the owner and group of the source volume to the current user. 83 | 84 | For example: 85 | 86 | ```bash 87 | podman run -p 8000:8000 \ 88 | -v {path to the directory containing the database file}:/database:U \ 89 | -e KUZU_FILE={database file name} \ 90 | --rm kuzudb/api-server:latest 91 | ``` 92 | 93 | or, 94 | 95 | ```bash 96 | podman run -p 8000:8000 \ 97 | -v {path to the directory containing the database file}:/database \ 98 | -e KUZU_FILE={database file name} \ 99 | --userns=keep-id \ 100 | --rm kuzudb/api-server:latest 101 | ``` 102 | 103 | Please refer to the official Podman docs for [mounting external volumes](https://docs.podman.io/en/latest/markdown/podman-run.1.html#mounting-external-volumes) and [user namespace mode](https://https://docs.podman.io/en/latest/markdown/podman-run.1.html#userns-mode) for more information. 104 | 105 | ## API endpoints 106 | 107 | The Kùzu API server provides the following endpoints: 108 | 109 | ### `GET /`: 110 | 111 | Get the status of the server. 112 | 113 | #### Example usage: 114 | 115 | With `fetch` in JavaScript: 116 | 117 | ```javascript 118 | fetch("http://localhost:8000") 119 | .then((response) => response.json()) 120 | .then((data) => console.log(data)); 121 | ``` 122 | 123 | With `curl` in the terminal: 124 | 125 | ```bash 126 | curl http://localhost:8000 127 | ``` 128 | 129 | With `request` in Python: 130 | 131 | ```python 132 | import requests 133 | response = requests.get("http://localhost:8000") 134 | print(response.json()) 135 | ``` 136 | 137 | #### Example response: 138 | 139 | ```json 140 | { 141 | "status": "ok", 142 | "version": "0.3.1", 143 | "mode": "READ_WRITE" 144 | } 145 | ``` 146 | 147 | ### `GET /schema`: 148 | 149 | Get the schema of the database. 150 | 151 | #### Example usage: 152 | 153 | With `fetch` in JavaScript: 154 | 155 | ```javascript 156 | fetch("http://localhost:8000/schema") 157 | .then((response) => response.json()) 158 | .then((data) => console.log(data)); 159 | ``` 160 | 161 | With `curl` in the terminal: 162 | 163 | ```bash 164 | curl http://localhost:8000/schema 165 | ``` 166 | 167 | With `request` in Python: 168 | 169 | ```python 170 | import requests 171 | response = requests.get("http://localhost:8000/schema") 172 | print(response.json()) 173 | ``` 174 | 175 | #### Example response: 176 | 177 | ```json 178 | { 179 | "nodeTables": [ 180 | { 181 | "name": "City", 182 | "comment": "", 183 | "properties": [ 184 | { 185 | "name": "name", 186 | "type": "STRING", 187 | "isPrimaryKey": true 188 | }, 189 | { 190 | "name": "population", 191 | "type": "INT64", 192 | "isPrimaryKey": false 193 | } 194 | ] 195 | }, 196 | { 197 | "name": "User", 198 | "comment": "", 199 | "properties": [ 200 | { 201 | "name": "name", 202 | "type": "STRING", 203 | "isPrimaryKey": true 204 | }, 205 | { 206 | "name": "age", 207 | "type": "INT64", 208 | "isPrimaryKey": false 209 | } 210 | ] 211 | } 212 | ], 213 | "relTables": [ 214 | { 215 | "name": "Follows", 216 | "comment": "", 217 | "properties": [ 218 | { 219 | "name": "since", 220 | "type": "INT64" 221 | } 222 | ], 223 | "src": "User", 224 | "dst": "User" 225 | }, 226 | { 227 | "name": "LivesIn", 228 | "comment": "", 229 | "properties": [], 230 | "src": "User", 231 | "dst": "City" 232 | } 233 | ], 234 | "relGroups": [], 235 | "rdf": [] 236 | } 237 | ``` 238 | 239 | ### `POST /cypher`: 240 | 241 | Execute a Cypher query and get the result. The request body should be a JSON object with a `query` field containing the Cypher query and an optional `params` field containing the parameters for the query (if the query is a parameterized query / prepared statement). 242 | 243 | #### Example usage: 244 | 245 | With `fetch` in JavaScript: 246 | 247 | ```javascript 248 | fetch("http://localhost:8000/cypher", { 249 | method: "POST", 250 | headers: { 251 | "Content-Type": "application/json", 252 | }, 253 | body: JSON.stringify({ 254 | query: "MATCH (u:User) WHERE u.age > $a RETURN u", 255 | params: { 256 | a: 20, 257 | }, 258 | }), 259 | }) 260 | .then((response) => response.text()) 261 | .then((data) => console.log(data)); 262 | ``` 263 | 264 | With `curl` in the terminal: 265 | 266 | ```bash 267 | curl -X POST\ 268 | -H "Content-Type: application/json" \ 269 | -d '{"query":"MATCH (u:User) WHERE u.age > $a RETURN u","params":{"a":25}}' \ 270 | http://localhost:8000/cypher 271 | ``` 272 | 273 | With `request` in Python: 274 | 275 | ```python 276 | import requests 277 | response = requests.post("http://localhost:8000/cypher", \ 278 | json={ 279 | "query": "MATCH (u:User) WHERE u.age > $a RETURN u", 280 | "params": {"a": 25} 281 | } 282 | ) 283 | print(response.json()) 284 | ``` 285 | 286 | #### Example response: 287 | 288 | ```json 289 | { 290 | "rows": [ 291 | { 292 | "u": { 293 | "name": "Adam", 294 | "age": 30, 295 | "_label": "User", 296 | "_id": { "offset": 0, "table": 0 } 297 | } 298 | }, 299 | { 300 | "u": { 301 | "name": "Karissa", 302 | "age": 40, 303 | "_label": "User", 304 | "_id": { "offset": 1, "table": 0 } 305 | } 306 | }, 307 | { 308 | "u": { 309 | "name": "Zhang", 310 | "age": 50, 311 | "_label": "User", 312 | "_id": { "offset": 2, "table": 0 } 313 | } 314 | } 315 | ], 316 | "dataTypes": { "u": "NODE" }, 317 | "isSchemaChanged": false 318 | } 319 | ``` 320 | 321 | ## Deployment 322 | 323 | A [GitHub actions pipeline](.github/workflows/build-and-deploy.yml) has been configured to automatically build and deploy 324 | the Docker image to [Docker Hub](https://hub.docker.com/) upon pushing to the master branch. The pipeline will build images 325 | for both `amd64` and `arm64` platforms. 326 | 327 | ## Contributing 328 | 329 | We welcome contributions to Kùzu API Server. By contributing to Kùzu API Server, you agree that your contributions will be licensed under the [MIT License](LICENSE). 330 | --------------------------------------------------------------------------------