@liorandb/core 1.0.13 → 1.0.14
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/dist/index.d.ts +60 -1
- package/dist/index.js +278 -45
- package/package.json +1 -1
- package/src/core/collection.ts +105 -5
- package/src/core/database.ts +89 -2
- package/src/core/index.ts +140 -0
- package/src/core/query.ts +105 -2
- package/src/ipc/index.ts +19 -3
- package/src/types/index.ts +51 -1
package/src/core/collection.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ClassicLevel } from "classic-level";
|
|
2
|
-
import { matchDocument, applyUpdate } from "./query.js";
|
|
2
|
+
import { matchDocument, applyUpdate, extractIndexQuery } from "./query.js";
|
|
3
3
|
import { v4 as uuid } from "uuid";
|
|
4
4
|
import { encryptData, decryptData } from "../utils/encryption.js";
|
|
5
5
|
import type { ZodSchema } from "zod";
|
|
6
6
|
import { validateSchema } from "../utils/schema.js";
|
|
7
|
+
import { Index } from "./index.js";
|
|
7
8
|
|
|
8
9
|
export interface UpdateOptions {
|
|
9
10
|
upsert?: boolean;
|
|
@@ -14,6 +15,7 @@ export class Collection<T = any> {
|
|
|
14
15
|
db: ClassicLevel<string, string>;
|
|
15
16
|
private queue: Promise<any> = Promise.resolve();
|
|
16
17
|
private schema?: ZodSchema<T>;
|
|
18
|
+
private indexes = new Map<string, Index>();
|
|
17
19
|
|
|
18
20
|
constructor(dir: string, schema?: ZodSchema<T>) {
|
|
19
21
|
this.dir = dir;
|
|
@@ -21,6 +23,18 @@ export class Collection<T = any> {
|
|
|
21
23
|
this.schema = schema;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
/* ---------------------- INDEX MANAGEMENT ---------------------- */
|
|
27
|
+
|
|
28
|
+
registerIndex(index: Index) {
|
|
29
|
+
this.indexes.set(index.field, index);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getIndex(field: string) {
|
|
33
|
+
return this.indexes.get(field);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* -------------------------- CORE -------------------------- */
|
|
37
|
+
|
|
24
38
|
setSchema(schema: ZodSchema<T>) {
|
|
25
39
|
this.schema = schema;
|
|
26
40
|
}
|
|
@@ -35,6 +49,9 @@ export class Collection<T = any> {
|
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
async close(): Promise<void> {
|
|
52
|
+
for (const idx of this.indexes.values()) {
|
|
53
|
+
try { await idx.close(); } catch {}
|
|
54
|
+
}
|
|
38
55
|
try { await this.db.close(); } catch {}
|
|
39
56
|
}
|
|
40
57
|
|
|
@@ -53,6 +70,8 @@ export class Collection<T = any> {
|
|
|
53
70
|
}
|
|
54
71
|
}
|
|
55
72
|
|
|
73
|
+
/* --------------------- PUBLIC API --------------------- */
|
|
74
|
+
|
|
56
75
|
insertOne(doc: T & { _id?: string }) {
|
|
57
76
|
return this._enqueue(() => this._exec("insertOne", [doc]));
|
|
58
77
|
}
|
|
@@ -99,12 +118,23 @@ export class Collection<T = any> {
|
|
|
99
118
|
);
|
|
100
119
|
}
|
|
101
120
|
|
|
121
|
+
/* ------------------ INDEX HOOK ------------------ */
|
|
122
|
+
|
|
123
|
+
private async _updateIndexes(oldDoc: any, newDoc: any) {
|
|
124
|
+
for (const index of this.indexes.values()) {
|
|
125
|
+
await index.update(oldDoc, newDoc);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
102
129
|
/* ---------------- Storage ---------------- */
|
|
103
130
|
|
|
104
131
|
private async _insertOne(doc: any) {
|
|
105
132
|
const _id = doc._id ?? uuid();
|
|
106
133
|
const final = this.validate({ _id, ...doc });
|
|
134
|
+
|
|
107
135
|
await this.db.put(String(_id), encryptData(final));
|
|
136
|
+
await this._updateIndexes(null, final);
|
|
137
|
+
|
|
108
138
|
return final;
|
|
109
139
|
}
|
|
110
140
|
|
|
@@ -115,11 +145,22 @@ export class Collection<T = any> {
|
|
|
115
145
|
for (const d of docs) {
|
|
116
146
|
const _id = d._id ?? uuid();
|
|
117
147
|
const final = this.validate({ _id, ...d });
|
|
118
|
-
|
|
148
|
+
|
|
149
|
+
batch.push({
|
|
150
|
+
type: "put",
|
|
151
|
+
key: String(_id),
|
|
152
|
+
value: encryptData(final)
|
|
153
|
+
});
|
|
154
|
+
|
|
119
155
|
out.push(final);
|
|
120
156
|
}
|
|
121
157
|
|
|
122
158
|
await this.db.batch(batch);
|
|
159
|
+
|
|
160
|
+
for (const doc of out) {
|
|
161
|
+
await this._updateIndexes(null, doc);
|
|
162
|
+
}
|
|
163
|
+
|
|
123
164
|
return out;
|
|
124
165
|
}
|
|
125
166
|
|
|
@@ -131,6 +172,19 @@ export class Collection<T = any> {
|
|
|
131
172
|
} catch { return null; }
|
|
132
173
|
}
|
|
133
174
|
|
|
175
|
+
const iq = extractIndexQuery(query);
|
|
176
|
+
if (iq && this.indexes.has(iq.field)) {
|
|
177
|
+
const ids = await this.indexes.get(iq.field)!.find(iq.value);
|
|
178
|
+
if (!ids.length) return null;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const enc = await this.db.get(ids[0]);
|
|
182
|
+
return enc ? decryptData(enc) : null;
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
134
188
|
for await (const [, enc] of this.db.iterator()) {
|
|
135
189
|
const v = decryptData(enc);
|
|
136
190
|
if (matchDocument(v, query)) return v;
|
|
@@ -142,17 +196,27 @@ export class Collection<T = any> {
|
|
|
142
196
|
private async _updateOne(filter: any, update: any, options: UpdateOptions) {
|
|
143
197
|
for await (const [key, enc] of this.db.iterator()) {
|
|
144
198
|
const value = decryptData(enc);
|
|
199
|
+
|
|
145
200
|
if (matchDocument(value, filter)) {
|
|
146
201
|
const updated = this.validate(applyUpdate(value, update)) as any;
|
|
147
202
|
updated._id = value._id;
|
|
203
|
+
|
|
148
204
|
await this.db.put(key, encryptData(updated));
|
|
205
|
+
await this._updateIndexes(value, updated);
|
|
206
|
+
|
|
149
207
|
return updated;
|
|
150
208
|
}
|
|
151
209
|
}
|
|
152
210
|
|
|
153
211
|
if (options?.upsert) {
|
|
154
|
-
const doc = this.validate({
|
|
212
|
+
const doc = this.validate({
|
|
213
|
+
_id: uuid(),
|
|
214
|
+
...applyUpdate({}, update)
|
|
215
|
+
}) as any;
|
|
216
|
+
|
|
155
217
|
await this.db.put(String(doc._id), encryptData(doc));
|
|
218
|
+
await this._updateIndexes(null, doc);
|
|
219
|
+
|
|
156
220
|
return doc;
|
|
157
221
|
}
|
|
158
222
|
|
|
@@ -164,10 +228,14 @@ export class Collection<T = any> {
|
|
|
164
228
|
|
|
165
229
|
for await (const [key, enc] of this.db.iterator()) {
|
|
166
230
|
const value = decryptData(enc);
|
|
231
|
+
|
|
167
232
|
if (matchDocument(value, filter)) {
|
|
168
233
|
const updated = this.validate(applyUpdate(value, update)) as any;
|
|
169
234
|
updated._id = value._id;
|
|
235
|
+
|
|
170
236
|
await this.db.put(key, encryptData(updated));
|
|
237
|
+
await this._updateIndexes(value, updated);
|
|
238
|
+
|
|
171
239
|
out.push(updated);
|
|
172
240
|
}
|
|
173
241
|
}
|
|
@@ -176,18 +244,38 @@ export class Collection<T = any> {
|
|
|
176
244
|
}
|
|
177
245
|
|
|
178
246
|
private async _find(query: any) {
|
|
247
|
+
const iq = extractIndexQuery(query);
|
|
248
|
+
|
|
249
|
+
if (iq && this.indexes.has(iq.field)) {
|
|
250
|
+
const ids = await this.indexes.get(iq.field)!.find(iq.value);
|
|
251
|
+
const out = [];
|
|
252
|
+
|
|
253
|
+
for (const id of ids) {
|
|
254
|
+
try {
|
|
255
|
+
const enc = await this.db.get(id);
|
|
256
|
+
if (enc) out.push(decryptData(enc));
|
|
257
|
+
} catch {}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
|
|
179
263
|
const out = [];
|
|
180
264
|
for await (const [, enc] of this.db.iterator()) {
|
|
181
265
|
const v = decryptData(enc);
|
|
182
266
|
if (matchDocument(v, query)) out.push(v);
|
|
183
267
|
}
|
|
268
|
+
|
|
184
269
|
return out;
|
|
185
270
|
}
|
|
186
271
|
|
|
187
272
|
private async _deleteOne(filter: any) {
|
|
188
273
|
for await (const [key, enc] of this.db.iterator()) {
|
|
189
|
-
|
|
274
|
+
const value = decryptData(enc);
|
|
275
|
+
|
|
276
|
+
if (matchDocument(value, filter)) {
|
|
190
277
|
await this.db.del(key);
|
|
278
|
+
await this._updateIndexes(value, null);
|
|
191
279
|
return true;
|
|
192
280
|
}
|
|
193
281
|
}
|
|
@@ -196,20 +284,32 @@ export class Collection<T = any> {
|
|
|
196
284
|
|
|
197
285
|
private async _deleteMany(filter: any) {
|
|
198
286
|
let count = 0;
|
|
287
|
+
|
|
199
288
|
for await (const [key, enc] of this.db.iterator()) {
|
|
200
|
-
|
|
289
|
+
const value = decryptData(enc);
|
|
290
|
+
|
|
291
|
+
if (matchDocument(value, filter)) {
|
|
201
292
|
await this.db.del(key);
|
|
293
|
+
await this._updateIndexes(value, null);
|
|
202
294
|
count++;
|
|
203
295
|
}
|
|
204
296
|
}
|
|
297
|
+
|
|
205
298
|
return count;
|
|
206
299
|
}
|
|
207
300
|
|
|
208
301
|
private async _countDocuments(filter: any) {
|
|
209
302
|
let c = 0;
|
|
303
|
+
|
|
304
|
+
const iq = extractIndexQuery(filter);
|
|
305
|
+
if (iq && this.indexes.has(iq.field)) {
|
|
306
|
+
return (await this.indexes.get(iq.field)!.find(iq.value)).length;
|
|
307
|
+
}
|
|
308
|
+
|
|
210
309
|
for await (const [, enc] of this.db.iterator()) {
|
|
211
310
|
if (matchDocument(decryptData(enc), filter)) c++;
|
|
212
311
|
}
|
|
312
|
+
|
|
213
313
|
return c;
|
|
214
314
|
}
|
|
215
315
|
}
|
package/src/core/database.ts
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import { Collection } from "./collection.js";
|
|
4
|
+
import { Index, IndexOptions } from "./index.js";
|
|
4
5
|
import type { LioranManager } from "../LioranManager.js";
|
|
5
6
|
import type { ZodSchema } from "zod";
|
|
6
7
|
|
|
8
|
+
/* ----------------------------- TYPES ----------------------------- */
|
|
9
|
+
|
|
7
10
|
type TXOp = { tx: number; col: string; op: string; args: any[] };
|
|
8
11
|
type TXCommit = { tx: number; commit: true };
|
|
9
12
|
type TXApplied = { tx: number; applied: true };
|
|
10
13
|
type WALEntry = TXOp | TXCommit | TXApplied;
|
|
11
14
|
|
|
15
|
+
type IndexMeta = {
|
|
16
|
+
field: string;
|
|
17
|
+
options: IndexOptions;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type DBMeta = {
|
|
21
|
+
version: number;
|
|
22
|
+
indexes: Record<string, IndexMeta[]>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const META_FILE = "__db_meta.json";
|
|
26
|
+
const META_VERSION = 1;
|
|
27
|
+
|
|
28
|
+
/* ---------------------- TRANSACTION CONTEXT ---------------------- */
|
|
29
|
+
|
|
12
30
|
class DBTransactionContext {
|
|
13
31
|
private ops: TXOp[] = [];
|
|
14
32
|
|
|
@@ -41,12 +59,16 @@ class DBTransactionContext {
|
|
|
41
59
|
}
|
|
42
60
|
}
|
|
43
61
|
|
|
62
|
+
/* ----------------------------- DATABASE ----------------------------- */
|
|
63
|
+
|
|
44
64
|
export class LioranDB {
|
|
45
65
|
basePath: string;
|
|
46
66
|
dbName: string;
|
|
47
67
|
manager: LioranManager;
|
|
48
68
|
collections: Map<string, Collection>;
|
|
49
69
|
private walPath: string;
|
|
70
|
+
private metaPath: string;
|
|
71
|
+
private meta!: DBMeta;
|
|
50
72
|
|
|
51
73
|
private static TX_SEQ = 0;
|
|
52
74
|
|
|
@@ -56,12 +78,36 @@ export class LioranDB {
|
|
|
56
78
|
this.manager = manager;
|
|
57
79
|
this.collections = new Map();
|
|
58
80
|
this.walPath = path.join(basePath, "__tx_wal.log");
|
|
81
|
+
this.metaPath = path.join(basePath, META_FILE);
|
|
59
82
|
|
|
60
83
|
fs.mkdirSync(basePath, { recursive: true });
|
|
61
84
|
|
|
85
|
+
this.loadMeta();
|
|
62
86
|
this.recoverFromWAL().catch(console.error);
|
|
63
87
|
}
|
|
64
88
|
|
|
89
|
+
/* ------------------------- META ------------------------- */
|
|
90
|
+
|
|
91
|
+
private loadMeta() {
|
|
92
|
+
if (!fs.existsSync(this.metaPath)) {
|
|
93
|
+
this.meta = { version: META_VERSION, indexes: {} };
|
|
94
|
+
this.saveMeta();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
this.meta = JSON.parse(fs.readFileSync(this.metaPath, "utf8"));
|
|
100
|
+
} catch {
|
|
101
|
+
throw new Error("Database metadata corrupted");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private saveMeta() {
|
|
106
|
+
fs.writeFileSync(this.metaPath, JSON.stringify(this.meta, null, 2));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ------------------------- WAL ------------------------- */
|
|
110
|
+
|
|
65
111
|
async writeWAL(entries: WALEntry[]) {
|
|
66
112
|
const fd = await fs.promises.open(this.walPath, "a");
|
|
67
113
|
for (const e of entries) {
|
|
@@ -97,14 +143,12 @@ export class LioranDB {
|
|
|
97
143
|
ops.get(entry.tx)!.push(entry);
|
|
98
144
|
}
|
|
99
145
|
} catch {
|
|
100
|
-
// Ignore corrupted WAL tail
|
|
101
146
|
break;
|
|
102
147
|
}
|
|
103
148
|
}
|
|
104
149
|
|
|
105
150
|
for (const tx of committed) {
|
|
106
151
|
if (applied.has(tx)) continue;
|
|
107
|
-
|
|
108
152
|
const txOps = ops.get(tx);
|
|
109
153
|
if (txOps) await this.applyTransaction(txOps);
|
|
110
154
|
}
|
|
@@ -119,6 +163,8 @@ export class LioranDB {
|
|
|
119
163
|
}
|
|
120
164
|
}
|
|
121
165
|
|
|
166
|
+
/* ------------------------- COLLECTION ------------------------- */
|
|
167
|
+
|
|
122
168
|
collection<T = any>(name: string, schema?: ZodSchema<T>): Collection<T> {
|
|
123
169
|
if (this.collections.has(name)) {
|
|
124
170
|
const col = this.collections.get(name)!;
|
|
@@ -130,10 +176,49 @@ export class LioranDB {
|
|
|
130
176
|
fs.mkdirSync(colPath, { recursive: true });
|
|
131
177
|
|
|
132
178
|
const col = new Collection<T>(colPath, schema);
|
|
179
|
+
|
|
180
|
+
// 🔥 Auto-load indexes for this collection
|
|
181
|
+
const metas = this.meta.indexes[name] ?? [];
|
|
182
|
+
for (const m of metas) {
|
|
183
|
+
col.registerIndex(new Index(colPath, m.field, m.options));
|
|
184
|
+
}
|
|
185
|
+
|
|
133
186
|
this.collections.set(name, col);
|
|
134
187
|
return col;
|
|
135
188
|
}
|
|
136
189
|
|
|
190
|
+
/* ------------------------- INDEX API ------------------------- */
|
|
191
|
+
|
|
192
|
+
async createIndex(
|
|
193
|
+
collection: string,
|
|
194
|
+
field: string,
|
|
195
|
+
options: IndexOptions = {}
|
|
196
|
+
) {
|
|
197
|
+
const col = this.collection(collection);
|
|
198
|
+
|
|
199
|
+
const existing = this.meta.indexes[collection]?.find(i => i.field === field);
|
|
200
|
+
if (existing) return;
|
|
201
|
+
|
|
202
|
+
const index = new Index(col.dir, field, options);
|
|
203
|
+
|
|
204
|
+
// 🔁 Build index from existing documents
|
|
205
|
+
for await (const [, enc] of col.db.iterator()) {
|
|
206
|
+
const doc = JSON.parse(Buffer.from(enc, "base64").subarray(32).toString("utf8"));
|
|
207
|
+
await index.insert(doc);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
col.registerIndex(index);
|
|
211
|
+
|
|
212
|
+
if (!this.meta.indexes[collection]) {
|
|
213
|
+
this.meta.indexes[collection] = [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this.meta.indexes[collection].push({ field, options });
|
|
217
|
+
this.saveMeta();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* ------------------------- TX API ------------------------- */
|
|
221
|
+
|
|
137
222
|
async transaction<T>(fn: (tx: DBTransactionContext) => Promise<T>): Promise<T> {
|
|
138
223
|
const txId = ++LioranDB.TX_SEQ;
|
|
139
224
|
const tx = new DBTransactionContext(this, txId);
|
|
@@ -144,6 +229,8 @@ export class LioranDB {
|
|
|
144
229
|
return result;
|
|
145
230
|
}
|
|
146
231
|
|
|
232
|
+
/* ------------------------- SHUTDOWN ------------------------- */
|
|
233
|
+
|
|
147
234
|
async close(): Promise<void> {
|
|
148
235
|
for (const col of this.collections.values()) {
|
|
149
236
|
try { await col.close(); } catch {}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { ClassicLevel } from "classic-level";
|
|
4
|
+
|
|
5
|
+
/* ----------------------------- TYPES ----------------------------- */
|
|
6
|
+
|
|
7
|
+
export interface IndexOptions {
|
|
8
|
+
unique?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type IndexValue = string | string[];
|
|
12
|
+
|
|
13
|
+
/* ----------------------------- INDEX ----------------------------- */
|
|
14
|
+
|
|
15
|
+
export class Index {
|
|
16
|
+
readonly field: string;
|
|
17
|
+
readonly unique: boolean;
|
|
18
|
+
readonly dir: string;
|
|
19
|
+
readonly db: ClassicLevel<string, string>;
|
|
20
|
+
|
|
21
|
+
constructor(baseDir: string, field: string, options: IndexOptions = {}) {
|
|
22
|
+
this.field = field;
|
|
23
|
+
this.unique = !!options.unique;
|
|
24
|
+
|
|
25
|
+
this.dir = path.join(baseDir, "__indexes", field + ".idx");
|
|
26
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
this.db = new ClassicLevel(this.dir, { valueEncoding: "utf8" });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ------------------------- INTERNAL ------------------------- */
|
|
32
|
+
|
|
33
|
+
private normalizeKey(value: any): string {
|
|
34
|
+
if (value === null || value === undefined) return "__null__";
|
|
35
|
+
|
|
36
|
+
if (typeof value === "object") {
|
|
37
|
+
return JSON.stringify(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return String(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private async getRaw(key: string): Promise<IndexValue | null> {
|
|
44
|
+
try {
|
|
45
|
+
const v = await this.db.get(key);
|
|
46
|
+
if (v === undefined) return null;
|
|
47
|
+
return JSON.parse(v);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async setRaw(key: string, value: IndexValue) {
|
|
54
|
+
await this.db.put(key, JSON.stringify(value));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private async delRaw(key: string) {
|
|
58
|
+
try { await this.db.del(key); } catch {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* --------------------------- API --------------------------- */
|
|
62
|
+
|
|
63
|
+
async insert(doc: any) {
|
|
64
|
+
const val = doc[this.field];
|
|
65
|
+
if (val === undefined) return;
|
|
66
|
+
|
|
67
|
+
const key = this.normalizeKey(val);
|
|
68
|
+
|
|
69
|
+
if (this.unique) {
|
|
70
|
+
const existing = await this.getRaw(key);
|
|
71
|
+
if (existing) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Unique index violation on "${this.field}" = ${val}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await this.setRaw(key, doc._id);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const arr = (await this.getRaw(key)) as string[] | null;
|
|
82
|
+
|
|
83
|
+
if (!arr) {
|
|
84
|
+
await this.setRaw(key, [doc._id]);
|
|
85
|
+
} else {
|
|
86
|
+
if (!arr.includes(doc._id)) {
|
|
87
|
+
arr.push(doc._id);
|
|
88
|
+
await this.setRaw(key, arr);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async delete(doc: any) {
|
|
94
|
+
const val = doc[this.field];
|
|
95
|
+
if (val === undefined) return;
|
|
96
|
+
|
|
97
|
+
const key = this.normalizeKey(val);
|
|
98
|
+
|
|
99
|
+
if (this.unique) {
|
|
100
|
+
await this.delRaw(key);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const arr = (await this.getRaw(key)) as string[] | null;
|
|
105
|
+
if (!arr) return;
|
|
106
|
+
|
|
107
|
+
const next = arr.filter(id => id !== doc._id);
|
|
108
|
+
|
|
109
|
+
if (next.length === 0) {
|
|
110
|
+
await this.delRaw(key);
|
|
111
|
+
} else {
|
|
112
|
+
await this.setRaw(key, next);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async update(oldDoc: any, newDoc: any) {
|
|
117
|
+
const oldVal = oldDoc?.[this.field];
|
|
118
|
+
const newVal = newDoc?.[this.field];
|
|
119
|
+
|
|
120
|
+
if (oldVal === newVal) return;
|
|
121
|
+
|
|
122
|
+
if (oldDoc) await this.delete(oldDoc);
|
|
123
|
+
if (newDoc) await this.insert(newDoc);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async find(value: any): Promise<string[]> {
|
|
127
|
+
const key = this.normalizeKey(value);
|
|
128
|
+
|
|
129
|
+
const raw = await this.getRaw(key);
|
|
130
|
+
if (!raw) return [];
|
|
131
|
+
|
|
132
|
+
if (this.unique) return [raw as string];
|
|
133
|
+
|
|
134
|
+
return raw as string[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async close() {
|
|
138
|
+
try { await this.db.close(); } catch {}
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/core/query.ts
CHANGED
|
@@ -2,6 +2,8 @@ function getByPath(obj: any, path: string): any {
|
|
|
2
2
|
return path.split(".").reduce((o, p) => (o ? o[p] : undefined), obj);
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
/* ----------------------------- MATCH ENGINE ----------------------------- */
|
|
6
|
+
|
|
5
7
|
export function matchDocument(doc: any, query: any): boolean {
|
|
6
8
|
for (const key of Object.keys(query)) {
|
|
7
9
|
const cond = query[key];
|
|
@@ -16,8 +18,7 @@ export function matchDocument(doc: any, query: any): boolean {
|
|
|
16
18
|
if (op === "$lte" && !(val <= v)) return false;
|
|
17
19
|
if (op === "$ne" && val === v) return false;
|
|
18
20
|
if (op === "$eq" && val !== v) return false;
|
|
19
|
-
if (op === "$in" && (!Array.isArray(v) || !v.includes(val)))
|
|
20
|
-
return false;
|
|
21
|
+
if (op === "$in" && (!Array.isArray(v) || !v.includes(val))) return false;
|
|
21
22
|
}
|
|
22
23
|
} else {
|
|
23
24
|
if (val !== cond) return false;
|
|
@@ -26,6 +27,8 @@ export function matchDocument(doc: any, query: any): boolean {
|
|
|
26
27
|
return true;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/* ------------------------------ UPDATE ENGINE ------------------------------ */
|
|
31
|
+
|
|
29
32
|
export function applyUpdate(oldDoc: any, update: any): any {
|
|
30
33
|
const doc = structuredClone(oldDoc);
|
|
31
34
|
|
|
@@ -61,3 +64,103 @@ export function applyUpdate(oldDoc: any, update: any): any {
|
|
|
61
64
|
|
|
62
65
|
return doc;
|
|
63
66
|
}
|
|
67
|
+
|
|
68
|
+
/* ------------------------------ INDEX ROUTER ------------------------------ */
|
|
69
|
+
|
|
70
|
+
export function extractIndexQuery(query: any): { field: string; value: any } | null {
|
|
71
|
+
for (const key of Object.keys(query)) {
|
|
72
|
+
const cond = query[key];
|
|
73
|
+
|
|
74
|
+
// Skip _id as it's handled separately
|
|
75
|
+
if (key === "_id") continue;
|
|
76
|
+
|
|
77
|
+
// Simple equality: { field: value }
|
|
78
|
+
if (!cond || typeof cond !== "object" || Array.isArray(cond)) {
|
|
79
|
+
return { field: key, value: cond };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// $eq operator: { field: { $eq: value } }
|
|
83
|
+
if ("$eq" in cond) {
|
|
84
|
+
return { field: key, value: cond.$eq };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface IndexProvider {
|
|
92
|
+
findByIndex(
|
|
93
|
+
field: string,
|
|
94
|
+
value: any
|
|
95
|
+
): Promise<Set<string> | null>;
|
|
96
|
+
|
|
97
|
+
rangeByIndex?(
|
|
98
|
+
field: string,
|
|
99
|
+
cond: any
|
|
100
|
+
): Promise<Set<string> | null>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Selects best possible index from query.
|
|
105
|
+
*/
|
|
106
|
+
export function selectIndex(query: any, indexes: Set<string>) {
|
|
107
|
+
for (const key of Object.keys(query)) {
|
|
108
|
+
if (!indexes.has(key)) continue;
|
|
109
|
+
|
|
110
|
+
const cond = query[key];
|
|
111
|
+
|
|
112
|
+
if (cond && typeof cond === "object" && !Array.isArray(cond)) {
|
|
113
|
+
return { field: key, cond };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { field: key, cond: { $eq: cond } };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Executes indexed query or fallback full scan.
|
|
124
|
+
*/
|
|
125
|
+
export async function runIndexedQuery(
|
|
126
|
+
query: any,
|
|
127
|
+
indexProvider: IndexProvider,
|
|
128
|
+
allDocIds: () => Promise<string[]>
|
|
129
|
+
): Promise<Set<string>> {
|
|
130
|
+
const indexes = (indexProvider as any).indexes as Set<string>;
|
|
131
|
+
|
|
132
|
+
if (!indexes?.size) {
|
|
133
|
+
return new Set(await allDocIds());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const sel = selectIndex(query, indexes);
|
|
137
|
+
if (!sel) {
|
|
138
|
+
return new Set(await allDocIds());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { field, cond } = sel;
|
|
142
|
+
|
|
143
|
+
if ("$eq" in cond) {
|
|
144
|
+
return (await indexProvider.findByIndex(field, cond.$eq)) ??
|
|
145
|
+
new Set(await allDocIds());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if ("$in" in cond) {
|
|
149
|
+
const out = new Set<string>();
|
|
150
|
+
for (const v of cond.$in) {
|
|
151
|
+
const r = await indexProvider.findByIndex(field, v);
|
|
152
|
+
if (r) for (const id of r) out.add(id);
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (
|
|
158
|
+
indexProvider.rangeByIndex &&
|
|
159
|
+
("$gt" in cond || "$gte" in cond || "$lt" in cond || "$lte" in cond)
|
|
160
|
+
) {
|
|
161
|
+
return (await indexProvider.rangeByIndex(field, cond)) ??
|
|
162
|
+
new Set(await allDocIds());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return new Set(await allDocIds());
|
|
166
|
+
}
|