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 +19 -0
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +119 -141
- package/benchmark/benchmark.js +60 -0
- package/benchmark/search_edit_benchmark.js +143 -0
- package/package.json +30 -14
- package/src/index.js +263 -0
- package/test/test.js +159 -0
- package/src/cli.js +0 -86
- package/src/encrypt.js +0 -24
- package/src/milodb.js +0 -102
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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.
|
|
4
|
-
"
|
|
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": "
|
|
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": "
|
|
11
|
-
"
|
|
12
|
-
|
|
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
|
-
"
|
|
19
|
-
"
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/yourusername/milodb.git"
|
|
20
30
|
},
|
|
21
|
-
"
|
|
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;
|