diff --git a/.gitignore b/.gitignore index 49e3680..a92d61d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ testBox.js node_modules *.dat config.json -stockpile.json \ No newline at end of file +stockpile.json +dist/ \ No newline at end of file diff --git a/README.md b/README.md index 2ab7da3..99959c9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![License: MIT](https://img.shields.io/github/license/BRAVO68WEB/stockpile)](https://github.com/BRAVO68WEB/stockpile/blob/master/LICENSE) [![Twitter: BRAVO68WEB](https://img.shields.io/twitter/follow/BRAVO68WEB.svg?style=social)](https://twitter.com/BRAVO68WEB) -> A Tiny Redis Server ... +> A Tiny Redis Server with no external dependencies ... Stockpile is a Tiny Redis Server, created from scratch in Node.js with only few dependencies. It is a work in progress, and is not yet ready for production use. It was initially a part of [CodeCrafter's - Create Redis Challenge](https://app.codecrafters.io/courses/redis/). But, I decided to make it a standalone project. I will be adding more features to it, and will be using it in my future projects. I will also be adding more tests to it. If you want to contribute, please feel free to do so. @@ -14,6 +14,17 @@ Stockpile is a Tiny Redis Server, created from scratch in Node.js with only few ## Usage +```sh +git clone https://github.com/BRAVO68WEB/stockpile +cd stockpile +yarn +./spawn_redis_server.sh +``` + +OR + +>Not test with ts release + ```sh npx @bravo68web/stockpile help ``` @@ -22,6 +33,22 @@ npx @bravo68web/stockpile help - [Node.js](https://github.com/BRAVO68WEB/stockpile-node-sdk) +## ChangeLog + +- **Mar 11, 2023** - Initial Commit +- **Mar 11, 2023** - Added `SET`, `GET`, `DEL` and `PING` commands +- **Mar 12, 2023** - Added `ECHO`, `EXISTS`, `KEYS`, `APPEND`, `STRLEN` commands +- **Mar 12, 2023** - Added `INCR`, `DECR`, `INCRBY`, `DECRBY` commands +- **MAR 14, 2023** - Added `SETNX`, `SETRANGE`, `GETRANGE`, `MSET`, `MGET`, `FLUSHDB`, `FLUSHALL`, `DBSIZE`, `RANDOMKEY` commands +- **MAR 15, 2023** - Added `EXPIRE`, `TTL`, `PERSIST`, `EXPIREAT`, `PEXPIREAT`, `PERSIST`, `MOVE`, `SELECT` commands +- **MAR 17, 2023** - Added `Added feature to dump/restore data and `AUH` command support +- **MAR 18, 2023** - Release v0.1.0 and Added npx support +- **MAR 19, 2023** - Added `BSON` for buffer support and Added `/health` endpoint for HTTP health check +- **MAR 19, 2023** - Dockerizing application +- **MAY 8, 2023** - Added Github Actions to Build Image to `GitHub Container Registry` +- **MAY 9, 2023** - Rewrite code to Typescript +- *... more to come* + ## Development ```sh @@ -32,7 +59,7 @@ cd stockpile ## Run tests ```sh -node app/main.js & +./spawn_redis_server.sh & ./test_local.sh ``` diff --git a/app/bootstrap.js b/app/bootstrap.js deleted file mode 100644 index 9a2327e..0000000 --- a/app/bootstrap.js +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env node - -// This is the main entry point for the application. - -// CLI arguments are passed to this file, and then passed to the main application. -// Do not use any CLI framework here, as it will be difficult to maintain. - -const commandName = process.argv[2]; -const commandArgs = process.argv.slice(3); -const fs = require("fs"); -const os = require("os"); -const path = require("path"); -const pkg = require("../package.json"); - -if (commandName === "help") { - console.log("Stockpile CLI"); - console.log("Version " + pkg.version); - console.log(""); - console.log("Commands:"); - console.log( - " init --name --configpath --auth --port --dumppath | Create a new config file" - ); - console.log(" start --configpath | Start Stockpile"); - console.log(" help Display this help message"); - console.log(""); - console.log("Github : ", pkg.repository.url.split("+")[1]); - console.log("Author : ", pkg.author.name); - process.exit(0); -} else if (commandName === "init") { - try { - console.log("Initializing new Stockpile config file"); - - let config = { - name: "", - configpath: os.homedir() + "/.stockpile.config.json", - auth: "", - port: 6379, - dumppath: os.homedir() + "/.stockpile.dump", - }; - - if (commandArgs.length % 2 !== 0) { - console.log("Invalid init syntax"); - return; - } - - for (const i in commandArgs) { - if (commandArgs[i] === "--name") { - config.name = commandArgs[parseInt(i) + 1]; - } else if (commandArgs[i] === "--configpath") { - config.configpath = commandArgs[parseInt(i) + 1]; - } else if (commandArgs[i] === "--auth") { - config.auth = commandArgs[parseInt(i) + 1]; - } else if (commandArgs[i] === "--port") { - config.port = commandArgs[parseInt(i) + 1]; - } else if (commandArgs[i] === "--dumppath") { - config.dumppath = commandArgs[parseInt(i) + 1]; - } - } - - fs.writeFileSync( - path.resolve(config.configpath), - JSON.stringify(config) - ); - console.log("Config file created at " + config.configpath); - process.exit(0); - } catch (err) { - console.log(err); - process.exit(1); - } -} else if (commandName === "start") { - console.log("Starting Stockpile"); - - let configpath = ""; - for (const i in commandArgs) { - if (commandArgs[i] === "--configpath") { - configpath = commandArgs[parseInt(i) + 1]; - } - } - if (configpath === "") { - console.log("No config file specified"); - return; - } - - if (!fs.existsSync(configpath)) { - console.log("Config file does not exist"); - return; - } - - const config = JSON.parse(fs.readFileSync(configpath)); - - ["name", "configpath", "port", "dumppath"].forEach((key) => { - if (!config[key]) { - console.log("Invalid config file"); - process.exit(1); - } - }); - - const Stockpile = require("./main.js"); - Stockpile(config); -} else { - console.log("Invalid command"); -} diff --git a/app/bootstrap.ts b/app/bootstrap.ts new file mode 100644 index 0000000..3244c47 --- /dev/null +++ b/app/bootstrap.ts @@ -0,0 +1,114 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import Stockpile from "./main"; + +const [commandName, ...commandArgs] = process.argv.slice(2); +const version = "v0.2.0"; + +if (commandName === "help") { + console.log("Stockpile CLI"); + console.log(`Version : ${version}`); + console.log(""); + console.log("Commands:"); + console.log( + " init --name --configpath --auth --port --dumppath | Create a new config file" + ); + console.log(" start --configpath | Start Stockpile"); + console.log(" help Display this help message"); + console.log(""); + console.log("Github : ", "https://github.com/BRAVO68WEB/stockpile"); + console.log("Author : ", "https://github.com/BRAVO68WEB/"); + process.exit(0); +} else if (commandName === "init") { + try { + console.log("Initializing new Stockpile config file"); + + const config = { + name: "", + configpath: path.resolve(os.homedir(), ".stockpile.config.json"), + auth: "", + port: 6379, + dumppath: path.resolve(os.homedir(), ".stockpile.dump"), + }; + + if (commandArgs.length % 2 !== 0) { + console.log("Invalid init syntax"); + process.exit(1); + } + + for (let i = 0; i < commandArgs.length; i += 2) { + const option = commandArgs[i]; + const value = commandArgs[i + 1]; + + switch (option) { + case "--name": + config.name = value; + break; + case "--configpath": + config.configpath = value; + break; + case "--auth": + config.auth = value; + break; + case "--port": + config.port = parseInt(value); + break; + case "--dumppath": + config.dumppath = value; + break; + default: + console.log("Invalid init syntax"); + process.exit(1); + } + } + + fs.writeFileSync(config.configpath, JSON.stringify(config)); + console.log(`Config file created at ${config.configpath}`); + process.exit(0); + } catch (err) { + console.log(err); + process.exit(1); + } +} else if (commandName === "start") { + console.log(`Stockpile ${version}`); + + let configpath = ""; + + for (let i = 0; i < commandArgs.length; i += 2) { + const option = commandArgs[i]; + const value = commandArgs[i + 1]; + + if (option === "--configpath") { + configpath = value; + break; + } + } + + if (configpath === "") { + console.log("No config file specified"); + process.exit(1); + } + + if (!fs.existsSync(configpath)) { + console.log("Config file does not exist"); + process.exit(1); + } + + const config = JSON.parse(fs.readFileSync(configpath, "utf-8")); + + if ( + !config.name || + !config.configpath || + !config.port || + !config.dumppath + ) { + console.log("Invalid config file"); + process.exit(1); + } + + Stockpile(config); +} else { + console.log("Invalid command"); + process.exit(1); +} diff --git a/app/buffer.js b/app/buffer.ts similarity index 62% rename from app/buffer.js rename to app/buffer.ts index 5be1986..c8253db 100644 --- a/app/buffer.js +++ b/app/buffer.ts @@ -1,6 +1,9 @@ -const { BSON } = require("bson"); +import { BSON } from "bson"; -function createBinaryData(mapData) { +type MapData = Map[]; +type MapEntry = [string, any]; + +function createBinaryData(mapData: MapData) { const data = {}; // Add each map in the array to the data object for (let i = 0; i < mapData.length; i++) { @@ -12,25 +15,18 @@ function createBinaryData(mapData) { function readBinaryData(buffer) { const data = BSON.deserialize(buffer); - const mapData = []; + const mapData: MapData = []; // Convert each object in the data object to a Map - for (const key in data) { + for (const key in data as MapEntry) { const obj = data[key]; const map = new Map(); for (const [key, value] of Object.entries(obj)) { - if (typeof value === "object") { - map.set(key, value); - } else { - map.set(key, value); - } + map.set(key, value); } mapData.push(map); } return mapData; } -module.exports = { - createBinaryData, - readBinaryData, -}; +export { createBinaryData, readBinaryData }; diff --git a/app/main.js b/app/main.js deleted file mode 100644 index 9aff306..0000000 --- a/app/main.js +++ /dev/null @@ -1,533 +0,0 @@ -const net = require("net"); -const fs = require("fs"); -const path = require("path"); -const pkg = require("../package.json"); -const { createBinaryData, readBinaryData } = require("./buffer"); - -module.exports = (config) => { - const MAX_DATABASES = 16; - const DEFAULT_DATABASE = 0; - - const authConfig = { - enabled: config.auth ? true : false, - isAuthenticated: false, - password: config.auth ? config.auth : "", - }; - - let dataStore = new Array(MAX_DATABASES); - for (let i = 0; i < MAX_DATABASES; i++) { - dataStore[i] = new Map(); - } - - if (fs.existsSync(path.join(config.dumppath))) { - dataStore = fs.readFileSync(path.join(config.dumppath)); - dataStore = readBinaryData(dataStore); - } - - let currentDatabase = DEFAULT_DATABASE; - - const parseIncomingData = (data) => { - const lines = data.toString().split("\r\n"); - return lines; - }; - - const assignHandler = (command, connection) => { - command[2] = command[2].toUpperCase(); - console.log(command); - console.log(dataStore[currentDatabase]); - if ( - authConfig.enabled && - !authConfig.isAuthenticated && - command[2] !== "AUTH" - ) { - connection.write("-ERR NOAUTH Authentication required.\r\n"); - return; - } - - if (command[2] == "AUTH") { - if (authConfig.enabled) { - if (authConfig.isAuthenticated) { - connection.write("-ERR Already authenticated\r\n"); - } else if (command[4] == authConfig.password) { - authConfig.isAuthenticated = true; - connection.write("+OK Auth Successfull\r\n"); - } else if (command[4] != authConfig.password) { - connection.write("-ERR Invalid Password\r\n"); - } - } else { - connection.write("-ERR Authentication is not enabled\r\n"); - } - } else if (command[2] == "PING") { - connection.write("+PONG\r\n"); - } else if (command[2] == "SET") { - dataStore[currentDatabase].set(command[4], command[6]); - let expireTime = null; - - for (let i = 8; i < command.length; i += 2) { - if (command[i].toUpperCase() === "PX") { - expireTime = Date.now() + parseInt(command[i + 2]); - } else if (command[i].toUpperCase() === "EX") { - expireTime = Date.now() + parseInt(command[i + 2]) * 1000; - } - } - - if (expireTime) { - dataStore[currentDatabase].set( - command[4] + "_expire", - expireTime - ); - } - connection.write("+OK\r\n"); - } else if (command[2] == "GET") { - if ( - dataStore[currentDatabase].get(command[4] + "_expire") && - dataStore[currentDatabase].get(command[4] + "_expire") < - Date.now() - ) { - connection.write("$-1\r\n"); - dataStore[currentDatabase].delete(command[4]); - dataStore[currentDatabase].delete(command[4] + "_expire"); - } else if (dataStore[currentDatabase].get(command[4])) - connection.write( - "+" + dataStore[currentDatabase].get(command[4]) + "\r\n" - ); - else connection.write("$-1\r\n"); - } else if (command[2] == "DEL") { - dataStore[currentDatabase].delete(command[4]); - connection.write(":1\r\n"); - } else if (command[2] == "ECHO") { - connection.write( - "$" + command[4].length + "\r\n" + command[4] + "\r\n" - ); - } else if (command[2] == "EXISTS") { - if (dataStore[currentDatabase].has(command[4])) - connection.write(":1\r\n"); - else connection.write(":0\r\n"); - } else if (command[2] == "KEYS") { - let pattern = command[4]; - let keys = Array.from(dataStore[currentDatabase].keys()); - let matchingKeys = keys.filter((key) => { - const regex = new RegExp(pattern.replace("*", ".*")); - return regex.test(key); - }); - let response = "*" + matchingKeys.length + "\r\n"; - for (let key of matchingKeys) { - response += "$" + key.length + "\r\n" + key + "\r\n"; - } - connection.write(response); - } else if (command[2] == "APPEND") { - let key = command[4]; - let value = command[6]; - if (dataStore[currentDatabase].has(key)) { - let newValue = dataStore[currentDatabase].get(key) + value; - dataStore[currentDatabase].set(key, newValue); - connection.write(":" + newValue.length + "\r\n"); - } else { - dataStore[currentDatabase].set(key, value); - connection.write(":" + value.length + "\r\n"); - } - } else if (command[2] == "STRLEN") { - let key = command[4]; - if (dataStore[currentDatabase].has(key)) { - let value = dataStore[currentDatabase].get(key); - connection.write(":" + value.length + "\r\n"); - } else { - connection.write(":0\r\n"); - } - } else if (command[2] == "SETNX") { - let key = command[4]; - let value = command[6]; - if (dataStore[currentDatabase].has(key)) { - connection.write(":0\r\n"); - } else { - dataStore[currentDatabase].set(key, value); - connection.write(":1\r\n"); - } - } else if (command[2] == "SETRANGE") { - let key = command[4]; - let offset = command[6]; - let value = command[8]; - if (dataStore[currentDatabase].has(key)) { - let oldValue = dataStore[currentDatabase].get(key); - let newValue = - oldValue.substring(0, offset) + - value + - oldValue.substring(offset + value.length); - dataStore[currentDatabase].set(key, newValue); - connection.write(":" + newValue.length + "\r\n"); - } else { - dataStore[currentDatabase].set(key, value); - connection.write(":" + value.length + "\r\n"); - } - } else if (command[2] == "GETRANGE") { - let key = command[4]; - let start = command[6]; - let end = command[8]; - if (dataStore[currentDatabase].has(key)) { - let value = dataStore[currentDatabase].get(key); - let newValue = value.substring(start, end + 1); - connection.write( - "$" + newValue.length + "\r\n" + newValue + "\r\n" - ); - } else { - connection.write("$-1\r\n"); - } - } else if (command[2] == "MSET") { - const keyValuePairs = command.slice(4); - for (let i = 0; i < keyValuePairs.length; ) { - dataStore[currentDatabase].set( - keyValuePairs[i], - keyValuePairs[i + 2] - ); - i += 4; - } - connection.write("+OK\r\n"); - } else if (command[2] == "MGET") { - const keys = command.slice(1); - const values = keys.map( - (key) => dataStore[currentDatabase].get(key) ?? null - ); - - if (values.includes(null)) { - connection.write("$-1\r\n"); - } else { - const response = values.reduce((acc, value) => { - return `${acc}\r\n$${value.length}\r\n${value}`; - }, `*${values.length}`); - connection.write(response); - } - } else if (command[2] == "FLUSHDB") { - dataStore[currentDatabase].clear(); - connection.write("+OK\r\n"); - } else if (command[2] == "FLUSHALL") { - dataStore = []; - for (let i = 0; i < 16; i++) { - dataStore.push(new Map()); - } - connection.write("+OK\r\n"); - } else if (command[2] == "DBSIZE") { - let size = dataStore[currentDatabase].size; - connection.write(":" + size + "\r\n"); - } else if (command[2] == "SELECT") { - let database = command[4]; - currentDatabase = database; - connection.write("+OK\r\n"); - } else if (command[2] == "RANDOMKEY") { - let keys = Array.from(dataStore[currentDatabase].keys()); - if (keys.length == 0) { - connection.write("$-1\r\n"); - return; - } - let randomKey = keys[Math.floor(Math.random() * keys.length)]; - connection.write( - "$" + randomKey.length + "\r\n" + randomKey + "\r\n" - ); - } else if (command[2] == "INCR") { - let key = command[4]; - if (dataStore[currentDatabase].has(key)) { - let value = dataStore[currentDatabase].get(key); - let newValue = parseInt(value) + 1; - dataStore[currentDatabase].set(key, newValue.toString()); - connection.write(":" + newValue + "\r\n"); - } else { - dataStore[currentDatabase].set(key, "1"); - connection.write(":1\r\n"); - } - } else if (command[2] == "INCRBY") { - let key = command[4]; - let increment = command[6]; - if (dataStore[currentDatabase].has(key)) { - let value = dataStore[currentDatabase].get(key); - let newValue = parseInt(value) + parseInt(increment); - dataStore[currentDatabase].set(key, newValue.toString()); - connection.write(":" + newValue + "\r\n"); - } else { - dataStore[currentDatabase].set(key, increment); - connection.write(":" + increment + "\r\n"); - } - } else if (command[2] == "DECR") { - let key = command[4]; - if (dataStore[currentDatabase].has(key)) { - let value = dataStore[currentDatabase].get(key); - let newValue = parseInt(value) - 1; - dataStore[currentDatabase].set(key, newValue.toString()); - connection.write(":" + newValue + "\r\n"); - } else { - dataStore[currentDatabase].set(key, "-1"); - connection.write(":-1\r\n"); - } - } else if (command[2] == "DECRBY") { - let key = command[4]; - let decrement = command[6]; - if (dataStore[currentDatabase].has(key)) { - let value = dataStore[currentDatabase].get(key); - let newValue = parseInt(value) - parseInt(decrement); - dataStore[currentDatabase].set(key, newValue.toString()); - connection.write(":" + newValue + "\r\n"); - } else { - dataStore[currentDatabase].set(key, "-" + decrement); - connection.write(":-" + decrement + "\r\n"); - } - } else if (command[2] == "EXPIRE") { - const key = command[4]; - const seconds = parseInt(command[6]); - if (dataStore[currentDatabase].has(key)) { - const timestamp = Math.floor(Date.now() / 1000) + seconds; - dataStore[currentDatabase].set(`${key}:expire`, timestamp); - connection.write(":1\r\n"); - - setTimeout(() => { - if (dataStore[currentDatabase].has(key)) { - dataStore[currentDatabase].delete(key); - dataStore[currentDatabase].delete(`${key}:expire`); - } - }, seconds * 1000); - } else { - connection.write(":0\r\n"); - } - } else if (command[2] == "TTL") { - const key = command[4]; - if (dataStore[currentDatabase].has(key)) { - // Get the expiration time of the key - const expireKey = `${key}:expire`; - if (dataStore[currentDatabase].has(expireKey)) { - const timestamp = dataStore[currentDatabase].get(expireKey); - const ttl = Math.max( - timestamp - Math.floor(Date.now() / 1000), - 0 - ); - connection.write(`:${ttl}\r\n`); - } else { - connection.write(":-1\r\n"); - } - } else { - connection.write(":-2\r\n"); - } - } else if (command[2] == "EXPIREAT") { - const key = command[4]; - const timestamp = parseInt(command[6]); - if (dataStore[currentDatabase].has(key)) { - // Set the expiration time of the key - dataStore[currentDatabase].set(`${key}:expire`, timestamp); - connection.write(":1\r\n"); - } else { - connection.write(":0\r\n"); - } - } else if (command[2] == "PERSIST") { - const key = command[4]; - if (dataStore[currentDatabase].has(`${key}:expire`)) { - dataStore[currentDatabase].delete(`${key}:expire`); - connection.write(":1\r\n"); - } else { - connection.write(":0\r\n"); - } - } else if (command[2] == "HGET") { - const key = command[4]; - const field = command[6]; - if (dataStore[currentDatabase].has(key)) { - const value = dataStore[currentDatabase].get(key)[field]; - if (value !== undefined) { - connection.write(`$${value.length}\r\n${value}\r\n`); - } else { - connection.write("$-1\r\n"); - } - } else { - connection.write("$-1\r\n"); - } - } else if (command[2] == "HSET") { - const key = command[4]; - const field = command[6]; - const value = command[8]; - if (!dataStore[currentDatabase].has(key)) { - dataStore[currentDatabase].set(key, {}); - } - dataStore[currentDatabase].get(key)[field] = value; - connection.write(":1\r\n"); - } else if (command[2] == "HGETALL") { - const key = command[4]; - if (dataStore[currentDatabase].has(key)) { - const obj = dataStore[currentDatabase].get(key); - let response = "*" + Object.keys(obj).length * 2 + "\r\n"; - for (const [field, value] of Object.entries(obj)) { - response += `$${field.length}\r\n${field}\r\n`; - response += `$${value.length}\r\n${value}\r\n`; - } - connection.write(response); - } else { - connection.write("*0\r\n"); - } - } else if (command[2] == "HSETNX") { - const key = command[4]; - const field = command[6]; - const value = command[8]; - if (!dataStore[currentDatabase].has(key)) { - dataStore[currentDatabase].set(key, {}); - } - const obj = dataStore[currentDatabase].get(key); - if (obj[field] === undefined) { - obj[field] = value; - connection.write(":1\r\n"); - } else { - connection.write(":0\r\n"); - } - } else if (command[2] == "HMSET") { - const hash = command[1]; - const fieldsAndValues = command.slice(2); - const hashExists = dataStore[currentDatabase].hasOwnProperty(hash); - - if (!hashExists) { - dataStore[currentDatabase][hash] = {}; - } - - for (let i = 0; i < fieldsAndValues.length; i += 2) { - const field = fieldsAndValues[i]; - const value = fieldsAndValues[i + 1]; - dataStore[currentDatabase][hash][field] = value; - } - - connection.write("+OK\r\n"); - } else if (command[2] == "HINCRBY") { - const key = command[4]; - const field = command[6]; - const increment = Number(command[8]); - if (!dataStore[currentDatabase].has(key)) { - dataStore[currentDatabase].set(key, {}); - } - const obj = dataStore[currentDatabase].get(key); - if (obj[field] === undefined) { - obj[field] = "0"; - } - obj[field] = String(Number(obj[field]) + increment); - connection.write( - ":" + obj[field].length + "\r\n" + obj[field] + "\r\n" - ); - } else if (command[2] == "HDEL") { - const key = command[4]; - const fields = command.slice(6); - if (!dataStore[currentDatabase].has(key)) { - connection.write(":0\r\n"); - } else { - const obj = dataStore[currentDatabase].get(key); - let count = 0; - for (const field of fields) { - if (obj.hasOwnProperty(field)) { - delete obj[field]; - count += 1; - } - } - if (Object.keys(obj).length === 0) { - dataStore[currentDatabase].delete(key); - } - connection.write(":" + count + "\r\n"); - } - } else if (command[2] == "HEXISTS") { - const key = command[4]; - const field = command[6]; - if (!dataStore[currentDatabase].has(key)) { - connection.write(":0\r\n"); - } else { - const obj = dataStore[currentDatabase].get(key); - if (obj.hasOwnProperty(field)) { - connection.write(":1\r\n"); - } else { - connection.write(":0\r\n"); - } - } - } else if (command[2] == "HKEYS") { - const key = command[4]; - if (!dataStore[currentDatabase].has(key)) { - connection.write("*0\r\n"); - } else { - const obj = dataStore[currentDatabase].get(key); - const fields = Object.keys(obj); - let response = "*" + fields.length + "\r\n"; - for (const field of fields) { - response += "$" + field.length + "\r\n" + field + "\r\n"; - } - connection.write(response); - } - } else if (command[2] == "HLEN") { - const key = command[4]; - if (!dataStore[currentDatabase].has(key)) { - connection.write(":0\r\n"); - } else { - const obj = dataStore[currentDatabase].get(key); - const count = Object.keys(obj).length; - connection.write(":" + count + "\r\n"); - } - } else if (command[2] == "HSTRLEN") { - const key = command[4]; - const field = command[6]; - if (!dataStore[currentDatabase].has(key)) { - connection.write(":0\r\n"); - } else { - const obj = dataStore[currentDatabase].get(key); - if (obj.hasOwnProperty(field)) { - const value = obj[field]; - connection.write(":" + value.length + "\r\n"); - } else { - connection.write(":0\r\n"); - } - } - } else if (command[2] == "HVALS") { - const key = command[4]; - if (!dataStore[currentDatabase].has(key)) { - connection.write("*0\r\n"); - } else { - const obj = dataStore[currentDatabase].get(key); - const values = Object.values(obj); - let response = "*" + values.length + "\r\n"; - for (const value of values) { - response += "$" + value.length + "\r\n" + value + "\r\n"; - } - connection.write(response); - } - } else if (command[2] == "DUMP") { - saveDataToFile(dataStore); - connection.write("+OK Saving Current State\r\n"); - } else if (command[0].includes("health")) { - connection.write("OK !!\r\n"); - connection.end(); - } else if (command[2].includes("INFO")) { - connection.write(`+# Stockpile Server ${pkg.version}\r\n`); - } else { - connection.write("-ERR unknown command " + command[2] + "\r\n"); - } - }; - - let id = 0; - const server = net.createServer((connection) => { - connection.id = id; - id += 1; - connection.on("data", (data) => { - let command = parseIncomingData(data); - assignHandler(command, connection); - }); - - connection.on("end", (something) => { - console.log("Client disconnected"); - }); - - connection.on("error", (err) => { - console.log("Error in connection"); - console.log(err); - }); - }); - - server.listen(config.port, "0.0.0.0", () => { - console.log("Starting server"); - console.log("Stockpile server listening on port 6379"); - }); - - function saveDataToFile(data) { - const binaryData = createBinaryData(data); - fs.writeFileSync(config.dumppath, binaryData); - } - - process.on("SIGINT", () => { - console.log("\nTaking Snapshot !!"); - saveDataToFile(dataStore); - console.log("Shutting down server"); - process.exit(0); - }); -}; diff --git a/app/main.ts b/app/main.ts new file mode 100644 index 0000000..2604681 --- /dev/null +++ b/app/main.ts @@ -0,0 +1,673 @@ +import net, { Socket } from "net"; +import fs from "fs"; +import path from "path"; +import pkg from "../package.json" assert { type: "json" }; +import { createBinaryData, readBinaryData } from "./buffer"; + +interface IAuthConfig { + enabled: boolean; + isAuthenticated: boolean; + password: string; +} + +export interface IConfig { + port: number; + auth?: string; + dumppath: string; + configpath: string; + name: string; + logpath: string; +} + +interface IConnection extends Socket { + id: number; +} + +export default (config: IConfig) => { + const MAX_DATABASES = 16; + const DEFAULT_DATABASE = 0; + + const authConfig: IAuthConfig = { + enabled: config.auth ? true : false, + isAuthenticated: false, + password: config.auth ? config.auth : "", + }; + + let dataStore: any[] | Buffer = new Array(MAX_DATABASES); + for (let i = 0; i < MAX_DATABASES; i++) { + dataStore[i] = new Map(); + } + + if (fs.existsSync(path.join(config.dumppath))) { + dataStore = fs.readFileSync(path.join(config.dumppath)); + dataStore = readBinaryData(dataStore); + } + + let currentDatabase = DEFAULT_DATABASE; + + const parseIncomingData = (data) => { + const lines: string[] = data.toString().split("\r\n"); + return lines; + }; + + const assignHandler = (command: string[], connection: IConnection) => { + command[2] = command[2].toUpperCase(); + if ( + authConfig.enabled && + !authConfig.isAuthenticated && + command[2] !== "AUTH" + ) { + connection.write("-ERR NOAUTH Authentication required.\r\n"); + return; + } + + switch (command[2] as string) { + case "INFO": { + connection.write(`+# Stockpile Server ${pkg.version}\r\n`); + break; + } + case "AUTH": { + if (authConfig.enabled) { + if (authConfig.isAuthenticated) { + connection.write("-ERR Already authenticated\r\n"); + } else if (command[4] == authConfig.password) { + authConfig.isAuthenticated = true; + connection.write("+OK Auth Successfull\r\n"); + } else if (command[4] != authConfig.password) { + connection.write("-ERR Invalid Password\r\n"); + } + } else { + connection.write("-ERR Authentication is not enabled\r\n"); + } + break; + } + + case "PING": { + connection.write("+PONG\r\n"); + break; + } + + case "SET": { + dataStore[currentDatabase].set(command[4], command[6]); + let expireTime: number | null = null; + + for (let i = 8; i < command.length; i += 2) { + if (command[i].toUpperCase() === "PX") { + expireTime = Date.now() + parseInt(command[i + 2]); + } else if (command[i].toUpperCase() === "EX") { + expireTime = + Date.now() + parseInt(command[i + 2]) * 1000; + } + } + + if (expireTime) { + dataStore[currentDatabase].set( + command[4] + "_expire", + expireTime + ); + } + connection.write("+OK\r\n"); + break; + } + + case "GET": { + if ( + dataStore[currentDatabase].get(command[4] + "_expire") && + dataStore[currentDatabase].get(command[4] + "_expire") < + Date.now() + ) { + connection.write("$-1\r\n"); + dataStore[currentDatabase].delete(command[4]); + dataStore[currentDatabase].delete(command[4] + "_expire"); + } else if (dataStore[currentDatabase].get(command[4])) + connection.write( + "+" + + dataStore[currentDatabase].get(command[4]) + + "\r\n" + ); + else connection.write("$-1\r\n"); + break; + } + + case "DEL": { + dataStore[currentDatabase].delete(command[4]); + connection.write(":1\r\n"); + break; + } + + case "ECHO": { + connection.write( + "$" + command[4].length + "\r\n" + command[4] + "\r\n" + ); + break; + } + + case "EXISTS": { + if (dataStore[currentDatabase].has(command[4])) + connection.write(":1\r\n"); + else connection.write(":0\r\n"); + break; + } + + case "KEYS": { + let pattern = command[4]; + let keys = Array.from(dataStore[currentDatabase].keys()); + let matchingKeys = keys.filter((key: any) => { + const regex = new RegExp(pattern.replace("*", ".*")); + return regex.test(key); + }) as any[]; + let response = "*" + matchingKeys.length + "\r\n"; + for (let key of matchingKeys) { + response += "$" + key.length + "\r\n" + key + "\r\n"; + } + connection.write(response); + break; + } + + case "APPEND": { + let key = command[4]; + let value = command[6]; + if (dataStore[currentDatabase].has(key)) { + let newValue = dataStore[currentDatabase].get(key) + value; + dataStore[currentDatabase].set(key, newValue); + connection.write(":" + newValue.length + "\r\n"); + } else { + dataStore[currentDatabase].set(key, value); + connection.write(":" + value.length + "\r\n"); + } + break; + } + + case "STRLEN": { + let key = command[4]; + if (dataStore[currentDatabase].has(key)) { + let value = dataStore[currentDatabase].get(key); + connection.write(":" + value.length + "\r\n"); + } else { + connection.write(":0\r\n"); + } + break; + } + + case "SETNX": { + let key = command[4]; + let value = command[6]; + if (dataStore[currentDatabase].has(key)) { + connection.write(":0\r\n"); + } else { + dataStore[currentDatabase].set(key, value); + connection.write(":1\r\n"); + } + break; + } + + case "SETRANGE": { + let key = command[4]; + let offset = command[6]; + let value = command[8]; + if (dataStore[currentDatabase].has(key)) { + let oldValue = dataStore[currentDatabase].get(key); + let newValue = + oldValue.substring(0, offset) + + value + + oldValue.substring(offset + value.length); + dataStore[currentDatabase].set(key, newValue); + connection.write(":" + newValue.length + "\r\n"); + } else { + dataStore[currentDatabase].set(key, value); + connection.write(":" + value.length + "\r\n"); + } + break; + } + + case "GETRANGE": { + let key = command[4]; + let start = command[6]; + let end = command[8]; + if (dataStore[currentDatabase].has(key)) { + let value = dataStore[currentDatabase].get(key); + let newValue = value.substring(start, end + 1); + connection.write( + "$" + newValue.length + "\r\n" + newValue + "\r\n" + ); + } else { + connection.write("$-1\r\n"); + } + break; + } + + case "MSET": { + const keyValuePairs = command.slice(4); + for (let i = 0; i < keyValuePairs.length; ) { + dataStore[currentDatabase].set( + keyValuePairs[i], + keyValuePairs[i + 2] + ); + i += 4; + } + connection.write("+OK\r\n"); + break; + } + + case "MGET": { + const keys = command.slice(1); + const values = keys.map( + (key) => dataStore[currentDatabase].get(key) ?? null + ); + + if (values.includes(null)) { + connection.write("$-1\r\n"); + } else { + const response = values.reduce((acc, value) => { + return `${acc}\r\n$${value.length}\r\n${value}`; + }, `*${values.length}`); + connection.write(response); + } + break; + } + + case "FLUSHDB": { + dataStore[currentDatabase].clear(); + connection.write("+OK\r\n"); + break; + } + + case "FLUSHALL": { + dataStore = []; + for (let i = 0; i < 16; i++) { + dataStore.push(new Map()); + } + connection.write("+OK\r\n"); + break; + } + + case "DBSIZE": { + let size = dataStore[currentDatabase].size; + connection.write(":" + size + "\r\n"); + break; + } + + case "SELECT": { + let database = Number(command[4]); + currentDatabase = database; + connection.write("+OK\r\n"); + break; + } + + case "RANDOMKEY": { + let keys = Array.from(dataStore[currentDatabase].keys()); + if (keys.length == 0) { + connection.write("$-1\r\n"); + return; + } + let randomKey: string[] = keys[ + Math.floor(Math.random() * keys.length) + ] as string[]; + connection.write( + "$" + randomKey.length + "\r\n" + randomKey + "\r\n" + ); + break; + } + + case "INCR": { + let key = command[4]; + if (dataStore[currentDatabase].has(key)) { + let value = dataStore[currentDatabase].get(key); + let newValue = parseInt(value) + 1; + dataStore[currentDatabase].set(key, newValue.toString()); + connection.write(":" + newValue + "\r\n"); + } else { + dataStore[currentDatabase].set(key, "1"); + connection.write(":1\r\n"); + } + break; + } + + case "INCRBY": { + let key = command[4]; + let increment = command[6]; + if (dataStore[currentDatabase].has(key)) { + let value = dataStore[currentDatabase].get(key); + let newValue = parseInt(value) + parseInt(increment); + dataStore[currentDatabase].set(key, newValue.toString()); + connection.write(":" + newValue + "\r\n"); + } else { + dataStore[currentDatabase].set(key, increment); + connection.write(":" + increment + "\r\n"); + } + break; + } + + case "DECR": { + let key = command[4]; + if (dataStore[currentDatabase].has(key)) { + let value = dataStore[currentDatabase].get(key); + let newValue = parseInt(value) - 1; + dataStore[currentDatabase].set(key, newValue.toString()); + connection.write(":" + newValue + "\r\n"); + } else { + dataStore[currentDatabase].set(key, "-1"); + connection.write(":-1\r\n"); + } + break; + } + + case "DECRBY": { + let key = command[4]; + let decrement = command[6]; + if (dataStore[currentDatabase].has(key)) { + let value = dataStore[currentDatabase].get(key); + let newValue = parseInt(value) - parseInt(decrement); + dataStore[currentDatabase].set(key, newValue.toString()); + connection.write(":" + newValue + "\r\n"); + } else { + dataStore[currentDatabase].set(key, "-" + decrement); + connection.write(":-" + decrement + "\r\n"); + } + break; + } + + case "EXPIRE": { + const key = command[4]; + const seconds = parseInt(command[6]); + if (dataStore[currentDatabase].has(key)) { + const timestamp = Math.floor(Date.now() / 1000) + seconds; + dataStore[currentDatabase].set(`${key}:expire`, timestamp); + connection.write(":1\r\n"); + + setTimeout(() => { + if (dataStore[currentDatabase].has(key)) { + dataStore[currentDatabase].delete(key); + dataStore[currentDatabase].delete(`${key}:expire`); + } + }, seconds * 1000); + } else { + connection.write(":0\r\n"); + } + break; + } + + case "TTL": { + const key = command[4]; + if (dataStore[currentDatabase].has(key)) { + // Get the expiration time of the key + const expireKey = `${key}:expire`; + if (dataStore[currentDatabase].has(expireKey)) { + const timestamp = + dataStore[currentDatabase].get(expireKey); + const ttl = Math.max( + timestamp - Math.floor(Date.now() / 1000), + 0 + ); + connection.write(`:${ttl}\r\n`); + } else { + connection.write(":-1\r\n"); + } + } else { + connection.write(":-2\r\n"); + } + break; + } + + case "PERSIST": { + const key = command[4]; + if (dataStore[currentDatabase].has(`${key}:expire`)) { + dataStore[currentDatabase].delete(`${key}:expire`); + connection.write(":1\r\n"); + } else { + connection.write(":0\r\n"); + } + break; + } + + case "HGET": { + const key = command[4]; + const field = command[6]; + if (dataStore[currentDatabase].has(key)) { + const value = dataStore[currentDatabase].get(key)[field]; + if (value !== undefined) { + connection.write(`$${value.length}\r\n${value}\r\n`); + } else { + connection.write("$-1\r\n"); + } + } else { + connection.write("$-1\r\n"); + } + break; + } + + case "HSET": { + const key = command[4]; + const field = command[6]; + const value = command[8]; + if (!dataStore[currentDatabase].has(key)) { + dataStore[currentDatabase].set(key, {}); + } + dataStore[currentDatabase].get(key)[field] = value; + connection.write(":1\r\n"); + break; + } + + case "HGETALL": { + const key = command[4]; + if (dataStore[currentDatabase].has(key)) { + const obj = dataStore[currentDatabase].get(key); + let response = "*" + Object.keys(obj).length * 2 + "\r\n"; + for (const [field, value] of Object.entries(obj) as any) { + response += `$${field.length}\r\n${field}\r\n`; + response += `$${value.length}\r\n${value}\r\n`; + } + connection.write(response); + } else { + connection.write("*0\r\n"); + } + break; + } + + case "HSETNX": { + const key = command[4]; + const field = command[6]; + const value = command[8]; + if (!dataStore[currentDatabase].has(key)) { + dataStore[currentDatabase].set(key, {}); + } + const obj = dataStore[currentDatabase].get(key); + if (obj[field] === undefined) { + obj[field] = value; + connection.write(":1\r\n"); + } else { + connection.write(":0\r\n"); + } + break; + } + + case "HMSET": { + const hash = command[1]; + const fieldsAndValues = command.slice(2); + const hashExists = + dataStore[currentDatabase].hasOwnProperty(hash); + + if (!hashExists) { + dataStore[currentDatabase][hash] = {}; + } + + for (let i = 0; i < fieldsAndValues.length; i += 2) { + const field = fieldsAndValues[i]; + const value = fieldsAndValues[i + 1]; + dataStore[currentDatabase][hash][field] = value; + } + + connection.write("+OK\r\n"); + break; + } + + case "HINCRBY": { + const key = command[4]; + const field = command[6]; + const increment = Number(command[8]); + if (!dataStore[currentDatabase].has(key)) { + dataStore[currentDatabase].set(key, {}); + } + const obj = dataStore[currentDatabase].get(key); + if (obj[field] === undefined) { + obj[field] = "0"; + } + obj[field] = String(Number(obj[field]) + increment); + connection.write( + ":" + obj[field].length + "\r\n" + obj[field] + "\r\n" + ); + break; + } + + case "HDEL": { + const key = command[4]; + const fields = command.slice(6); + if (!dataStore[currentDatabase].has(key)) { + connection.write(":0\r\n"); + } else { + const obj = dataStore[currentDatabase].get(key); + let count = 0; + for (const field of fields) { + if (obj.hasOwnProperty(field)) { + delete obj[field]; + count += 1; + } + } + if (Object.keys(obj).length === 0) { + dataStore[currentDatabase].delete(key); + } + connection.write(":" + count + "\r\n"); + } + break; + } + + case "HEXISTS": { + const key = command[4]; + const field = command[6]; + if (!dataStore[currentDatabase].has(key)) { + connection.write(":0\r\n"); + } else { + const obj = dataStore[currentDatabase].get(key); + if (obj.hasOwnProperty(field)) { + connection.write(":1\r\n"); + } else { + connection.write(":0\r\n"); + } + } + break; + } + + case "HKEYS": { + const key = command[4]; + if (!dataStore[currentDatabase].has(key)) { + connection.write("*0\r\n"); + } else { + const obj = dataStore[currentDatabase].get(key); + const fields = Object.keys(obj); + let response = "*" + fields.length + "\r\n"; + for (const field of fields) { + response += + "$" + field.length + "\r\n" + field + "\r\n"; + } + connection.write(response); + } + break; + } + + case "HLEN": { + const key = command[4]; + if (!dataStore[currentDatabase].has(key)) { + connection.write(":0\r\n"); + } else { + const obj = dataStore[currentDatabase].get(key); + const count = Object.keys(obj).length; + connection.write(":" + count + "\r\n"); + } + break; + } + + case "HSTRLEN": { + const key = command[4]; + const field = command[6]; + if (!dataStore[currentDatabase].has(key)) { + connection.write(":0\r\n"); + } else { + const obj = dataStore[currentDatabase].get(key); + if (obj.hasOwnProperty(field)) { + const value = obj[field]; + connection.write(":" + value.length + "\r\n"); + } else { + connection.write(":0\r\n"); + } + } + break; + } + + case "HVALS": { + const key = command[4]; + if (!dataStore[currentDatabase].has(key)) { + connection.write("*0\r\n"); + } else { + const obj = dataStore[currentDatabase].get(key); + const values = Object.values(obj); + let response = "*" + values.length + "\r\n"; + for (const value of values as string[]) { + response += + "$" + value.length + "\r\n" + value + "\r\n"; + } + connection.write(response); + } + break; + } + + case "DUMP": { + saveDataToFile(dataStore); + connection.write("+OK Saving Current State\r\n"); + break; + } + + default: { + connection.write("-ERR unknown command " + command[2] + "\r\n"); + break; + } + } + }; + + let id = 0; + const handleConnection = (connection: IConnection) => { + connection.id = id; + id += 1; + + connection.on("data", (data) => { + let command = parseIncomingData(data); + assignHandler(command, connection); + }); + + connection.on("end", () => { + console.log(`Client ${connection.id} disconnected`); + }); + + connection.on("error", (err) => { + console.log(`Error in connection ${connection.id}`); + console.log(err); + }); + }; + + const server: net.Server = net.createServer(handleConnection as any); + + server.listen(config.port, "0.0.0.0", () => { + console.log("Listening on port " + config.port); + }); + + function saveDataToFile(data) { + const binaryData = createBinaryData(data); + fs.writeFileSync(config.dumppath, binaryData); + } + + process.on("SIGINT", () => { + console.log("\nTaking Snapshot !!"); + saveDataToFile(dataStore); + console.log("Shutting down server ..."); + process.exit(0); + }); +}; diff --git a/db/stockpile.db b/db/stockpile.db index 54efa45..af31986 100644 Binary files a/db/stockpile.db and b/db/stockpile.db differ diff --git a/package.json b/package.json index e7abe08..42aa7ae 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,20 @@ { "name": "@bravo68web/stockpile", - "version": "0.1.3", + "version": "0.2.0", "description": "A Tiny Redis Server ...", - "main": "app/bootstrap.js", + "main": "app/bootstrap.ts", "bin": { - "stockpile": "app/bootstrap.js" + "stockpile": "dist/app/bootstrap.js" }, + "type": "module", "scripts": { + "postinstall": "npm run build", "dev": "node app/bootstrap.js", - "test": "./test_local.sh" + "build": "rm -rf dist && tsc", + "dev:ts": "ts-node app/bootstrap.ts", + "prettier": "prettier --write \"**/*.{js,ts,json,md}\"", + "test": "./test_local.sh", + "app": "NODE_NO_WARNINGS=1 node --es-module-specifier-resolution=node --loader ts-node/esm ./dist/app/bootstrap.js" }, "repository": { "type": "git", @@ -37,6 +43,10 @@ "bson": "^5.1.0" }, "devDependencies": { - "prettier": "^2.8.4" + "@swc/wasm": "^1.3.57", + "@types/node": "^20.1.1", + "prettier": "^2.8.4", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" } } diff --git a/spawn_redis_server.sh b/spawn_redis_server.sh index 3ebc25d..fc9dadf 100755 --- a/spawn_redis_server.sh +++ b/spawn_redis_server.sh @@ -6,5 +6,5 @@ # # DON'T EDIT THIS! currentLoc=$(pwd) -node app/bootstrap.js init --name "stockpile-db" --port 6379 --configpath $currentLoc/stockpile.json --dumppath $currentLoc/stockpile.db -exec node app/bootstrap.js start --configpath $currentLoc/stockpile.json +yarn app init --name "stockpile-db" --port 6379 --configpath $currentLoc/stockpile.json --dumppath $currentLoc/db/stockpile.db +exec yarn app start --configpath $currentLoc/stockpile.json diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c66575b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "lib": ["es2018", "es5", "dom"], + "typeRoots": ["node_modules/@types", "./types"], + "resolveJsonModule": true, + "esModuleInterop": true, + "target": "ES2017", + "strict": true, + "module": "ESNext", + "moduleResolution": "node", + "outDir": "./dist", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "declaration": true, + "sourceMap": false, + "noImplicitAny": false + }, + "exclude": ["./node_modules/**/*", "./build/**/*"], + "include": ["./**/*.ts", "./**/*.tsx", "./**/*.json", "./**/*.js"], + "ts-node": { + "swc": true + } +} diff --git a/yarn.lock b/yarn.lock index 1cd611c..76a368e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,12 +2,131 @@ # yarn lockfile v1 +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@swc/wasm@^1.3.57": + version "1.3.57" + resolved "https://registry.yarnpkg.com/@swc/wasm/-/wasm-1.3.57.tgz#385e6fa1e36f238e78f8731b8bed6d2e4b07e304" + integrity sha512-K5E9cD+ZRDCPZoUIcem6xOEqyb6XxEepv6qm8Ck6l9KgWmvnsPc2ijMWCnGQlUYKl1Ff1WcJ+c54EFgaBHDbwQ== + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + +"@types/node@^20.1.1": + version "20.1.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.1.1.tgz#afc492e8dbe7f672dd3a13674823522b467a45ad" + integrity sha512-uKBEevTNb+l6/aCQaKVnUModfEMjAl98lw2Si9P5y4hLu9tm6AlX2ZIoXZX6Wh9lJueYPrGPKk5WMCNHg/u6/A== + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + bson@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/bson/-/bson-5.1.0.tgz#7b15cd9aa012b8bf9d320fbaefe15cc2fb657de2" integrity sha512-FEecNHkhYRBe7X9KDkdG12xNuz5VHGeH6mCE0B5sBmYtiR/Ux/9vUH/v4NUoBCDr6NuEhvahjoLiiRogptVW0A== +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + prettier@^2.8.4: version "2.8.4" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== + +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +typescript@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==