lmcs-db 1.0.4 → 2.0.0
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/README.md +241 -85
- package/dist/core/collection.d.ts +33 -0
- package/dist/core/collection.js +287 -0
- package/dist/core/database.d.ts +35 -0
- package/dist/core/database.js +165 -0
- package/dist/core/indexer.d.ts +20 -0
- package/dist/core/indexer.js +89 -0
- package/dist/core/transaction-context.d.ts +13 -0
- package/dist/core/transaction-context.js +48 -0
- package/dist/core/transaction.d.ts +25 -0
- package/dist/core/transaction.js +122 -0
- package/dist/crypto/key-derivation.d.ts +0 -0
- package/dist/crypto/key-derivation.js +1 -0
- package/dist/crypto/manager.d.ts +22 -0
- package/dist/crypto/manager.js +76 -0
- package/dist/crypto/vault.d.ts +18 -0
- package/dist/crypto/vault.js +44 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +12 -9
- package/dist/persistence/AsyncWriteWorker.js +11 -7
- package/dist/storage/aol.d.ts +26 -0
- package/dist/storage/aol.js +166 -0
- package/dist/storage/base.d.ts +36 -0
- package/dist/storage/base.js +13 -0
- package/dist/storage/binary.d.ts +21 -0
- package/dist/storage/binary.js +124 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.js +13 -0
- package/dist/storage/json.d.ts +18 -0
- package/dist/storage/json.js +153 -0
- package/dist/storage/memory.d.ts +14 -0
- package/dist/storage/memory.js +42 -0
- package/dist/utils/checksum.d.ts +0 -0
- package/dist/utils/checksum.js +1 -0
- package/dist/utils/errors.d.ts +16 -0
- package/dist/utils/errors.js +37 -0
- package/dist/utils/lock.d.ts +9 -0
- package/dist/utils/lock.js +75 -0
- package/package.json +11 -5
package/README.md
CHANGED
|
@@ -1,125 +1,281 @@
|
|
|
1
|
-
#
|
|
1
|
+
# LMCS-DB v2.0
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://typescriptlang.org)
|
|
4
|
+
[](https://nodejs.org)
|
|
5
|
+
[](LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
**Lightweight Modular Collection Storage** — A high-performance, file-based NoSQL database for Node.js with multiple storage engines, ACID transactions, and military-grade encryption.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## ✨ Features
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- 🧾 Formato binário com cabeçalho, tamanho e CRC32 (container estilo SQLite)
|
|
17
|
-
- 🚀 Auto-criação de diretórios ao salvar
|
|
18
|
-
|
|
19
|
-
---
|
|
11
|
+
- **🗄️ Multiple Storage Engines**: Memory, JSON, Binary, and Append-Only Log (AOL)
|
|
12
|
+
- **🔐 Built-in Encryption**: AES-256-GCM with PBKDF2 key derivation
|
|
13
|
+
- **🔄 ACID Transactions**: Multi-document transactions with rollback support
|
|
14
|
+
- **⚡ High Performance**: In-memory indexes, streaming queries, and batch operations
|
|
15
|
+
- **🔍 Advanced Queries**: MongoDB-like operators ($gt, $lt, $or, $and, $in)
|
|
16
|
+
- **📦 Zero Dependencies**: Lightweight with minimal footprint
|
|
17
|
+
- **🧪 Full TypeScript**: Type-safe collections with IntelliSense support
|
|
20
18
|
|
|
19
|
+
## 🚀 Quick Start
|
|
21
20
|
|
|
22
21
|
```bash
|
|
23
22
|
npm install lmcs-db
|
|
24
|
-
|
|
25
|
-
yarn add lmcs-db
|
|
23
|
+
```
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
import {
|
|
25
|
+
```typescript
|
|
26
|
+
import { Database, StorageType } from "lmcs-db";
|
|
29
27
|
|
|
30
28
|
interface User {
|
|
31
|
-
_id
|
|
29
|
+
_id?: string;
|
|
32
30
|
name: string;
|
|
33
31
|
email: string;
|
|
34
32
|
age: number;
|
|
35
|
-
active: boolean;
|
|
36
33
|
}
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
// Create database
|
|
36
|
+
const db = await Database.create({
|
|
37
|
+
storageType: StorageType.Binary,
|
|
38
|
+
databaseName: "myapp",
|
|
39
|
+
encryptionKey: "your-secret-key-32-chars!!", // Optional
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const users = db.collection<User>("users");
|
|
43
|
+
|
|
44
|
+
// Insert
|
|
45
|
+
await users.insert({ name: "Alice", email: "alice@test.com", age: 30 });
|
|
46
|
+
|
|
47
|
+
// Query
|
|
48
|
+
const adults = await users.findAll({
|
|
49
|
+
filter: { age: { $gte: 18 } },
|
|
50
|
+
sort: { name: 1 },
|
|
51
|
+
limit: 10,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Transaction
|
|
55
|
+
await db.transaction(async (trx) => {
|
|
56
|
+
await trx.insert("users", { name: "Bob", age: 25 });
|
|
57
|
+
await trx.update("users", "alice-id", { age: 31 });
|
|
58
|
+
});
|
|
59
|
+
```
|
|
45
60
|
|
|
46
|
-
|
|
61
|
+
💾 Storage Engines
|
|
62
|
+
| Engine | Persistence | Speed | Use Case | Compression |
|
|
63
|
+
| ---------- | ----------- | ------------- | ------------------------------------- | -------------- |
|
|
64
|
+
| **Memory** | ❌ Volatile | ⚡ Ultra-fast | Cache, testing, temporary data | N/A |
|
|
65
|
+
| **JSON** | ✅ File | 🐢 Moderate | Config files, small datasets (<10MB) | None (text) |
|
|
66
|
+
| **Binary** | ✅ File | 🚀 Fast | General purpose, medium datasets | Binary packing |
|
|
67
|
+
| **AOL** | ✅ File | ⚡ Fast writes | Logs, event sourcing, high throughput | Compaction |
|
|
47
68
|
|
|
48
|
-
|
|
49
|
-
name: 'Alice',
|
|
50
|
-
email: 'alice@example.com',
|
|
51
|
-
age: 30,
|
|
52
|
-
active: true
|
|
53
|
-
});
|
|
69
|
+
Engine Details
|
|
54
70
|
|
|
55
|
-
|
|
56
|
-
filter: { active: true }
|
|
57
|
-
});
|
|
71
|
+
Memory Storage
|
|
58
72
|
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
```typescript
|
|
74
|
+
const db = await createDatabase({
|
|
75
|
+
storageType: "memory",
|
|
76
|
+
databaseName: "cache",
|
|
77
|
+
});
|
|
78
|
+
// Data lost on process exit. Fastest option.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
JSON Storage
|
|
61
82
|
|
|
62
|
-
|
|
83
|
+
```typescript
|
|
84
|
+
const db = await createDatabase({
|
|
85
|
+
storageType: "json",
|
|
86
|
+
databaseName: "config",
|
|
87
|
+
});
|
|
88
|
+
// Human-readable, but slower than binary.
|
|
63
89
|
```
|
|
64
90
|
|
|
91
|
+
Binary Storage
|
|
65
92
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
93
|
+
```typescript
|
|
94
|
+
const db = await createDatabase({
|
|
95
|
+
storageType: "binary",
|
|
96
|
+
databaseName: "data",
|
|
97
|
+
encryptionKey: "secret", // Optional encryption
|
|
98
|
+
});
|
|
99
|
+
// Compact binary format with CRC32 checksums
|
|
100
|
+
```
|
|
69
101
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
102
|
+
AOL (Append-Only Log)
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const db = await Database.create({
|
|
106
|
+
storageType: StorageType.AOL,
|
|
107
|
+
databaseName: "events",
|
|
108
|
+
bufferSize: 1000, // Buffer before fsync
|
|
109
|
+
compactionInterval: 60000, // Automatic cleanup every 60s
|
|
110
|
+
});
|
|
111
|
+
// O(1) writes, perfect for event sourcing
|
|
112
|
+
```
|
|
76
113
|
|
|
77
|
-
|
|
114
|
+
🔍 Query API
|
|
78
115
|
|
|
79
|
-
|
|
116
|
+
Basic Queries
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// Find one
|
|
120
|
+
const user = await users.findOne({ email: "alice@test.com" });
|
|
121
|
+
|
|
122
|
+
// Find all
|
|
123
|
+
const all = await users.findAll();
|
|
124
|
+
|
|
125
|
+
// Count
|
|
126
|
+
const total = await users.count();
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Advanced Filtering
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// Comparison operators
|
|
133
|
+
const adults = await users.findAll({ filter: { age: { $gte: 18 } } });
|
|
134
|
+
const rich = await users.findAll({ filter: { salary: { $gt: 100000 } } });
|
|
135
|
+
|
|
136
|
+
// Logical operators
|
|
137
|
+
const result = await users.findAll({
|
|
138
|
+
filter: {
|
|
139
|
+
$or: [{ age: { $lt: 18 } }, { vip: true }],
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Array operators (if field is array)
|
|
144
|
+
const tagged = await posts.findAll({
|
|
145
|
+
filter: { tags: { $in: ["typescript", "nodejs"] } },
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Sorting and Pagination
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
const page = await users.findAll({
|
|
153
|
+
filter: { active: true },
|
|
154
|
+
sort: { createdAt: -1 }, // -1 = descending, 1 = ascending
|
|
155
|
+
skip: 20, // Offset
|
|
156
|
+
limit: 10, // Page size
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Streaming (Memory Efficient)
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// Process millions of records without loading into memory
|
|
164
|
+
const stream = logs.findStream({ filter: { level: "error" } });
|
|
165
|
+
|
|
166
|
+
for await (const error of stream) {
|
|
167
|
+
await sendAlert(error);
|
|
80
168
|
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
🔄 Transactions
|
|
172
|
+
ACID transactions ensure data consistency across multiple operations:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
await db.transaction(async (trx) => {
|
|
176
|
+
// All operations succeed or all rollback
|
|
177
|
+
const order = await trx.insert("orders", { total: 100, status: "pending" });
|
|
178
|
+
await trx.insert("order_items", { orderId: order._id, product: "Laptop" });
|
|
179
|
+
await trx.update("inventory", "laptop-123", { stock: { $dec: 1 } });
|
|
81
180
|
|
|
82
|
-
|
|
181
|
+
if (somethingWrong) {
|
|
182
|
+
throw new Error("Rollback everything");
|
|
183
|
+
}
|
|
184
|
+
});
|
|
83
185
|
```
|
|
84
186
|
|
|
85
|
-
|
|
86
|
-
|
|
187
|
+
🔐 Security
|
|
188
|
+
Encryption
|
|
189
|
+
Algorithm: AES-256-GCM
|
|
190
|
+
Key Derivation: PBKDF2 with 100,000 iterations
|
|
191
|
+
Unique IV per encryption operation
|
|
192
|
+
Authentication tag prevents tampering
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const db = await Database.create({
|
|
196
|
+
storageType: StorageType.Binary,
|
|
197
|
+
databaseName: "secrets",
|
|
198
|
+
encryptionKey: process.env.DB_KEY, // Load from secure source
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// All data transparently encrypted on disk
|
|
202
|
+
await secrets.insert({ password: "super-secret" });
|
|
203
|
+
```
|
|
87
204
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
- `databaseName`: string — Nome do arquivo base do banco
|
|
91
|
-
- `encryptionKey`: string (opcional) — Chave usada para criptografia AES
|
|
92
|
-
- `customPath`: string (opcional) — Diretório onde será criado o arquivo de armazenamento (criado automaticamente se não existir)
|
|
205
|
+
Indexing
|
|
206
|
+
Create indexes for fast queries:
|
|
93
207
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
- find
|
|
98
|
-
- findAll
|
|
99
|
-
- update
|
|
100
|
-
- delete
|
|
101
|
-
- count
|
|
208
|
+
```typescript
|
|
209
|
+
// Single field
|
|
210
|
+
users.createIndex("email", { unique: true });
|
|
102
211
|
|
|
103
|
-
|
|
212
|
+
// Compound
|
|
213
|
+
orders.createIndex(["userId", "createdAt"]);
|
|
104
214
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
215
|
+
// Sparse (skip null values)
|
|
216
|
+
users.createIndex("phone", { sparse: true });
|
|
217
|
+
```
|
|
218
|
+
📊 Performance Tips
|
|
219
|
+
1. Use Memory storage for unit tests (10x faster)
|
|
220
|
+
2. Batch inserts instead of individual awaits
|
|
221
|
+
3. Create indexes on frequently queried fields
|
|
222
|
+
4. Use streaming for large datasets (>10k records)
|
|
223
|
+
5. Compact AOL periodically to reclaim space
|
|
224
|
+
6. Enable checksums for critical data integrity
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// Batch insert (much faster)
|
|
228
|
+
await Promise.all(
|
|
229
|
+
items.map(item => collection.insert(item))
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Compact AOL storage
|
|
233
|
+
await db.compact();
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
🧪 Testing
|
|
237
|
+
```bash
|
|
238
|
+
# Run all tests
|
|
239
|
+
npm test
|
|
109
240
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Se um banco for carregado com uma chave incorreta, ele será reiniciado como vazio, com um aviso exibido no console.
|
|
241
|
+
# Run specific suite
|
|
242
|
+
npm test -- storage.test.ts
|
|
113
243
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
244
|
+
# With coverage
|
|
245
|
+
npm run test:coverage
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
📁 Project Structure
|
|
249
|
+
```bash
|
|
250
|
+
data/
|
|
251
|
+
├── myapp.bin # Binary storage file
|
|
252
|
+
├── myapp.json # JSON storage file
|
|
253
|
+
└── myapp.aol # Append-only log
|
|
254
|
+
|
|
255
|
+
src/
|
|
256
|
+
├── core/
|
|
257
|
+
│ ├── database.ts # Main database class
|
|
258
|
+
│ ├── collection.ts # Collection operations
|
|
259
|
+
│ ├── transaction.ts # ACID transactions
|
|
260
|
+
│ └── indexer.ts # Index management
|
|
261
|
+
├── storage/
|
|
262
|
+
│ ├── base.ts # Storage interface
|
|
263
|
+
│ ├── memory.ts # In-memory storage
|
|
264
|
+
│ ├── json.ts # JSON file storage
|
|
265
|
+
│ ├── binary.ts # Binary storage
|
|
266
|
+
│ └── aol.ts # Append-only log
|
|
267
|
+
└── crypto/
|
|
268
|
+
└── manager.ts # Encryption utilities
|
|
269
|
+
```
|
|
117
270
|
|
|
118
|
-
|
|
119
|
-
|
|
271
|
+
🤝 Contributing
|
|
272
|
+
1. Fork the repository
|
|
273
|
+
2. Create your feature branch (git checkout -b feature/amazing)
|
|
274
|
+
3. Commit changes (git commit -m 'Add amazing feature')
|
|
275
|
+
4. Push to branch (git push origin feature/amazing)
|
|
276
|
+
5. Open a Pull Request
|
|
120
277
|
|
|
121
|
-
📄
|
|
122
|
-
MIT
|
|
278
|
+
📄 License
|
|
279
|
+
[MIT License](LICENSE) - see [LICENSE](LICENSE) file.
|
|
123
280
|
|
|
124
|
-
|
|
125
|
-
Desenvolvido por Leandro A da Silva.
|
|
281
|
+
Made with ❤️ by Leandro A. da Silva
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { BaseStorage, LogEntry } from "../storage";
|
|
2
|
+
export interface QueryOptions {
|
|
3
|
+
filter?: Record<string, any>;
|
|
4
|
+
sort?: Record<string, 1 | -1>;
|
|
5
|
+
limit?: number;
|
|
6
|
+
skip?: number;
|
|
7
|
+
batchSize?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class Collection<T extends Record<string, any>> {
|
|
10
|
+
private name;
|
|
11
|
+
private storage;
|
|
12
|
+
private data;
|
|
13
|
+
private indexes;
|
|
14
|
+
private crypto;
|
|
15
|
+
constructor(name: string, storage: BaseStorage);
|
|
16
|
+
private loadFromStorage;
|
|
17
|
+
applyLogEntry(entry: LogEntry): void;
|
|
18
|
+
private addToIndexes;
|
|
19
|
+
private removeFromIndexes;
|
|
20
|
+
insert(doc: Omit<T, "_id"> & {
|
|
21
|
+
_id?: string;
|
|
22
|
+
}): Promise<T>;
|
|
23
|
+
update(filter: Partial<T>, updates: Partial<T>): Promise<number>;
|
|
24
|
+
delete(filter: Partial<T>): Promise<number>;
|
|
25
|
+
findOne(filter: Partial<T>): Promise<T | null>;
|
|
26
|
+
findAll(options?: QueryOptions): Promise<T[]>;
|
|
27
|
+
findStream(options?: QueryOptions): AsyncGenerator<T>;
|
|
28
|
+
createIndex(field: keyof T): void;
|
|
29
|
+
private matchesFilter;
|
|
30
|
+
private getNestedValue;
|
|
31
|
+
private queryByIndex;
|
|
32
|
+
count(): number;
|
|
33
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Collection = void 0;
|
|
4
|
+
const manager_1 = require("../crypto/manager");
|
|
5
|
+
const uuid_1 = require("uuid");
|
|
6
|
+
class Collection {
|
|
7
|
+
name;
|
|
8
|
+
storage;
|
|
9
|
+
data = new Map();
|
|
10
|
+
indexes = new Map();
|
|
11
|
+
crypto = new manager_1.CryptoManager(); // Instância sem chave para hash apenas
|
|
12
|
+
constructor(name, storage) {
|
|
13
|
+
this.name = name;
|
|
14
|
+
this.storage = storage;
|
|
15
|
+
this.loadFromStorage().catch(console.error);
|
|
16
|
+
}
|
|
17
|
+
async loadFromStorage() {
|
|
18
|
+
try {
|
|
19
|
+
if (!this.storage.readStream) {
|
|
20
|
+
throw new Error("Storage does not support streaming");
|
|
21
|
+
}
|
|
22
|
+
const stream = this.storage.readStream();
|
|
23
|
+
for await (const entry of stream) {
|
|
24
|
+
this.applyLogEntry(entry);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.error(`Failed to load collection ${this.name}:`, err);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
applyLogEntry(entry) {
|
|
32
|
+
if (entry.collection !== this.name)
|
|
33
|
+
return;
|
|
34
|
+
const id = entry.id;
|
|
35
|
+
switch (entry.op) {
|
|
36
|
+
case "INSERT":
|
|
37
|
+
case "UPDATE":
|
|
38
|
+
const existing = this.data.get(id);
|
|
39
|
+
if (existing) {
|
|
40
|
+
this.removeFromIndexes(id, existing);
|
|
41
|
+
}
|
|
42
|
+
this.data.set(id, entry.data);
|
|
43
|
+
this.addToIndexes(id, entry.data);
|
|
44
|
+
break;
|
|
45
|
+
case "DELETE":
|
|
46
|
+
const old = this.data.get(id);
|
|
47
|
+
if (old) {
|
|
48
|
+
this.removeFromIndexes(id, old);
|
|
49
|
+
}
|
|
50
|
+
this.data.delete(id);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
addToIndexes(id, doc) {
|
|
55
|
+
for (const [field, index] of this.indexes) {
|
|
56
|
+
const value = doc[field];
|
|
57
|
+
if (value !== undefined) {
|
|
58
|
+
if (!index.has(value))
|
|
59
|
+
index.set(value, new Set());
|
|
60
|
+
index.get(value).add(id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
removeFromIndexes(id, doc) {
|
|
65
|
+
for (const [field, index] of this.indexes) {
|
|
66
|
+
const value = doc[field];
|
|
67
|
+
if (value !== undefined) {
|
|
68
|
+
index.get(value)?.delete(id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async insert(doc) {
|
|
73
|
+
const id = doc._id || (0, uuid_1.v7)();
|
|
74
|
+
if (this.data.has(id)) {
|
|
75
|
+
throw new Error(`Document with id ${id} already exists`);
|
|
76
|
+
}
|
|
77
|
+
const fullDoc = { ...doc, _id: id };
|
|
78
|
+
await this.storage.append({
|
|
79
|
+
op: "INSERT",
|
|
80
|
+
collection: this.name,
|
|
81
|
+
id,
|
|
82
|
+
data: fullDoc,
|
|
83
|
+
checksum: "",
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
});
|
|
86
|
+
this.data.set(id, fullDoc);
|
|
87
|
+
this.addToIndexes(id, fullDoc);
|
|
88
|
+
return fullDoc;
|
|
89
|
+
}
|
|
90
|
+
async update(filter, updates) {
|
|
91
|
+
let count = 0;
|
|
92
|
+
for (const [id, doc] of this.data.entries()) {
|
|
93
|
+
if (this.matchesFilter(doc, filter)) {
|
|
94
|
+
const newDoc = { ...doc, ...updates, _id: id };
|
|
95
|
+
await this.storage.append({
|
|
96
|
+
op: "UPDATE",
|
|
97
|
+
collection: this.name,
|
|
98
|
+
id,
|
|
99
|
+
data: newDoc,
|
|
100
|
+
checksum: "",
|
|
101
|
+
timestamp: Date.now(),
|
|
102
|
+
});
|
|
103
|
+
this.removeFromIndexes(id, doc);
|
|
104
|
+
this.data.set(id, newDoc);
|
|
105
|
+
this.addToIndexes(id, newDoc);
|
|
106
|
+
count++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return count;
|
|
110
|
+
}
|
|
111
|
+
async delete(filter) {
|
|
112
|
+
let count = 0;
|
|
113
|
+
const toDelete = [];
|
|
114
|
+
for (const [id, doc] of this.data.entries()) {
|
|
115
|
+
if (this.matchesFilter(doc, filter)) {
|
|
116
|
+
toDelete.push(id);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const id of toDelete) {
|
|
120
|
+
const doc = this.data.get(id);
|
|
121
|
+
await this.storage.append({
|
|
122
|
+
op: "DELETE",
|
|
123
|
+
collection: this.name,
|
|
124
|
+
id,
|
|
125
|
+
checksum: "",
|
|
126
|
+
timestamp: Date.now(),
|
|
127
|
+
});
|
|
128
|
+
this.removeFromIndexes(id, doc);
|
|
129
|
+
this.data.delete(id);
|
|
130
|
+
count++;
|
|
131
|
+
}
|
|
132
|
+
return count;
|
|
133
|
+
}
|
|
134
|
+
async findOne(filter) {
|
|
135
|
+
// Tenta usar índice primeiro
|
|
136
|
+
const indexedId = this.queryByIndex(filter);
|
|
137
|
+
if (indexedId) {
|
|
138
|
+
return this.data.get(indexedId) || null;
|
|
139
|
+
}
|
|
140
|
+
for (const doc of this.data.values()) {
|
|
141
|
+
if (this.matchesFilter(doc, filter))
|
|
142
|
+
return doc;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
async findAll(options = {}) {
|
|
147
|
+
let results = Array.from(this.data.values());
|
|
148
|
+
if (options.filter) {
|
|
149
|
+
results = results.filter((d) => this.matchesFilter(d, options.filter));
|
|
150
|
+
}
|
|
151
|
+
if (options.sort) {
|
|
152
|
+
results.sort((a, b) => {
|
|
153
|
+
for (const [field, dir] of Object.entries(options.sort)) {
|
|
154
|
+
const aVal = a[field];
|
|
155
|
+
const bVal = b[field];
|
|
156
|
+
if (aVal < bVal)
|
|
157
|
+
return dir === 1 ? -1 : 1;
|
|
158
|
+
if (aVal > bVal)
|
|
159
|
+
return dir === 1 ? 1 : -1;
|
|
160
|
+
}
|
|
161
|
+
return 0;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (options.skip)
|
|
165
|
+
results = results.slice(options.skip);
|
|
166
|
+
if (options.limit)
|
|
167
|
+
results = results.slice(0, options.limit);
|
|
168
|
+
return results;
|
|
169
|
+
}
|
|
170
|
+
async *findStream(options = {}) {
|
|
171
|
+
if (options.sort) {
|
|
172
|
+
const all = await this.findAll(options);
|
|
173
|
+
for (const doc of all)
|
|
174
|
+
yield doc;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
let count = 0;
|
|
178
|
+
let skipped = 0;
|
|
179
|
+
for (const doc of this.data.values()) {
|
|
180
|
+
if (options.filter && !this.matchesFilter(doc, options.filter))
|
|
181
|
+
continue;
|
|
182
|
+
if (options.skip && skipped < options.skip) {
|
|
183
|
+
skipped++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (options.limit && count >= options.limit)
|
|
187
|
+
break;
|
|
188
|
+
yield doc;
|
|
189
|
+
count++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
createIndex(field) {
|
|
193
|
+
if (this.indexes.has(field))
|
|
194
|
+
return;
|
|
195
|
+
this.indexes.set(field, new Map());
|
|
196
|
+
// Indexa existentes
|
|
197
|
+
for (const [id, doc] of this.data.entries()) {
|
|
198
|
+
this.addToIndexes(id, doc);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
matchesFilter(doc, filter) {
|
|
202
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
203
|
+
if (key === "$or") {
|
|
204
|
+
if (!Array.isArray(value))
|
|
205
|
+
return false;
|
|
206
|
+
if (!value.some((condition) => this.matchesFilter(doc, condition)))
|
|
207
|
+
return false;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (key === "$and") {
|
|
211
|
+
if (!Array.isArray(value))
|
|
212
|
+
return false;
|
|
213
|
+
if (!value.every((condition) => this.matchesFilter(doc, condition)))
|
|
214
|
+
return false;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
// Handle dot notation for nested fields
|
|
218
|
+
const docValue = this.getNestedValue(doc, key);
|
|
219
|
+
if (typeof value === "object" && value !== null) {
|
|
220
|
+
// Handle operators like $gt, $lt, etc.
|
|
221
|
+
for (const [op, opValue] of Object.entries(value)) {
|
|
222
|
+
switch (op) {
|
|
223
|
+
case "$gt":
|
|
224
|
+
if (!(docValue > opValue))
|
|
225
|
+
return false;
|
|
226
|
+
break;
|
|
227
|
+
case "$gte":
|
|
228
|
+
if (!(docValue >= opValue))
|
|
229
|
+
return false;
|
|
230
|
+
break;
|
|
231
|
+
case "$lt":
|
|
232
|
+
if (!(docValue < opValue))
|
|
233
|
+
return false;
|
|
234
|
+
break;
|
|
235
|
+
case "$lte":
|
|
236
|
+
if (!(docValue <= opValue))
|
|
237
|
+
return false;
|
|
238
|
+
break;
|
|
239
|
+
case "$ne":
|
|
240
|
+
if (docValue === opValue)
|
|
241
|
+
return false;
|
|
242
|
+
break;
|
|
243
|
+
case "$in":
|
|
244
|
+
if (!Array.isArray(opValue) || !opValue.includes(docValue))
|
|
245
|
+
return false;
|
|
246
|
+
break;
|
|
247
|
+
case "$nin":
|
|
248
|
+
if (Array.isArray(opValue) && opValue.includes(docValue))
|
|
249
|
+
return false;
|
|
250
|
+
break;
|
|
251
|
+
default:
|
|
252
|
+
// If it's not an operator, treat as equality check for object
|
|
253
|
+
if (JSON.stringify(docValue) !== JSON.stringify(value))
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// Direct equality
|
|
260
|
+
if (docValue !== value)
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
getNestedValue(obj, path) {
|
|
267
|
+
return path.split('.').reduce((o, p) => (o ? o[p] : undefined), obj);
|
|
268
|
+
}
|
|
269
|
+
queryByIndex(filter) {
|
|
270
|
+
const entries = Object.entries(filter);
|
|
271
|
+
if (entries.length !== 1)
|
|
272
|
+
return null;
|
|
273
|
+
const [field, value] = entries[0];
|
|
274
|
+
const index = this.indexes.get(field);
|
|
275
|
+
if (!index)
|
|
276
|
+
return null;
|
|
277
|
+
const ids = index.get(value);
|
|
278
|
+
if (ids && ids.size > 0) {
|
|
279
|
+
return Array.from(ids)[0];
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
count() {
|
|
284
|
+
return this.data.size;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
exports.Collection = Collection;
|