milodb 1.0.7 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "env": {
3
+ "node": true,
4
+ "es2021": true
5
+ },
6
+ "extends": "eslint:recommended",
7
+ "parserOptions": {
8
+ "ecmaVersion": 12,
9
+ "sourceType": "module"
10
+ },
11
+ "rules": {
12
+ "indent": ["error", 4],
13
+ "linebreak-style": ["error", "unix"],
14
+ "quotes": ["error", "single"],
15
+ "semi": ["error", "always"],
16
+ "no-unused-vars": ["warn"],
17
+ "no-console": ["warn", { "allow": ["warn", "error"] }]
18
+ }
19
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.1] - 2024-03-19
9
+
10
+ ### Changed
11
+ - Updated package metadata
12
+ - Added ESLint configuration
13
+ - Added comprehensive documentation
14
+ - Added benchmark tools
15
+ - Added proper project structure
16
+
17
+ ## [1.0.0] - 2024-03-19
18
+
19
+ ### Added
20
+ - Initial release
21
+ - Basic key-value storage functionality
22
+ - Optional encryption using AES-256-GCM
23
+ - TTL (Time To Live) support
24
+ - Batch operations
25
+ - Advanced search with regex support
26
+ - Database statistics
27
+ - Comprehensive test suite
28
+ - Benchmark tools
29
+ - Documentation
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 MiloDB
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.
package/README.md CHANGED
@@ -1,141 +1,119 @@
1
- Here’s a sample `README.md` file that explains how to use the database manager NPM package you've created.
2
-
3
- ---
4
-
5
- # JSON Database Manager with Encryption
6
-
7
- This is a simple database manager that allows you to store and manage your data in encrypted JSON files. It provides an easy-to-use CLI interface to perform CRUD operations (Create, Read, Update, Delete) on tables stored as JSON files.
8
-
9
- ### Features
10
- - **Encrypted Data**: Store your data securely using AES encryption.
11
- - **CRUD Operations**: Create, read, update, and delete records in JSON tables.
12
- - **Command-Line Interface (CLI)**: Interact with the database using simple CLI commands.
13
-
14
- ### Table of Contents
15
- - [Installation](#installation)
16
- - [Usage](#usage)
17
- - [Create a Table](#create-a-table)
18
- - [Add a Record](#add-a-record)
19
- - [Edit a Record](#edit-a-record)
20
- - [Delete a Record](#delete-a-record)
21
- - [Get All Records](#get-all-records)
22
- - [License](#license)
23
-
24
- ---
25
-
26
- ## Installation
27
-
28
- 1. Clone the repository or download the package.
29
-
30
- 2. Install the dependencies:
31
- ```bash
32
- npm install
33
- ```
34
-
35
- 3. Link the package globally to use the CLI commands:
36
- ```bash
37
- npm link
38
- ```
39
-
40
- 4. The commands will now be available globally through the terminal.
41
-
42
- ---
43
-
44
- ## Usage
45
-
46
- Once installed, you can use the following commands to manage your encrypted JSON database.
47
-
48
- ### Create a Table
49
-
50
- To create a new table, run the following command:
51
-
52
- ```bash
53
- db-manager createTable <tableName>
54
- ```
55
-
56
- This will create a new table (a JSON file) in the `./db` directory. The table will be initialized as an empty array.
57
-
58
- **Example**:
59
- ```bash
60
- db-manager createTable users
61
- ```
62
-
63
- This will create a table named `users.json` inside the `./db` folder.
64
-
65
- ### Add a Record
66
-
67
- To add a new record to a table, use the `addRecord` command. The record must be a valid JSON object.
68
-
69
- ```bash
70
- db-manager addRecord <tableName> <record>
71
- ```
72
-
73
- **Example**:
74
- ```bash
75
- db-manager addRecord users '{"id": 1, "name": "John Doe", "email": "john@example.com"}'
76
- ```
77
-
78
- This will add the record to the `users` table. The data will be encrypted before being stored.
79
-
80
- ### Edit a Record
81
-
82
- To edit an existing record, use the `editRecord` command. You need to specify the table name, the `recordId` (the ID of the record to be updated), and the new updated record in JSON format.
83
-
84
- ```bash
85
- db-manager editRecord <tableName> <recordId> <updatedRecord>
86
- ```
87
-
88
- **Example**:
89
- ```bash
90
- db-manager editRecord users 1 '{"id": 1, "name": "John Doe", "email": "john.doe@example.com"}'
91
- ```
92
-
93
- This will update the record with ID `1` in the `users` table.
94
-
95
- ### Delete a Record
96
-
97
- To delete a record from a table, use the `deleteRecord` command. Provide the table name and the `recordId` of the record you want to delete.
98
-
99
- ```bash
100
- db-manager deleteRecord <tableName> <recordId>
101
- ```
102
-
103
- **Example**:
104
- ```bash
105
- db-manager deleteRecord users 1
106
- ```
107
-
108
- This will delete the record with ID `1` from the `users` table.
109
-
110
- ### Get All Records
111
-
112
- To retrieve all records from a table, use the `getRecords` command. This will output all the records in the specified table.
113
-
114
- ```bash
115
- db-manager getRecords <tableName>
116
- ```
117
-
118
- **Example**:
119
- ```bash
120
- db-manager getRecords users
121
- ```
122
-
123
- This will display all the records from the `users` table. The data will be decrypted before being shown.
124
-
125
- ---
126
-
127
- ## License
128
-
129
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
130
-
131
- ---
132
-
133
- ### Notes
134
-
135
- - All the data is stored encrypted for security. When retrieving data, it will be automatically decrypted before being displayed.
136
- - Make sure you use a strong `secretKey` in the `encrypt.js` file for enhanced security.
137
- - This package uses the `fs-extra` module to interact with the file system and store data as JSON files.
138
-
139
- ---
140
-
141
- This README will help users get started with the database manager and use it for simple operations on encrypted JSON data files.
1
+ # milodb
2
+
3
+ A simple mini database with optional encryption to store key-value pairs. Features include encryption, TTL support, batch operations, and advanced search capabilities.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install milodb
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```javascript
14
+ const MinoDB = require('milodb');
15
+
16
+ // Initialize with an optional password for encryption and flag to enable/disable encryption
17
+ const db = new MinoDB('my-secret-password', true); // true to encrypt data
18
+
19
+ // Basic operations
20
+ await db.set('name', 'John Doe');
21
+ console.log(await db.get('name')); // Output: John Doe
22
+
23
+ // Set with TTL (Time To Live) in milliseconds
24
+ await db.set('temp', 'This will expire', 3600000); // Expires in 1 hour
25
+
26
+ // Delete an entry
27
+ await db.delete('name');
28
+ console.log(await db.get('name')); // Output: null
29
+
30
+ // Clear all entries
31
+ await db.clear();
32
+
33
+ // Advanced search with options
34
+ const results = await db.search('John', {
35
+ regex: false, // Use regex for searching
36
+ caseSensitive: false, // Case-sensitive search
37
+ limit: 10 // Limit number of results
38
+ });
39
+
40
+ // Batch operations
41
+ const batchResults = await db.batch([
42
+ { type: 'set', key: 'key1', value: 'value1', ttl: 3600000 },
43
+ { type: 'set', key: 'key2', value: 'value2' },
44
+ { type: 'delete', key: 'key3' }
45
+ ]);
46
+
47
+ // Get database statistics
48
+ const stats = await db.stats();
49
+ console.log(stats);
50
+ // Output: { total: 2, expired: 0, active: 2 }
51
+ ```
52
+
53
+ ## Features:
54
+ - Secure encryption using AES-256-GCM with salt
55
+ - TTL (Time To Live) support for entries
56
+ - Async operations (set, get, delete, list, search)
57
+ - Batch operations support
58
+ - Advanced search with regex and case-sensitivity options
59
+ - Automatic cleanup of expired entries
60
+ - Database statistics
61
+ - Store data in a JSON file
62
+ - Proper error handling and validation
63
+ - Lazy initialization for better performance
64
+
65
+ ## Security Features:
66
+ - Uses PBKDF2 for key derivation
67
+ - Implements salt for each encryption
68
+ - Uses AES-256-GCM for authenticated encryption
69
+ - Proper IV handling
70
+ - Input validation
71
+
72
+ ## Performance Characteristics
73
+
74
+ The database is designed for simplicity and ease of use, with some performance considerations:
75
+
76
+ ### Memory Usage
77
+ - All data is loaded into memory when the database is initialized
78
+ - Memory usage scales linearly with the number of entries and their size
79
+ - Typical memory usage: ~100-200 bytes per entry (for small values)
80
+
81
+ ### Performance Limits
82
+ - Recommended for up to 100,000 entries with small values
83
+ - Can handle up to 1 million entries, but with increased memory usage and slower operations
84
+ - Not recommended for very large values (>1MB) or millions of entries
85
+
86
+ ### Benchmark
87
+ A benchmark script is included to test performance with large datasets:
88
+
89
+ ```bash
90
+ node benchmark/benchmark.js
91
+ ```
92
+
93
+ The benchmark will:
94
+ - Insert 1 million key-value pairs
95
+ - Measure insertion time and memory usage
96
+ - Verify sample values
97
+ - Clean up the test database
98
+
99
+ ### Performance Tips
100
+ 1. Use batch operations for multiple writes
101
+ 2. Keep values small when possible
102
+ 3. Use TTL to automatically clean up old data
103
+ 4. Consider using a more robust database for:
104
+ - Millions of entries
105
+ - Very large values
106
+ - High-frequency writes
107
+ - Distributed systems
108
+
109
+ ## Error Handling:
110
+ The database will throw errors for:
111
+ - Invalid key types (must be string)
112
+ - Empty keys
113
+ - Undefined values
114
+ - Missing password when encryption is enabled
115
+ - Invalid encryption/decryption operations
116
+
117
+ ## License
118
+ MIT
119
+
@@ -0,0 +1,60 @@
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+ const MinoDB = require('../src/index');
4
+
5
+ const BENCHMARK_DB_PATH = path.join(__dirname, '..', 'db', 'benchmark-db.json');
6
+ const NUM_ROWS = 1_000_000;
7
+ const SAMPLE_KEYS = [0, Math.floor(NUM_ROWS / 2), NUM_ROWS - 1];
8
+
9
+ async function cleanup() {
10
+ try {
11
+ await fs.unlink(BENCHMARK_DB_PATH);
12
+ } catch (err) {
13
+ if (err.code !== 'ENOENT') throw err;
14
+ }
15
+ }
16
+
17
+ async function runBenchmark() {
18
+ console.log(`Benchmark: Inserting ${NUM_ROWS.toLocaleString()} rows...`);
19
+ await cleanup();
20
+ const db = new MinoDB(null, false);
21
+ db.dbPath = BENCHMARK_DB_PATH;
22
+
23
+ const start = Date.now();
24
+ for (let i = 0; i < NUM_ROWS; i++) {
25
+ await db.set(`key${i}`, `value${i}`);
26
+ if (i > 0 && i % 100_000 === 0) {
27
+ console.log(` Inserted ${i.toLocaleString()} rows...`);
28
+ }
29
+ }
30
+ const end = Date.now();
31
+ const mem = process.memoryUsage();
32
+ console.log(`Insert time: ${(end - start) / 1000}s`);
33
+ console.log(`Memory usage: ${(mem.rss / 1024 / 1024).toFixed(2)} MB (RSS)`);
34
+
35
+ // Verify a few values
36
+ let allCorrect = true;
37
+ for (const idx of SAMPLE_KEYS) {
38
+ const key = `key${idx}`;
39
+ const expected = `value${idx}`;
40
+ const actual = await db.get(key);
41
+ if (actual !== expected) {
42
+ console.error(` Value mismatch for ${key}: expected ${expected}, got ${actual}`);
43
+ allCorrect = false;
44
+ }
45
+ }
46
+ if (allCorrect) {
47
+ console.log('Sample value verification: PASSED');
48
+ } else {
49
+ console.log('Sample value verification: FAILED');
50
+ }
51
+
52
+ // Clean up
53
+ await cleanup();
54
+ console.log('Benchmark database cleaned up.');
55
+ }
56
+
57
+ runBenchmark().catch(err => {
58
+ console.error('Benchmark failed:', err);
59
+ process.exit(1);
60
+ });
@@ -0,0 +1,143 @@
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+ const MinoDB = require('../src/index');
4
+
5
+ const BENCHMARK_DB_PATH = path.join(__dirname, '..', 'db', 'benchmark-db.json');
6
+ const NUM_ROWS = 1_000_000;
7
+ const SEARCH_SAMPLES = 1000; // Number of search operations to perform
8
+ const EDIT_SAMPLES = 1000; // Number of edit operations to perform
9
+
10
+ async function runSearchEditBenchmark() {
11
+ console.log('Starting Search and Edit Benchmark...\n');
12
+
13
+ // Initialize database
14
+ const db = new MinoDB(null, false);
15
+ db.dbPath = BENCHMARK_DB_PATH;
16
+
17
+ // Generate test data if it doesn't exist
18
+ if (!await fileExists(BENCHMARK_DB_PATH)) {
19
+ console.log(`Creating test database with ${NUM_ROWS.toLocaleString()} entries...`);
20
+ for (let i = 0; i < NUM_ROWS; i++) {
21
+ await db.set(`key${i}`, `value${i}`);
22
+ if (i > 0 && i % 100_000 === 0) {
23
+ console.log(` Inserted ${i.toLocaleString()} entries...`);
24
+ }
25
+ }
26
+ }
27
+
28
+ // Benchmark Search Operations
29
+ console.log('\nBenchmarking Search Operations...');
30
+ const searchTimes = [];
31
+ const searchPatterns = [
32
+ 'value1', // Common prefix
33
+ '999', // Common suffix
34
+ '500', // Middle value
35
+ 'key1', // Key search
36
+ 'nonexistent' // No matches
37
+ ];
38
+
39
+ for (const pattern of searchPatterns) {
40
+ console.log(`\nSearching for pattern: "${pattern}"`);
41
+
42
+ // Basic search
43
+ const basicStart = Date.now();
44
+ const basicResults = await db.search(pattern);
45
+ const basicTime = Date.now() - basicStart;
46
+ searchTimes.push({ pattern, type: 'basic', time: basicTime, results: Object.keys(basicResults).length });
47
+ console.log(` Basic search: ${basicTime}ms, found ${Object.keys(basicResults).length} results`);
48
+
49
+ // Regex search
50
+ const regexStart = Date.now();
51
+ const regexResults = await db.search(pattern, { regex: true });
52
+ const regexTime = Date.now() - regexStart;
53
+ searchTimes.push({ pattern, type: 'regex', time: regexTime, results: Object.keys(regexResults).length });
54
+ console.log(` Regex search: ${regexTime}ms, found ${Object.keys(regexResults).length} results`);
55
+
56
+ // Case-sensitive search
57
+ const caseStart = Date.now();
58
+ const caseResults = await db.search(pattern.toUpperCase(), { caseSensitive: true });
59
+ const caseTime = Date.now() - caseStart;
60
+ searchTimes.push({ pattern, type: 'case-sensitive', time: caseTime, results: Object.keys(caseResults).length });
61
+ console.log(` Case-sensitive search: ${caseTime}ms, found ${Object.keys(caseResults).length} results`);
62
+ }
63
+
64
+ // Benchmark Edit Operations
65
+ console.log('\nBenchmarking Edit Operations...');
66
+ const editTimes = [];
67
+
68
+ // Random edits
69
+ for (let i = 0; i < EDIT_SAMPLES; i++) {
70
+ const key = `key${Math.floor(Math.random() * NUM_ROWS)}`;
71
+ const newValue = `updated_value_${i}`;
72
+
73
+ const start = Date.now();
74
+ await db.set(key, newValue);
75
+ const time = Date.now() - start;
76
+ editTimes.push(time);
77
+
78
+ if (i % 100 === 0) {
79
+ console.log(` Completed ${i} edits...`);
80
+ }
81
+ }
82
+
83
+ // Batch edits
84
+ console.log('\nBenchmarking Batch Edit Operations...');
85
+ const batchSize = 100;
86
+ const batchTimes = [];
87
+
88
+ for (let i = 0; i < EDIT_SAMPLES / batchSize; i++) {
89
+ const operations = [];
90
+ for (let j = 0; j < batchSize; j++) {
91
+ const key = `key${Math.floor(Math.random() * NUM_ROWS)}`;
92
+ operations.push({
93
+ type: 'set',
94
+ key,
95
+ value: `batch_updated_${i}_${j}`
96
+ });
97
+ }
98
+
99
+ const start = Date.now();
100
+ await db.batch(operations);
101
+ const time = Date.now() - start;
102
+ batchTimes.push(time);
103
+
104
+ console.log(` Completed batch ${i + 1}...`);
105
+ }
106
+
107
+ // Print Results
108
+ console.log('\nBenchmark Results:');
109
+ console.log('\nSearch Operations:');
110
+ searchTimes.forEach(({ pattern, type, time, results }) => {
111
+ console.log(` ${type} search for "${pattern}": ${time}ms, found ${results} results`);
112
+ });
113
+
114
+ console.log('\nEdit Operations:');
115
+ const avgEditTime = editTimes.reduce((a, b) => a + b, 0) / editTimes.length;
116
+ console.log(` Average single edit time: ${avgEditTime.toFixed(2)}ms`);
117
+
118
+ console.log('\nBatch Edit Operations:');
119
+ const avgBatchTime = batchTimes.reduce((a, b) => a + b, 0) / batchTimes.length;
120
+ console.log(` Average batch edit time (${batchSize} operations): ${avgBatchTime.toFixed(2)}ms`);
121
+ console.log(` Average time per operation in batch: ${(avgBatchTime / batchSize).toFixed(2)}ms`);
122
+
123
+ // Memory Usage
124
+ const mem = process.memoryUsage();
125
+ console.log('\nMemory Usage:');
126
+ console.log(` RSS: ${(mem.rss / 1024 / 1024).toFixed(2)} MB`);
127
+ console.log(` Heap Total: ${(mem.heapTotal / 1024 / 1024).toFixed(2)} MB`);
128
+ console.log(` Heap Used: ${(mem.heapUsed / 1024 / 1024).toFixed(2)} MB`);
129
+ }
130
+
131
+ async function fileExists(path) {
132
+ try {
133
+ await fs.access(path);
134
+ return true;
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
139
+
140
+ runSearchEditBenchmark().catch(err => {
141
+ console.error('Benchmark failed:', err);
142
+ process.exit(1);
143
+ });
package/package.json CHANGED
@@ -1,22 +1,38 @@
1
1
  {
2
2
  "name": "milodb",
3
- "version": "1.0.7",
4
- "main": "src/milodb.js",
3
+ "version": "1.1.1",
4
+ "description": "A simple mini database with optional encryption to store key-value pairs",
5
+ "main": "src/index.js",
5
6
  "scripts": {
6
- "test": "echo \"Error: no test specified\" && exit 1"
7
+ "test": "node test/test.js",
8
+ "benchmark": "node benchmark/benchmark.js",
9
+ "benchmark:search": "node benchmark/search_edit_benchmark.js",
10
+ "lint": "eslint src/ test/ benchmark/",
11
+ "clean": "rm -rf db/*.json"
7
12
  },
8
- "keywords": [],
13
+ "keywords": [
14
+ "database",
15
+ "key-value",
16
+ "encryption",
17
+ "storage",
18
+ "json",
19
+ "simple",
20
+ "mini"
21
+ ],
9
22
  "author": "",
10
- "license": "ISC",
11
- "description": "",
12
- "dependencies": {
13
- "commander": "^12.1.0",
14
- "crypto": "^1.0.1",
15
- "fs-extra": "^11.2.0",
16
- "inquirer": "^12.1.0"
23
+ "license": "MIT",
24
+ "engines": {
25
+ "node": ">=14.0.0"
17
26
  },
18
- "bin": {
19
- "milodb": "./src/cli.js"
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/yourusername/milodb.git"
20
30
  },
21
- "preferGlobal": true
31
+ "bugs": {
32
+ "url": "https://github.com/yourusername/milodb/issues"
33
+ },
34
+ "homepage": "https://github.com/yourusername/milodb#readme",
35
+ "devDependencies": {
36
+ "eslint": "^8.0.0"
37
+ }
22
38
  }
package/src/index.js ADDED
@@ -0,0 +1,263 @@
1
+ const fs = require('fs').promises;
2
+ const crypto = require('crypto');
3
+ const path = require('path');
4
+ const zlib = require('zlib');
5
+ const { promisify } = require('util');
6
+
7
+ const gzip = promisify(zlib.gzip);
8
+ const gunzip = promisify(zlib.gunzip);
9
+
10
+ // Helper function for encryption with salt
11
+ function encrypt(text, password) {
12
+ if (!password) throw new Error('Password is required for encryption');
13
+ const salt = crypto.randomBytes(16);
14
+ const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
15
+ const iv = crypto.randomBytes(16);
16
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
17
+
18
+ let encrypted = cipher.update(text, 'utf8', 'hex');
19
+ encrypted += cipher.final('hex');
20
+ const authTag = cipher.getAuthTag();
21
+
22
+ return {
23
+ encrypted,
24
+ salt: salt.toString('hex'),
25
+ iv: iv.toString('hex'),
26
+ authTag: authTag.toString('hex')
27
+ };
28
+ }
29
+
30
+ // Helper function for decryption with salt
31
+ function decrypt(encryptedData, password) {
32
+ if (!password) throw new Error('Password is required for decryption');
33
+ const { encrypted, salt, iv, authTag } = encryptedData;
34
+
35
+ const key = crypto.pbkdf2Sync(password, Buffer.from(salt, 'hex'), 100000, 32, 'sha256');
36
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'hex'));
37
+ decipher.setAuthTag(Buffer.from(authTag, 'hex'));
38
+
39
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
40
+ decrypted += decipher.final('utf8');
41
+ return decrypted;
42
+ }
43
+
44
+ // Helper function to compress data
45
+ async function compress(data) {
46
+ return await gzip(Buffer.from(data));
47
+ }
48
+
49
+ // Helper function to decompress data
50
+ async function decompress(data) {
51
+ return (await gunzip(data)).toString();
52
+ }
53
+
54
+ // Basic mini database class
55
+ class MinoDB {
56
+ constructor(password = null, encryptData = true) {
57
+ this.dbPath = path.join(__dirname, '..', 'db', 'database.json');
58
+ this.password = password;
59
+ this.encryptData = encryptData;
60
+ this.db = {};
61
+ this.initialized = false;
62
+ }
63
+
64
+ // Initialize the database
65
+ async init() {
66
+ if (this.initialized) return;
67
+ await this.loadDatabase();
68
+ this.initialized = true;
69
+ }
70
+
71
+ // Load the database from the file
72
+ async loadDatabase() {
73
+ try {
74
+ const rawData = await fs.readFile(this.dbPath, 'utf8');
75
+ const data = this.encryptData ? decrypt(JSON.parse(rawData), this.password) : rawData;
76
+ this.db = JSON.parse(data);
77
+
78
+ // Clean expired entries
79
+ await this.cleanExpired();
80
+ } catch (err) {
81
+ if (err.code !== 'ENOENT') throw err;
82
+ this.db = {};
83
+ }
84
+ }
85
+
86
+ // Save the database to the file
87
+ async saveDatabase() {
88
+ const data = JSON.stringify(this.db, null, 2);
89
+ let dataToSave;
90
+
91
+ if (this.encryptData) {
92
+ dataToSave = JSON.stringify(encrypt(data, this.password));
93
+ } else {
94
+ dataToSave = data;
95
+ }
96
+
97
+ await fs.writeFile(this.dbPath, dataToSave, 'utf8');
98
+ }
99
+
100
+ // Validate key and value
101
+ validateKeyValue(key, value) {
102
+ if (typeof key !== 'string') throw new Error('Key must be a string');
103
+ if (key.length === 0) throw new Error('Key cannot be empty');
104
+ if (value === undefined) throw new Error('Value cannot be undefined');
105
+ }
106
+
107
+ // Add or update an entry (asynchronous)
108
+ async set(key, value, ttl = null) {
109
+ await this.init();
110
+ this.validateKeyValue(key, value);
111
+
112
+ const entry = {
113
+ value,
114
+ createdAt: Date.now(),
115
+ expiresAt: ttl ? Date.now() + ttl : null
116
+ };
117
+
118
+ this.db[key] = entry;
119
+ await this.saveDatabase();
120
+ }
121
+
122
+ // Get an entry by key (asynchronous)
123
+ async get(key) {
124
+ await this.init();
125
+ const entry = this.db[key];
126
+
127
+ if (!entry) return null;
128
+ if (entry.expiresAt && entry.expiresAt < Date.now()) {
129
+ delete this.db[key];
130
+ await this.saveDatabase();
131
+ return null;
132
+ }
133
+
134
+ return entry.value;
135
+ }
136
+
137
+ // Delete an entry by key (asynchronous)
138
+ async delete(key) {
139
+ await this.init();
140
+ if (this.db[key]) {
141
+ delete this.db[key];
142
+ await this.saveDatabase();
143
+ return true;
144
+ }
145
+ return false;
146
+ }
147
+
148
+ // List all entries (asynchronous)
149
+ async list() {
150
+ await this.init();
151
+ await this.cleanExpired();
152
+ const result = {};
153
+ for (const [key, entry] of Object.entries(this.db)) {
154
+ result[key] = entry.value;
155
+ }
156
+ return result;
157
+ }
158
+
159
+ // Clear the database (asynchronous)
160
+ async clear() {
161
+ await this.init();
162
+ this.db = {};
163
+ await this.saveDatabase();
164
+ return 'Database cleared.';
165
+ }
166
+
167
+ // Search for values (asynchronous)
168
+ async search(query, options = {}) {
169
+ await this.init();
170
+ await this.cleanExpired();
171
+
172
+ const {
173
+ regex = false,
174
+ caseSensitive = false,
175
+ limit = 0
176
+ } = options;
177
+
178
+ const results = {};
179
+ let count = 0;
180
+
181
+ for (const [key, entry] of Object.entries(this.db)) {
182
+ if (limit > 0 && count >= limit) break;
183
+
184
+ const value = entry.value;
185
+ const searchStr = JSON.stringify(value);
186
+ let matches = false;
187
+
188
+ if (regex) {
189
+ const flags = caseSensitive ? '' : 'i';
190
+ matches = new RegExp(query, flags).test(searchStr);
191
+ } else {
192
+ const searchValue = caseSensitive ? query : query.toLowerCase();
193
+ const compareValue = caseSensitive ? searchStr : searchStr.toLowerCase();
194
+ matches = compareValue.includes(searchValue);
195
+ }
196
+
197
+ if (matches) {
198
+ results[key] = value;
199
+ count++;
200
+ }
201
+ }
202
+
203
+ return results;
204
+ }
205
+
206
+ // Clean expired entries
207
+ async cleanExpired() {
208
+ const now = Date.now();
209
+ let cleaned = false;
210
+
211
+ for (const [key, entry] of Object.entries(this.db)) {
212
+ if (entry.expiresAt && entry.expiresAt < now) {
213
+ delete this.db[key];
214
+ cleaned = true;
215
+ }
216
+ }
217
+
218
+ if (cleaned) {
219
+ await this.saveDatabase();
220
+ }
221
+ }
222
+
223
+ // Batch operations
224
+ async batch(operations) {
225
+ await this.init();
226
+ const results = [];
227
+
228
+ for (const op of operations) {
229
+ switch (op.type) {
230
+ case 'set':
231
+ await this.set(op.key, op.value, op.ttl);
232
+ results.push({ success: true });
233
+ break;
234
+ case 'delete':
235
+ results.push({ success: await this.delete(op.key) });
236
+ break;
237
+ default:
238
+ results.push({ success: false, error: 'Invalid operation type' });
239
+ }
240
+ }
241
+
242
+ return results;
243
+ }
244
+
245
+ // Get database statistics
246
+ async stats() {
247
+ await this.init();
248
+ await this.cleanExpired();
249
+
250
+ const total = Object.keys(this.db).length;
251
+ const expired = Object.values(this.db).filter(entry =>
252
+ entry.expiresAt && entry.expiresAt < Date.now()
253
+ ).length;
254
+
255
+ return {
256
+ total,
257
+ expired,
258
+ active: total - expired
259
+ };
260
+ }
261
+ }
262
+
263
+ module.exports = MinoDB;
package/test/test.js ADDED
@@ -0,0 +1,159 @@
1
+ const assert = require('assert');
2
+ const fs = require('fs').promises;
3
+ const path = require('path');
4
+ const MinoDB = require('../src/index');
5
+
6
+ // Helper function to clean up test database
7
+ async function cleanup() {
8
+ try {
9
+ await fs.unlink(path.join(__dirname, '..', 'db', 'database.json'));
10
+ } catch (err) {
11
+ if (err.code !== 'ENOENT') throw err;
12
+ }
13
+ }
14
+
15
+ // Helper function to delay execution
16
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
17
+
18
+ async function runTests() {
19
+ console.log('Starting tests...\n');
20
+
21
+ // Test 1: Basic Operations
22
+ console.log('Test 1: Basic Operations');
23
+ const db = new MinoDB('test-password', true);
24
+
25
+ // Test set and get
26
+ await db.set('test-key', 'test-value');
27
+ const value = await db.get('test-key');
28
+ assert.strictEqual(value, 'test-value', 'Basic set/get failed');
29
+ console.log('✓ Basic set/get passed');
30
+
31
+ // Test delete
32
+ await db.delete('test-key');
33
+ const deletedValue = await db.get('test-key');
34
+ assert.strictEqual(deletedValue, null, 'Delete operation failed');
35
+ console.log('✓ Delete operation passed');
36
+
37
+ // Test 2: TTL Support
38
+ console.log('\nTest 2: TTL Support');
39
+ await db.set('temp-key', 'temp-value', 1000); // 1 second TTL
40
+ const tempValue = await db.get('temp-key');
41
+ assert.strictEqual(tempValue, 'temp-value', 'TTL set failed');
42
+ console.log('✓ TTL set passed');
43
+
44
+ await delay(1100); // Wait for TTL to expire
45
+ const expiredValue = await db.get('temp-key');
46
+ assert.strictEqual(expiredValue, null, 'TTL expiration failed');
47
+ console.log('✓ TTL expiration passed');
48
+
49
+ // Test 3: Batch Operations
50
+ console.log('\nTest 3: Batch Operations');
51
+ const batchResults = await db.batch([
52
+ { type: 'set', key: 'batch1', value: 'value1' },
53
+ { type: 'set', key: 'batch2', value: 'value2', ttl: 1000 },
54
+ { type: 'delete', key: 'batch1' }
55
+ ]);
56
+
57
+ assert.strictEqual(batchResults.length, 3, 'Batch operation count mismatch');
58
+ assert.strictEqual(batchResults[0].success, true, 'Batch set failed');
59
+ assert.strictEqual(batchResults[1].success, true, 'Batch set with TTL failed');
60
+ assert.strictEqual(batchResults[2].success, true, 'Batch delete failed');
61
+ console.log('✓ Batch operations passed');
62
+
63
+ // Test 4: Search Operations
64
+ console.log('\nTest 4: Search Operations');
65
+ await db.set('search1', 'hello world');
66
+ await db.set('search2', 'hello universe');
67
+ await db.set('search3', 'different text');
68
+
69
+ // Test basic search
70
+ const searchResults = await db.search('hello');
71
+ assert.strictEqual(Object.keys(searchResults).length, 2, 'Basic search failed');
72
+ console.log('✓ Basic search passed');
73
+
74
+ // Test regex search
75
+ const regexResults = await db.search('hello', { regex: true });
76
+ assert.strictEqual(Object.keys(regexResults).length, 2, 'Regex search failed');
77
+ console.log('✓ Regex search passed');
78
+
79
+ // Test case-sensitive search
80
+ const caseResults = await db.search('HELLO', { caseSensitive: true });
81
+ assert.strictEqual(Object.keys(caseResults).length, 0, 'Case-sensitive search failed');
82
+ console.log('✓ Case-sensitive search passed');
83
+
84
+ // Test 5: Database Statistics
85
+ console.log('\nTest 5: Database Statistics');
86
+ const stats = await db.stats();
87
+ assert.strictEqual(typeof stats.total, 'number', 'Stats total type mismatch');
88
+ assert.strictEqual(typeof stats.expired, 'number', 'Stats expired type mismatch');
89
+ assert.strictEqual(typeof stats.active, 'number', 'Stats active type mismatch');
90
+ console.log('✓ Database statistics passed');
91
+
92
+ // Test 6: Error Handling
93
+ console.log('\nTest 6: Error Handling');
94
+
95
+ // Test invalid key type
96
+ try {
97
+ await db.set(123, 'value');
98
+ assert.fail('Should have thrown error for invalid key type');
99
+ } catch (err) {
100
+ assert.strictEqual(err.message, 'Key must be a string', 'Invalid key type error message mismatch');
101
+ }
102
+ console.log('✓ Invalid key type handling passed');
103
+
104
+ // Test empty key
105
+ try {
106
+ await db.set('', 'value');
107
+ assert.fail('Should have thrown error for empty key');
108
+ } catch (err) {
109
+ assert.strictEqual(err.message, 'Key cannot be empty', 'Empty key error message mismatch');
110
+ }
111
+ console.log('✓ Empty key handling passed');
112
+
113
+ // Test undefined value
114
+ try {
115
+ await db.set('key', undefined);
116
+ assert.fail('Should have thrown error for undefined value');
117
+ } catch (err) {
118
+ assert.strictEqual(err.message, 'Value cannot be undefined', 'Undefined value error message mismatch');
119
+ }
120
+ console.log('✓ Undefined value handling passed');
121
+
122
+ // Test 7: Encryption
123
+ console.log('\nTest 7: Encryption');
124
+ const encDbPath = path.join(__dirname, '..', 'db', 'encryption-test.json');
125
+ // Clean up before test
126
+ try { await fs.unlink(encDbPath); } catch (e) {}
127
+ const EncryptedMinoDB = require('../src/index');
128
+ const encryptedDb = new EncryptedMinoDB('encryption-password', true);
129
+ encryptedDb.dbPath = encDbPath;
130
+ await encryptedDb.set('encrypted-key', 'sensitive-data');
131
+
132
+ // Try to read with wrong password
133
+ const wrongDb = new EncryptedMinoDB('wrong-password', true);
134
+ wrongDb.dbPath = encDbPath;
135
+ try {
136
+ await wrongDb.get('encrypted-key');
137
+ assert.fail('Should have thrown error for wrong password');
138
+ } catch (err) {
139
+ assert(
140
+ err.message.includes('decrypt') ||
141
+ err.message.includes('unable to authenticate') ||
142
+ err.message.includes('Unsupported state'),
143
+ 'Wrong password error message mismatch'
144
+ );
145
+ }
146
+ console.log('✓ Encryption handling passed');
147
+ // Clean up after test
148
+ try { await fs.unlink(encDbPath); } catch (e) {}
149
+
150
+ // Cleanup
151
+ await cleanup();
152
+ console.log('\nAll tests passed successfully! 🎉');
153
+ }
154
+
155
+ // Run the tests
156
+ runTests().catch(err => {
157
+ console.error('Test failed:', err);
158
+ process.exit(1);
159
+ });
package/src/cli.js DELETED
@@ -1,86 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const path = require('path');
4
- const fs = require('fs-extra');
5
- const inquirer = require('inquirer');
6
- const MiloDB = require(path.join(__dirname, 'milodb')); // Ensure correct path
7
-
8
- // Function to initialize the database
9
- async function initDatabase() {
10
- const dbPath = path.join(process.cwd(), 'db'); // Set the path where the database will be stored
11
-
12
- try {
13
- // Ask the user if they want to encrypt the data
14
- const { encryptData } = await inquirer.prompt([
15
- {
16
- type: 'confirm',
17
- name: 'encryptData',
18
- message: 'Do you want to encrypt the data?',
19
- default: false
20
- }
21
- ]);
22
-
23
- // If encryption is chosen, ask for an email to use as a key
24
- let email = '';
25
- if (encryptData) {
26
- const emailAnswer = await inquirer.prompt([
27
- {
28
- type: 'input',
29
- name: 'email',
30
- message: 'Enter your email to use for encryption:',
31
- validate: (input) => input.includes('@') ? true : 'Please enter a valid email address'
32
- }
33
- ]);
34
- email = emailAnswer.email;
35
- }
36
-
37
- // Ask if they want to create the "users" table
38
- const { createUsersTable } = await inquirer.prompt([
39
- {
40
- type: 'confirm',
41
- name: 'createUsersTable',
42
- message: 'Do you want to create a default "users" table?',
43
- default: true
44
- }
45
- ]);
46
-
47
- // Create the configuration file with encryption settings
48
- const config = {
49
- encryptData,
50
- email,
51
- };
52
-
53
- await fs.writeFile(path.join(process.cwd(), 'milo.config.js'), `module.exports = ${JSON.stringify(config, null, 2)};`, 'utf-8');
54
-
55
- console.log('Configuration saved in milo.config.js');
56
-
57
- // Create a new instance of MiloDB
58
- const db = new MiloDB(dbPath, encryptData); // Pass encryption setting to MiloDB
59
-
60
- // Ensure the database folder exists
61
- await fs.ensureDir(dbPath);
62
-
63
- console.log(`Database initialized at: ${dbPath}`);
64
-
65
- // Optionally create the users table
66
- if (createUsersTable) {
67
- await db.createTable('users');
68
- console.log('Default "users" table created.');
69
- }
70
-
71
- } catch (error) {
72
- console.error('Error initializing database:', error.message);
73
- process.exit(1);
74
- }
75
- }
76
-
77
- // Parse command line arguments
78
- const args = process.argv.slice(2);
79
-
80
- // Handle the init command
81
- if (args[0] === 'init') {
82
- initDatabase();
83
- } else {
84
- console.log('Unknown command. Please use "milodb init" to initialize the database.');
85
- process.exit(1);
86
- }
package/src/encrypt.js DELETED
@@ -1,24 +0,0 @@
1
- // src/encrypt.js
2
- const crypto = require('crypto');
3
- const secretKey = 'your-secret-key'; // Use a strong key for real-world applications
4
- const algorithm = 'aes-256-cbc';
5
-
6
- // Encrypt data
7
- function encrypt(data) {
8
- const iv = crypto.randomBytes(16);
9
- const cipher = crypto.createCipheriv(algorithm, Buffer.from(secretKey), iv);
10
- let encryptedData = cipher.update(data, 'utf8', 'hex');
11
- encryptedData += cipher.final('hex');
12
- return `${iv.toString('hex')}:${encryptedData}`;
13
- }
14
-
15
- // Decrypt data
16
- function decrypt(encryptedData) {
17
- const [iv, data] = encryptedData.split(':');
18
- const decipher = crypto.createDecipheriv(algorithm, Buffer.from(secretKey), Buffer.from(iv, 'hex'));
19
- let decryptedData = decipher.update(data, 'hex', 'utf8');
20
- decryptedData += decipher.final('utf8');
21
- return decryptedData;
22
- }
23
-
24
- module.exports = { encrypt, decrypt };
package/src/milodb.js DELETED
@@ -1,102 +0,0 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const { encrypt, decrypt } = require('./encrypt');
4
-
5
- class MiloDB {
6
- constructor(dbPath = path.join(process.cwd(), 'db'), encryptData = false) {
7
- // Use the root folder for dbPath if not provided
8
- this.dbPath = dbPath;
9
- this.encryptData = encryptData;
10
- fs.ensureDirSync(this.dbPath); // Ensure that the DB path exists
11
- }
12
-
13
- // Create a new table (i.e., a new JSON file)
14
- async createTable(tableName) {
15
- const tablePath = path.join(this.dbPath, `${tableName}.json`);
16
- if (fs.existsSync(tablePath)) {
17
- throw new Error(`Table ${tableName} already exists.`);
18
- }
19
- await fs.writeJson(tablePath, [], { spaces: 2 });
20
- console.log(`Table ${tableName} created successfully.`);
21
- }
22
-
23
- // Add a record to a table
24
- async addRecord(tableName, record) {
25
- const tablePath = path.join(this.dbPath, `${tableName}.json`);
26
- if (!fs.existsSync(tablePath)) {
27
- throw new Error(`Table ${tableName} does not exist.`);
28
- }
29
- const table = await fs.readJson(tablePath);
30
-
31
- // Apply encryption if needed
32
- if (this.encryptData) {
33
- record = encrypt(JSON.stringify(record));
34
- }
35
-
36
- table.push(record);
37
- await fs.writeJson(tablePath, table, { spaces: 2 });
38
- console.log('Record added successfully.');
39
- }
40
-
41
- // Edit a record in a table
42
- async editRecord(tableName, recordId, updatedRecord) {
43
- const tablePath = path.join(this.dbPath, `${tableName}.json`);
44
- if (!fs.existsSync(tablePath)) {
45
- throw new Error(`Table ${tableName} does not exist.`);
46
- }
47
- const table = await fs.readJson(tablePath);
48
- const index = table.findIndex((rec) => this.decryptIfNeeded(rec).id === recordId);
49
- if (index === -1) {
50
- throw new Error(`Record with ID ${recordId} not found.`);
51
- }
52
-
53
- // Apply encryption if needed
54
- table[index] = this.encryptIfNeeded(updatedRecord);
55
- await fs.writeJson(tablePath, table, { spaces: 2 });
56
- console.log('Record updated successfully.');
57
- }
58
-
59
- // Delete a record from a table
60
- async deleteRecord(tableName, recordId) {
61
- const tablePath = path.join(this.dbPath, `${tableName}.json`);
62
- if (!fs.existsSync(tablePath)) {
63
- throw new Error(`Table ${tableName} does not exist.`);
64
- }
65
- const table = await fs.readJson(tablePath);
66
- const index = table.findIndex((rec) => this.decryptIfNeeded(rec).id === recordId);
67
- if (index === -1) {
68
- throw new Error(`Record with ID ${recordId} not found.`);
69
- }
70
- table.splice(index, 1);
71
- await fs.writeJson(tablePath, table, { spaces: 2 });
72
- console.log('Record deleted successfully.');
73
- }
74
-
75
- // Get all records from a table
76
- async getRecords(tableName) {
77
- const tablePath = path.join(this.dbPath, `${tableName}.json`);
78
- if (!fs.existsSync(tablePath)) {
79
- throw new Error(`Table ${tableName} does not exist.`);
80
- }
81
- const table = await fs.readJson(tablePath);
82
- return table.map((record) => this.decryptIfNeeded(record));
83
- }
84
-
85
- // Helper function to decrypt a record if encryption is enabled
86
- decryptIfNeeded(record) {
87
- if (this.encryptData) {
88
- return JSON.parse(decrypt(record));
89
- }
90
- return JSON.parse(record);
91
- }
92
-
93
- // Helper function to encrypt a record if encryption is enabled
94
- encryptIfNeeded(record) {
95
- if (this.encryptData) {
96
- return encrypt(JSON.stringify(record));
97
- }
98
- return JSON.stringify(record);
99
- }
100
- }
101
-
102
- module.exports = MiloDB;