@liorandb/core 1.0.17 → 1.0.18
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 +82 -19
- package/dist/index.js +128 -98
- package/package.json +1 -1
- package/src/core/collection.ts +150 -107
- package/src/core/database.ts +23 -29
- package/src/types/index.ts +107 -34
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ declare class Index {
|
|
|
21
21
|
close(): Promise<void>;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
interface Migration<T = any> {
|
|
25
|
+
from: number;
|
|
26
|
+
to: number;
|
|
27
|
+
migrate: (doc: any) => T;
|
|
28
|
+
}
|
|
24
29
|
interface UpdateOptions$1 {
|
|
25
30
|
upsert?: boolean;
|
|
26
31
|
}
|
|
@@ -29,20 +34,25 @@ declare class Collection<T = any> {
|
|
|
29
34
|
db: ClassicLevel<string, string>;
|
|
30
35
|
private queue;
|
|
31
36
|
private schema?;
|
|
37
|
+
private schemaVersion;
|
|
38
|
+
private migrations;
|
|
32
39
|
private indexes;
|
|
33
|
-
constructor(dir: string, schema?: ZodSchema<T
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
setSchema(schema: ZodSchema<T>): void;
|
|
40
|
+
constructor(dir: string, schema?: ZodSchema<T>, schemaVersion?: number);
|
|
41
|
+
setSchema(schema: ZodSchema<T>, version: number): void;
|
|
42
|
+
addMigration(migration: Migration<T>): void;
|
|
37
43
|
private validate;
|
|
44
|
+
private migrateIfNeeded;
|
|
38
45
|
private _enqueue;
|
|
39
46
|
close(): Promise<void>;
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
registerIndex(index: Index): void;
|
|
48
|
+
getIndex(field: string): Index | undefined;
|
|
42
49
|
private _updateIndexes;
|
|
50
|
+
compact(): Promise<void>;
|
|
51
|
+
_exec(op: string, args: any[]): Promise<number | boolean | T | T[] | null>;
|
|
43
52
|
private _insertOne;
|
|
44
53
|
private _insertMany;
|
|
45
54
|
private _getCandidateIds;
|
|
55
|
+
private _readAndMigrate;
|
|
46
56
|
private _find;
|
|
47
57
|
private _findOne;
|
|
48
58
|
private _countDocuments;
|
|
@@ -50,15 +60,15 @@ declare class Collection<T = any> {
|
|
|
50
60
|
private _updateMany;
|
|
51
61
|
private _deleteOne;
|
|
52
62
|
private _deleteMany;
|
|
53
|
-
insertOne(doc: any): Promise<
|
|
54
|
-
insertMany(docs: any[]): Promise<
|
|
55
|
-
find(query?: any): Promise<
|
|
56
|
-
findOne(query?: any): Promise<
|
|
57
|
-
updateOne(filter: any, update: any, options?: UpdateOptions$1): Promise<
|
|
58
|
-
updateMany(filter: any, update: any): Promise<
|
|
59
|
-
deleteOne(filter: any): Promise<
|
|
60
|
-
deleteMany(filter: any): Promise<
|
|
61
|
-
countDocuments(filter?: any): Promise<
|
|
63
|
+
insertOne(doc: any): Promise<number | boolean | T | T[] | null>;
|
|
64
|
+
insertMany(docs: any[]): Promise<number | boolean | T | T[] | null>;
|
|
65
|
+
find(query?: any): Promise<number | boolean | T | T[] | null>;
|
|
66
|
+
findOne(query?: any): Promise<number | boolean | T | T[] | null>;
|
|
67
|
+
updateOne(filter: any, update: any, options?: UpdateOptions$1): Promise<number | boolean | T | T[] | null>;
|
|
68
|
+
updateMany(filter: any, update: any): Promise<number | boolean | T | T[] | null>;
|
|
69
|
+
deleteOne(filter: any): Promise<number | boolean | T | T[] | null>;
|
|
70
|
+
deleteMany(filter: any): Promise<number | boolean | T | T[] | null>;
|
|
71
|
+
countDocuments(filter?: any): Promise<number | boolean | T | T[] | null>;
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
type TXOp = {
|
|
@@ -105,7 +115,7 @@ declare class LioranDB {
|
|
|
105
115
|
clearWAL(): Promise<void>;
|
|
106
116
|
private recoverFromWAL;
|
|
107
117
|
applyTransaction(ops: TXOp[]): Promise<void>;
|
|
108
|
-
collection<T = any>(name: string, schema?: ZodSchema<T
|
|
118
|
+
collection<T = any>(name: string, schema?: ZodSchema<T>, schemaVersion?: number): Collection<T>;
|
|
109
119
|
createIndex(collection: string, field: string, options?: IndexOptions): Promise<void>;
|
|
110
120
|
compactCollection(name: string): Promise<void>;
|
|
111
121
|
compactAll(): Promise<void>;
|
|
@@ -149,13 +159,23 @@ interface LioranManagerOptions {
|
|
|
149
159
|
rootPath?: string;
|
|
150
160
|
encryptionKey?: string | Buffer;
|
|
151
161
|
ipc?: boolean;
|
|
162
|
+
/**
|
|
163
|
+
* If true, database auto-applies pending migrations on startup.
|
|
164
|
+
*/
|
|
165
|
+
autoMigrate?: boolean;
|
|
152
166
|
}
|
|
153
167
|
interface UpdateOptions {
|
|
154
168
|
upsert?: boolean;
|
|
169
|
+
/**
|
|
170
|
+
* If true, returns the modified document instead of the original.
|
|
171
|
+
*/
|
|
172
|
+
returnNew?: boolean;
|
|
155
173
|
}
|
|
156
|
-
type Query<T = any> = Partial<T>
|
|
174
|
+
type Query<T = any> = Partial<T> | ({
|
|
175
|
+
[K in keyof T]?: any;
|
|
176
|
+
} & {
|
|
157
177
|
[key: string]: any;
|
|
158
|
-
};
|
|
178
|
+
});
|
|
159
179
|
type IndexType = "hash" | "btree";
|
|
160
180
|
interface IndexDefinition<T = any> {
|
|
161
181
|
field: keyof T | string;
|
|
@@ -177,6 +197,40 @@ interface QueryExplainPlan {
|
|
|
177
197
|
returnedDocuments: number;
|
|
178
198
|
executionTimeMs: number;
|
|
179
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Per-collection document schema version
|
|
202
|
+
*/
|
|
203
|
+
type SchemaVersion = number;
|
|
204
|
+
/**
|
|
205
|
+
* Collection-level migration definition
|
|
206
|
+
*/
|
|
207
|
+
interface CollectionMigration<T = any> {
|
|
208
|
+
from: SchemaVersion;
|
|
209
|
+
to: SchemaVersion;
|
|
210
|
+
migrate: (doc: any) => T;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Database-level migration definition
|
|
214
|
+
*/
|
|
215
|
+
interface DatabaseMigration {
|
|
216
|
+
from: string;
|
|
217
|
+
to: string;
|
|
218
|
+
migrate: () => Promise<void>;
|
|
219
|
+
}
|
|
220
|
+
interface CollectionOptions<T = any> {
|
|
221
|
+
/**
|
|
222
|
+
* Zod schema used for validation
|
|
223
|
+
*/
|
|
224
|
+
schema?: any;
|
|
225
|
+
/**
|
|
226
|
+
* Current document schema version
|
|
227
|
+
*/
|
|
228
|
+
schemaVersion?: SchemaVersion;
|
|
229
|
+
/**
|
|
230
|
+
* Optional migrations for automatic document upgrading
|
|
231
|
+
*/
|
|
232
|
+
migrations?: CollectionMigration<T>[];
|
|
233
|
+
}
|
|
180
234
|
interface CollectionIndexAPI<T = any> {
|
|
181
235
|
createIndex(def: IndexDefinition<T>): Promise<void>;
|
|
182
236
|
dropIndex(field: keyof T | string): Promise<void>;
|
|
@@ -186,5 +240,14 @@ interface CollectionIndexAPI<T = any> {
|
|
|
186
240
|
interface DatabaseIndexAPI {
|
|
187
241
|
rebuildAllIndexes(): Promise<void>;
|
|
188
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Database migration coordination API
|
|
245
|
+
*/
|
|
246
|
+
interface DatabaseMigrationAPI {
|
|
247
|
+
migrate(from: string, to: string, fn: () => Promise<void>): void;
|
|
248
|
+
applyMigrations(targetVersion: string): Promise<void>;
|
|
249
|
+
getSchemaVersion(): string;
|
|
250
|
+
setSchemaVersion(version: string): void;
|
|
251
|
+
}
|
|
189
252
|
|
|
190
|
-
export { type CollectionIndexAPI, type DatabaseIndexAPI, type IndexDefinition, type IndexMetadata, type IndexType, LioranDB, LioranManager, type LioranManagerOptions, type Query, type QueryExplainPlan, type UpdateOptions, getBaseDBFolder };
|
|
253
|
+
export { type CollectionIndexAPI, type CollectionMigration, type CollectionOptions, type DatabaseIndexAPI, type DatabaseMigration, type DatabaseMigrationAPI, type IndexDefinition, type IndexMetadata, type IndexType, LioranDB, LioranManager, type LioranManagerOptions, type Query, type QueryExplainPlan, type SchemaVersion, type UpdateOptions, getBaseDBFolder };
|
package/dist/index.js
CHANGED
|
@@ -370,26 +370,43 @@ var Collection = class {
|
|
|
370
370
|
db;
|
|
371
371
|
queue = Promise.resolve();
|
|
372
372
|
schema;
|
|
373
|
+
schemaVersion = 1;
|
|
374
|
+
migrations = [];
|
|
373
375
|
indexes = /* @__PURE__ */ new Map();
|
|
374
|
-
constructor(dir, schema) {
|
|
376
|
+
constructor(dir, schema, schemaVersion = 1) {
|
|
375
377
|
this.dir = dir;
|
|
376
378
|
this.db = new ClassicLevel3(dir, { valueEncoding: "utf8" });
|
|
377
379
|
this.schema = schema;
|
|
380
|
+
this.schemaVersion = schemaVersion;
|
|
378
381
|
}
|
|
379
|
-
/*
|
|
380
|
-
|
|
381
|
-
this.indexes.set(index.field, index);
|
|
382
|
-
}
|
|
383
|
-
getIndex(field) {
|
|
384
|
-
return this.indexes.get(field);
|
|
385
|
-
}
|
|
386
|
-
/* -------------------------- CORE -------------------------- */
|
|
387
|
-
setSchema(schema) {
|
|
382
|
+
/* ===================== SCHEMA ===================== */
|
|
383
|
+
setSchema(schema, version) {
|
|
388
384
|
this.schema = schema;
|
|
385
|
+
this.schemaVersion = version;
|
|
386
|
+
}
|
|
387
|
+
addMigration(migration) {
|
|
388
|
+
this.migrations.push(migration);
|
|
389
|
+
this.migrations.sort((a, b) => a.from - b.from);
|
|
389
390
|
}
|
|
390
391
|
validate(doc) {
|
|
391
392
|
return this.schema ? validateSchema(this.schema, doc) : doc;
|
|
392
393
|
}
|
|
394
|
+
migrateIfNeeded(doc) {
|
|
395
|
+
let currentVersion = doc.__v ?? 1;
|
|
396
|
+
if (currentVersion === this.schemaVersion) {
|
|
397
|
+
return doc;
|
|
398
|
+
}
|
|
399
|
+
let working = doc;
|
|
400
|
+
for (const migration of this.migrations) {
|
|
401
|
+
if (migration.from === currentVersion) {
|
|
402
|
+
working = migration.migrate(working);
|
|
403
|
+
currentVersion = migration.to;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
working.__v = this.schemaVersion;
|
|
407
|
+
return this.validate(working);
|
|
408
|
+
}
|
|
409
|
+
/* ===================== QUEUE ===================== */
|
|
393
410
|
_enqueue(task) {
|
|
394
411
|
this.queue = this.queue.then(task).catch(console.error);
|
|
395
412
|
return this.queue;
|
|
@@ -406,7 +423,19 @@ var Collection = class {
|
|
|
406
423
|
} catch {
|
|
407
424
|
}
|
|
408
425
|
}
|
|
409
|
-
/*
|
|
426
|
+
/* ===================== INDEX MANAGEMENT ===================== */
|
|
427
|
+
registerIndex(index) {
|
|
428
|
+
this.indexes.set(index.field, index);
|
|
429
|
+
}
|
|
430
|
+
getIndex(field) {
|
|
431
|
+
return this.indexes.get(field);
|
|
432
|
+
}
|
|
433
|
+
async _updateIndexes(oldDoc, newDoc) {
|
|
434
|
+
for (const index of this.indexes.values()) {
|
|
435
|
+
await index.update(oldDoc, newDoc);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/* ===================== COMPACTION ===================== */
|
|
410
439
|
async compact() {
|
|
411
440
|
return this._enqueue(async () => {
|
|
412
441
|
try {
|
|
@@ -418,6 +447,7 @@ var Collection = class {
|
|
|
418
447
|
await rebuildIndexes(this);
|
|
419
448
|
});
|
|
420
449
|
}
|
|
450
|
+
/* ===================== INTERNAL EXEC ===================== */
|
|
421
451
|
async _exec(op, args) {
|
|
422
452
|
switch (op) {
|
|
423
453
|
case "insertOne":
|
|
@@ -442,16 +472,14 @@ var Collection = class {
|
|
|
442
472
|
throw new Error(`Unknown operation: ${op}`);
|
|
443
473
|
}
|
|
444
474
|
}
|
|
445
|
-
/*
|
|
446
|
-
async _updateIndexes(oldDoc, newDoc) {
|
|
447
|
-
for (const index of this.indexes.values()) {
|
|
448
|
-
await index.update(oldDoc, newDoc);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
/* ---------------- Storage ---------------- */
|
|
475
|
+
/* ===================== STORAGE ===================== */
|
|
452
476
|
async _insertOne(doc) {
|
|
453
477
|
const _id = doc._id ?? uuid();
|
|
454
|
-
const final = this.validate({
|
|
478
|
+
const final = this.validate({
|
|
479
|
+
_id,
|
|
480
|
+
...doc,
|
|
481
|
+
__v: this.schemaVersion
|
|
482
|
+
});
|
|
455
483
|
await this.db.put(String(_id), encryptData(final));
|
|
456
484
|
await this._updateIndexes(null, final);
|
|
457
485
|
return final;
|
|
@@ -461,7 +489,11 @@ var Collection = class {
|
|
|
461
489
|
const out = [];
|
|
462
490
|
for (const d of docs) {
|
|
463
491
|
const _id = d._id ?? uuid();
|
|
464
|
-
const final = this.validate({
|
|
492
|
+
const final = this.validate({
|
|
493
|
+
_id,
|
|
494
|
+
...d,
|
|
495
|
+
__v: this.schemaVersion
|
|
496
|
+
});
|
|
465
497
|
batch.push({
|
|
466
498
|
type: "put",
|
|
467
499
|
key: String(_id),
|
|
@@ -475,7 +507,7 @@ var Collection = class {
|
|
|
475
507
|
}
|
|
476
508
|
return out;
|
|
477
509
|
}
|
|
478
|
-
/*
|
|
510
|
+
/* ===================== QUERY ===================== */
|
|
479
511
|
async _getCandidateIds(query) {
|
|
480
512
|
const indexedFields = new Set(this.indexes.keys());
|
|
481
513
|
return runIndexedQuery(
|
|
@@ -497,15 +529,26 @@ var Collection = class {
|
|
|
497
529
|
}
|
|
498
530
|
);
|
|
499
531
|
}
|
|
532
|
+
async _readAndMigrate(id) {
|
|
533
|
+
const enc = await this.db.get(id);
|
|
534
|
+
if (!enc) return null;
|
|
535
|
+
const raw = decryptData(enc);
|
|
536
|
+
const migrated = this.migrateIfNeeded(raw);
|
|
537
|
+
if (raw.__v !== this.schemaVersion) {
|
|
538
|
+
await this.db.put(id, encryptData(migrated));
|
|
539
|
+
await this._updateIndexes(raw, migrated);
|
|
540
|
+
}
|
|
541
|
+
return migrated;
|
|
542
|
+
}
|
|
500
543
|
async _find(query) {
|
|
501
544
|
const ids = await this._getCandidateIds(query);
|
|
502
545
|
const out = [];
|
|
503
546
|
for (const id of ids) {
|
|
504
547
|
try {
|
|
505
|
-
const
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
548
|
+
const doc = await this._readAndMigrate(id);
|
|
549
|
+
if (doc && matchDocument(doc, query)) {
|
|
550
|
+
out.push(doc);
|
|
551
|
+
}
|
|
509
552
|
} catch {
|
|
510
553
|
}
|
|
511
554
|
}
|
|
@@ -514,8 +557,7 @@ var Collection = class {
|
|
|
514
557
|
async _findOne(query) {
|
|
515
558
|
if (query?._id) {
|
|
516
559
|
try {
|
|
517
|
-
|
|
518
|
-
return enc ? decryptData(enc) : null;
|
|
560
|
+
return await this._readAndMigrate(String(query._id));
|
|
519
561
|
} catch {
|
|
520
562
|
return null;
|
|
521
563
|
}
|
|
@@ -523,10 +565,10 @@ var Collection = class {
|
|
|
523
565
|
const ids = await this._getCandidateIds(query);
|
|
524
566
|
for (const id of ids) {
|
|
525
567
|
try {
|
|
526
|
-
const
|
|
527
|
-
if (
|
|
528
|
-
|
|
529
|
-
|
|
568
|
+
const doc = await this._readAndMigrate(id);
|
|
569
|
+
if (doc && matchDocument(doc, query)) {
|
|
570
|
+
return doc;
|
|
571
|
+
}
|
|
530
572
|
} catch {
|
|
531
573
|
}
|
|
532
574
|
}
|
|
@@ -537,40 +579,34 @@ var Collection = class {
|
|
|
537
579
|
let count = 0;
|
|
538
580
|
for (const id of ids) {
|
|
539
581
|
try {
|
|
540
|
-
const
|
|
541
|
-
if (
|
|
542
|
-
|
|
582
|
+
const doc = await this._readAndMigrate(id);
|
|
583
|
+
if (doc && matchDocument(doc, filter)) {
|
|
584
|
+
count++;
|
|
585
|
+
}
|
|
543
586
|
} catch {
|
|
544
587
|
}
|
|
545
588
|
}
|
|
546
589
|
return count;
|
|
547
590
|
}
|
|
548
|
-
/*
|
|
591
|
+
/* ===================== UPDATE ===================== */
|
|
549
592
|
async _updateOne(filter, update, options) {
|
|
550
593
|
const ids = await this._getCandidateIds(filter);
|
|
551
594
|
for (const id of ids) {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
} catch {
|
|
595
|
+
const existing = await this._readAndMigrate(id);
|
|
596
|
+
if (!existing) continue;
|
|
597
|
+
if (matchDocument(existing, filter)) {
|
|
598
|
+
const updated = this.validate({
|
|
599
|
+
...applyUpdate(existing, update),
|
|
600
|
+
_id: existing._id,
|
|
601
|
+
__v: this.schemaVersion
|
|
602
|
+
});
|
|
603
|
+
await this.db.put(id, encryptData(updated));
|
|
604
|
+
await this._updateIndexes(existing, updated);
|
|
605
|
+
return updated;
|
|
564
606
|
}
|
|
565
607
|
}
|
|
566
608
|
if (options?.upsert) {
|
|
567
|
-
|
|
568
|
-
_id: uuid(),
|
|
569
|
-
...applyUpdate({}, update)
|
|
570
|
-
});
|
|
571
|
-
await this.db.put(String(doc._id), encryptData(doc));
|
|
572
|
-
await this._updateIndexes(null, doc);
|
|
573
|
-
return doc;
|
|
609
|
+
return this._insertOne(applyUpdate({}, update));
|
|
574
610
|
}
|
|
575
611
|
return null;
|
|
576
612
|
}
|
|
@@ -578,36 +614,31 @@ var Collection = class {
|
|
|
578
614
|
const ids = await this._getCandidateIds(filter);
|
|
579
615
|
const out = [];
|
|
580
616
|
for (const id of ids) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
} catch {
|
|
617
|
+
const existing = await this._readAndMigrate(id);
|
|
618
|
+
if (!existing) continue;
|
|
619
|
+
if (matchDocument(existing, filter)) {
|
|
620
|
+
const updated = this.validate({
|
|
621
|
+
...applyUpdate(existing, update),
|
|
622
|
+
_id: existing._id,
|
|
623
|
+
__v: this.schemaVersion
|
|
624
|
+
});
|
|
625
|
+
await this.db.put(id, encryptData(updated));
|
|
626
|
+
await this._updateIndexes(existing, updated);
|
|
627
|
+
out.push(updated);
|
|
593
628
|
}
|
|
594
629
|
}
|
|
595
630
|
return out;
|
|
596
631
|
}
|
|
597
|
-
/*
|
|
632
|
+
/* ===================== DELETE ===================== */
|
|
598
633
|
async _deleteOne(filter) {
|
|
599
634
|
const ids = await this._getCandidateIds(filter);
|
|
600
635
|
for (const id of ids) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
await this._updateIndexes(value, null);
|
|
608
|
-
return true;
|
|
609
|
-
}
|
|
610
|
-
} catch {
|
|
636
|
+
const existing = await this._readAndMigrate(id);
|
|
637
|
+
if (!existing) continue;
|
|
638
|
+
if (matchDocument(existing, filter)) {
|
|
639
|
+
await this.db.del(id);
|
|
640
|
+
await this._updateIndexes(existing, null);
|
|
641
|
+
return true;
|
|
611
642
|
}
|
|
612
643
|
}
|
|
613
644
|
return false;
|
|
@@ -616,21 +647,17 @@ var Collection = class {
|
|
|
616
647
|
const ids = await this._getCandidateIds(filter);
|
|
617
648
|
let count = 0;
|
|
618
649
|
for (const id of ids) {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
await this._updateIndexes(value, null);
|
|
626
|
-
count++;
|
|
627
|
-
}
|
|
628
|
-
} catch {
|
|
650
|
+
const existing = await this._readAndMigrate(id);
|
|
651
|
+
if (!existing) continue;
|
|
652
|
+
if (matchDocument(existing, filter)) {
|
|
653
|
+
await this.db.del(id);
|
|
654
|
+
await this._updateIndexes(existing, null);
|
|
655
|
+
count++;
|
|
629
656
|
}
|
|
630
657
|
}
|
|
631
658
|
return count;
|
|
632
659
|
}
|
|
633
|
-
/*
|
|
660
|
+
/* ===================== PUBLIC API ===================== */
|
|
634
661
|
insertOne(doc) {
|
|
635
662
|
return this._enqueue(() => this._exec("insertOne", [doc]));
|
|
636
663
|
}
|
|
@@ -875,7 +902,7 @@ var LioranDB = class _LioranDB {
|
|
|
875
902
|
this.meta.schemaVersion = v;
|
|
876
903
|
this.saveMeta();
|
|
877
904
|
}
|
|
878
|
-
/* -------------------------
|
|
905
|
+
/* ------------------------- DB MIGRATIONS ------------------------- */
|
|
879
906
|
migrate(from, to, fn) {
|
|
880
907
|
this.migrator.register(from, to, async (db) => {
|
|
881
908
|
await fn(db);
|
|
@@ -930,15 +957,21 @@ var LioranDB = class _LioranDB {
|
|
|
930
957
|
}
|
|
931
958
|
}
|
|
932
959
|
/* ------------------------- COLLECTION ------------------------- */
|
|
933
|
-
collection(name, schema) {
|
|
960
|
+
collection(name, schema, schemaVersion) {
|
|
934
961
|
if (this.collections.has(name)) {
|
|
935
962
|
const col2 = this.collections.get(name);
|
|
936
|
-
if (schema)
|
|
963
|
+
if (schema && schemaVersion !== void 0) {
|
|
964
|
+
col2.setSchema(schema, schemaVersion);
|
|
965
|
+
}
|
|
937
966
|
return col2;
|
|
938
967
|
}
|
|
939
968
|
const colPath = path4.join(this.basePath, name);
|
|
940
969
|
fs4.mkdirSync(colPath, { recursive: true });
|
|
941
|
-
const col = new Collection(
|
|
970
|
+
const col = new Collection(
|
|
971
|
+
colPath,
|
|
972
|
+
schema,
|
|
973
|
+
schemaVersion ?? 1
|
|
974
|
+
);
|
|
942
975
|
const metas = this.meta.indexes[name] ?? [];
|
|
943
976
|
for (const m of metas) {
|
|
944
977
|
col.registerIndex(new Index(colPath, m.field, m.options));
|
|
@@ -955,11 +988,11 @@ var LioranDB = class _LioranDB {
|
|
|
955
988
|
for await (const [key, enc] of col.db.iterator()) {
|
|
956
989
|
if (!enc) continue;
|
|
957
990
|
try {
|
|
958
|
-
const doc =
|
|
991
|
+
const doc = decryptData(enc);
|
|
959
992
|
await index.insert(doc);
|
|
960
993
|
} catch (err) {
|
|
961
|
-
const
|
|
962
|
-
console.warn(`
|
|
994
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
995
|
+
console.warn(`Index build skipped doc ${key}: ${msg}`);
|
|
963
996
|
}
|
|
964
997
|
}
|
|
965
998
|
col.registerIndex(index);
|
|
@@ -1000,9 +1033,6 @@ var LioranDB = class _LioranDB {
|
|
|
1000
1033
|
this.collections.clear();
|
|
1001
1034
|
}
|
|
1002
1035
|
};
|
|
1003
|
-
function decryptData2(enc) {
|
|
1004
|
-
throw new Error("Function not implemented.");
|
|
1005
|
-
}
|
|
1006
1036
|
|
|
1007
1037
|
// src/utils/rootpath.ts
|
|
1008
1038
|
import os2 from "os";
|
package/package.json
CHANGED
package/src/core/collection.ts
CHANGED
|
@@ -11,6 +11,14 @@ import { validateSchema } from "../utils/schema.js";
|
|
|
11
11
|
import { Index } from "./index.js";
|
|
12
12
|
import { compactCollectionEngine, rebuildIndexes } from "./compaction.js";
|
|
13
13
|
|
|
14
|
+
/* ===================== SCHEMA VERSIONING ===================== */
|
|
15
|
+
|
|
16
|
+
export interface Migration<T = any> {
|
|
17
|
+
from: number;
|
|
18
|
+
to: number;
|
|
19
|
+
migrate: (doc: any) => T;
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
export interface UpdateOptions {
|
|
15
23
|
upsert?: boolean;
|
|
16
24
|
}
|
|
@@ -19,35 +27,62 @@ export class Collection<T = any> {
|
|
|
19
27
|
dir: string;
|
|
20
28
|
db: ClassicLevel<string, string>;
|
|
21
29
|
private queue: Promise<any> = Promise.resolve();
|
|
30
|
+
|
|
22
31
|
private schema?: ZodSchema<T>;
|
|
32
|
+
private schemaVersion: number = 1;
|
|
33
|
+
private migrations: Migration<T>[] = [];
|
|
34
|
+
|
|
23
35
|
private indexes = new Map<string, Index>();
|
|
24
36
|
|
|
25
|
-
constructor(
|
|
37
|
+
constructor(
|
|
38
|
+
dir: string,
|
|
39
|
+
schema?: ZodSchema<T>,
|
|
40
|
+
schemaVersion: number = 1
|
|
41
|
+
) {
|
|
26
42
|
this.dir = dir;
|
|
27
43
|
this.db = new ClassicLevel(dir, { valueEncoding: "utf8" });
|
|
28
44
|
this.schema = schema;
|
|
45
|
+
this.schemaVersion = schemaVersion;
|
|
29
46
|
}
|
|
30
47
|
|
|
31
|
-
/*
|
|
48
|
+
/* ===================== SCHEMA ===================== */
|
|
32
49
|
|
|
33
|
-
|
|
34
|
-
this.
|
|
50
|
+
setSchema(schema: ZodSchema<T>, version: number) {
|
|
51
|
+
this.schema = schema;
|
|
52
|
+
this.schemaVersion = version;
|
|
35
53
|
}
|
|
36
54
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/* -------------------------- CORE -------------------------- */
|
|
42
|
-
|
|
43
|
-
setSchema(schema: ZodSchema<T>) {
|
|
44
|
-
this.schema = schema;
|
|
55
|
+
addMigration(migration: Migration<T>) {
|
|
56
|
+
this.migrations.push(migration);
|
|
57
|
+
this.migrations.sort((a, b) => a.from - b.from);
|
|
45
58
|
}
|
|
46
59
|
|
|
47
60
|
private validate(doc: any): T {
|
|
48
61
|
return this.schema ? validateSchema(this.schema, doc) : doc;
|
|
49
62
|
}
|
|
50
63
|
|
|
64
|
+
private migrateIfNeeded(doc: any): T {
|
|
65
|
+
let currentVersion = doc.__v ?? 1;
|
|
66
|
+
|
|
67
|
+
if (currentVersion === this.schemaVersion) {
|
|
68
|
+
return doc;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let working = doc;
|
|
72
|
+
|
|
73
|
+
for (const migration of this.migrations) {
|
|
74
|
+
if (migration.from === currentVersion) {
|
|
75
|
+
working = migration.migrate(working);
|
|
76
|
+
currentVersion = migration.to;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
working.__v = this.schemaVersion;
|
|
81
|
+
return this.validate(working);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ===================== QUEUE ===================== */
|
|
85
|
+
|
|
51
86
|
private _enqueue<R>(task: () => Promise<R>): Promise<R> {
|
|
52
87
|
this.queue = this.queue.then(task).catch(console.error);
|
|
53
88
|
return this.queue;
|
|
@@ -60,24 +95,38 @@ export class Collection<T = any> {
|
|
|
60
95
|
try { await this.db.close(); } catch {}
|
|
61
96
|
}
|
|
62
97
|
|
|
63
|
-
/*
|
|
98
|
+
/* ===================== INDEX MANAGEMENT ===================== */
|
|
99
|
+
|
|
100
|
+
registerIndex(index: Index) {
|
|
101
|
+
this.indexes.set(index.field, index);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getIndex(field: string) {
|
|
105
|
+
return this.indexes.get(field);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async _updateIndexes(oldDoc: any, newDoc: any) {
|
|
109
|
+
for (const index of this.indexes.values()) {
|
|
110
|
+
await index.update(oldDoc, newDoc);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* ===================== COMPACTION ===================== */
|
|
64
115
|
|
|
65
116
|
async compact(): Promise<void> {
|
|
66
117
|
return this._enqueue(async () => {
|
|
67
|
-
// Close active DB handles
|
|
68
118
|
try { await this.db.close(); } catch {}
|
|
69
119
|
|
|
70
|
-
// Run compaction engine
|
|
71
120
|
await compactCollectionEngine(this);
|
|
72
121
|
|
|
73
|
-
// Reopen fresh DB
|
|
74
122
|
this.db = new ClassicLevel(this.dir, { valueEncoding: "utf8" });
|
|
75
123
|
|
|
76
|
-
// Rebuild indexes
|
|
77
124
|
await rebuildIndexes(this);
|
|
78
125
|
});
|
|
79
126
|
}
|
|
80
127
|
|
|
128
|
+
/* ===================== INTERNAL EXEC ===================== */
|
|
129
|
+
|
|
81
130
|
async _exec(op: string, args: any[]) {
|
|
82
131
|
switch (op) {
|
|
83
132
|
case "insertOne": return this._insertOne(args[0]);
|
|
@@ -93,19 +142,15 @@ export class Collection<T = any> {
|
|
|
93
142
|
}
|
|
94
143
|
}
|
|
95
144
|
|
|
96
|
-
/*
|
|
97
|
-
|
|
98
|
-
private async _updateIndexes(oldDoc: any, newDoc: any) {
|
|
99
|
-
for (const index of this.indexes.values()) {
|
|
100
|
-
await index.update(oldDoc, newDoc);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/* ---------------- Storage ---------------- */
|
|
145
|
+
/* ===================== STORAGE ===================== */
|
|
105
146
|
|
|
106
147
|
private async _insertOne(doc: any) {
|
|
107
148
|
const _id = doc._id ?? uuid();
|
|
108
|
-
const final = this.validate({
|
|
149
|
+
const final = this.validate({
|
|
150
|
+
_id,
|
|
151
|
+
...doc,
|
|
152
|
+
__v: this.schemaVersion
|
|
153
|
+
});
|
|
109
154
|
|
|
110
155
|
await this.db.put(String(_id), encryptData(final));
|
|
111
156
|
await this._updateIndexes(null, final);
|
|
@@ -114,12 +159,16 @@ export class Collection<T = any> {
|
|
|
114
159
|
}
|
|
115
160
|
|
|
116
161
|
private async _insertMany(docs: any[]) {
|
|
117
|
-
const batch:
|
|
162
|
+
const batch: any[] = [];
|
|
118
163
|
const out = [];
|
|
119
164
|
|
|
120
165
|
for (const d of docs) {
|
|
121
166
|
const _id = d._id ?? uuid();
|
|
122
|
-
const final = this.validate({
|
|
167
|
+
const final = this.validate({
|
|
168
|
+
_id,
|
|
169
|
+
...d,
|
|
170
|
+
__v: this.schemaVersion
|
|
171
|
+
});
|
|
123
172
|
|
|
124
173
|
batch.push({
|
|
125
174
|
type: "put",
|
|
@@ -139,7 +188,7 @@ export class Collection<T = any> {
|
|
|
139
188
|
return out;
|
|
140
189
|
}
|
|
141
190
|
|
|
142
|
-
/*
|
|
191
|
+
/* ===================== QUERY ===================== */
|
|
143
192
|
|
|
144
193
|
private async _getCandidateIds(query: any): Promise<Set<string>> {
|
|
145
194
|
const indexedFields = new Set(this.indexes.keys());
|
|
@@ -148,7 +197,6 @@ export class Collection<T = any> {
|
|
|
148
197
|
query,
|
|
149
198
|
{
|
|
150
199
|
indexes: indexedFields,
|
|
151
|
-
|
|
152
200
|
findByIndex: async (field, value) => {
|
|
153
201
|
const idx = this.indexes.get(field);
|
|
154
202
|
if (!idx) return null;
|
|
@@ -165,17 +213,32 @@ export class Collection<T = any> {
|
|
|
165
213
|
);
|
|
166
214
|
}
|
|
167
215
|
|
|
216
|
+
private async _readAndMigrate(id: string) {
|
|
217
|
+
const enc = await this.db.get(id);
|
|
218
|
+
if (!enc) return null;
|
|
219
|
+
|
|
220
|
+
const raw = decryptData(enc);
|
|
221
|
+
const migrated = this.migrateIfNeeded(raw);
|
|
222
|
+
|
|
223
|
+
// Lazy write-back if migrated
|
|
224
|
+
if (raw.__v !== this.schemaVersion) {
|
|
225
|
+
await this.db.put(id, encryptData(migrated));
|
|
226
|
+
await this._updateIndexes(raw, migrated);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return migrated;
|
|
230
|
+
}
|
|
231
|
+
|
|
168
232
|
private async _find(query: any) {
|
|
169
233
|
const ids = await this._getCandidateIds(query);
|
|
170
234
|
const out = [];
|
|
171
235
|
|
|
172
236
|
for (const id of ids) {
|
|
173
237
|
try {
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (matchDocument(doc, query)) out.push(doc);
|
|
238
|
+
const doc = await this._readAndMigrate(id);
|
|
239
|
+
if (doc && matchDocument(doc, query)) {
|
|
240
|
+
out.push(doc);
|
|
241
|
+
}
|
|
179
242
|
} catch {}
|
|
180
243
|
}
|
|
181
244
|
|
|
@@ -185,8 +248,7 @@ export class Collection<T = any> {
|
|
|
185
248
|
private async _findOne(query: any) {
|
|
186
249
|
if (query?._id) {
|
|
187
250
|
try {
|
|
188
|
-
|
|
189
|
-
return enc ? decryptData(enc) : null;
|
|
251
|
+
return await this._readAndMigrate(String(query._id));
|
|
190
252
|
} catch { return null; }
|
|
191
253
|
}
|
|
192
254
|
|
|
@@ -194,11 +256,10 @@ export class Collection<T = any> {
|
|
|
194
256
|
|
|
195
257
|
for (const id of ids) {
|
|
196
258
|
try {
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (matchDocument(doc, query)) return doc;
|
|
259
|
+
const doc = await this._readAndMigrate(id);
|
|
260
|
+
if (doc && matchDocument(doc, query)) {
|
|
261
|
+
return doc;
|
|
262
|
+
}
|
|
202
263
|
} catch {}
|
|
203
264
|
}
|
|
204
265
|
|
|
@@ -211,50 +272,41 @@ export class Collection<T = any> {
|
|
|
211
272
|
|
|
212
273
|
for (const id of ids) {
|
|
213
274
|
try {
|
|
214
|
-
const
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
275
|
+
const doc = await this._readAndMigrate(id);
|
|
276
|
+
if (doc && matchDocument(doc, filter)) {
|
|
277
|
+
count++;
|
|
278
|
+
}
|
|
218
279
|
} catch {}
|
|
219
280
|
}
|
|
220
281
|
|
|
221
282
|
return count;
|
|
222
283
|
}
|
|
223
284
|
|
|
224
|
-
/*
|
|
285
|
+
/* ===================== UPDATE ===================== */
|
|
225
286
|
|
|
226
287
|
private async _updateOne(filter: any, update: any, options: UpdateOptions) {
|
|
227
288
|
const ids = await this._getCandidateIds(filter);
|
|
228
289
|
|
|
229
290
|
for (const id of ids) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (!enc) continue;
|
|
291
|
+
const existing = await this._readAndMigrate(id);
|
|
292
|
+
if (!existing) continue;
|
|
233
293
|
|
|
234
|
-
|
|
294
|
+
if (matchDocument(existing, filter)) {
|
|
295
|
+
const updated = this.validate({
|
|
296
|
+
...applyUpdate(existing, update),
|
|
297
|
+
_id: (existing as any)._id,
|
|
298
|
+
__v: this.schemaVersion
|
|
299
|
+
});
|
|
235
300
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
updated._id = value._id;
|
|
301
|
+
await this.db.put(id, encryptData(updated));
|
|
302
|
+
await this._updateIndexes(existing, updated);
|
|
239
303
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return updated;
|
|
244
|
-
}
|
|
245
|
-
} catch {}
|
|
304
|
+
return updated;
|
|
305
|
+
}
|
|
246
306
|
}
|
|
247
307
|
|
|
248
308
|
if (options?.upsert) {
|
|
249
|
-
|
|
250
|
-
_id: uuid(),
|
|
251
|
-
...applyUpdate({}, update)
|
|
252
|
-
}) as any;
|
|
253
|
-
|
|
254
|
-
await this.db.put(String(doc._id), encryptData(doc));
|
|
255
|
-
await this._updateIndexes(null, doc);
|
|
256
|
-
|
|
257
|
-
return doc;
|
|
309
|
+
return this._insertOne(applyUpdate({}, update));
|
|
258
310
|
}
|
|
259
311
|
|
|
260
312
|
return null;
|
|
@@ -265,45 +317,40 @@ export class Collection<T = any> {
|
|
|
265
317
|
const out = [];
|
|
266
318
|
|
|
267
319
|
for (const id of ids) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (!enc) continue;
|
|
271
|
-
|
|
272
|
-
const value = decryptData(enc);
|
|
320
|
+
const existing = await this._readAndMigrate(id);
|
|
321
|
+
if (!existing) continue;
|
|
273
322
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
323
|
+
if (matchDocument(existing, filter)) {
|
|
324
|
+
const updated = this.validate({
|
|
325
|
+
...applyUpdate(existing, update),
|
|
326
|
+
_id: (existing as any)._id,
|
|
327
|
+
__v: this.schemaVersion
|
|
328
|
+
});
|
|
277
329
|
|
|
278
|
-
|
|
279
|
-
|
|
330
|
+
await this.db.put(id, encryptData(updated));
|
|
331
|
+
await this._updateIndexes(existing, updated);
|
|
280
332
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
} catch {}
|
|
333
|
+
out.push(updated);
|
|
334
|
+
}
|
|
284
335
|
}
|
|
285
336
|
|
|
286
337
|
return out;
|
|
287
338
|
}
|
|
288
339
|
|
|
289
|
-
/*
|
|
340
|
+
/* ===================== DELETE ===================== */
|
|
290
341
|
|
|
291
342
|
private async _deleteOne(filter: any) {
|
|
292
343
|
const ids = await this._getCandidateIds(filter);
|
|
293
344
|
|
|
294
345
|
for (const id of ids) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (!enc) continue;
|
|
298
|
-
|
|
299
|
-
const value = decryptData(enc);
|
|
346
|
+
const existing = await this._readAndMigrate(id);
|
|
347
|
+
if (!existing) continue;
|
|
300
348
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
} catch {}
|
|
349
|
+
if (matchDocument(existing, filter)) {
|
|
350
|
+
await this.db.del(id);
|
|
351
|
+
await this._updateIndexes(existing, null);
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
307
354
|
}
|
|
308
355
|
|
|
309
356
|
return false;
|
|
@@ -314,24 +361,20 @@ export class Collection<T = any> {
|
|
|
314
361
|
let count = 0;
|
|
315
362
|
|
|
316
363
|
for (const id of ids) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (!enc) continue;
|
|
364
|
+
const existing = await this._readAndMigrate(id);
|
|
365
|
+
if (!existing) continue;
|
|
320
366
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
count++;
|
|
327
|
-
}
|
|
328
|
-
} catch {}
|
|
367
|
+
if (matchDocument(existing, filter)) {
|
|
368
|
+
await this.db.del(id);
|
|
369
|
+
await this._updateIndexes(existing, null);
|
|
370
|
+
count++;
|
|
371
|
+
}
|
|
329
372
|
}
|
|
330
373
|
|
|
331
374
|
return count;
|
|
332
375
|
}
|
|
333
376
|
|
|
334
|
-
/*
|
|
377
|
+
/* ===================== PUBLIC API ===================== */
|
|
335
378
|
|
|
336
379
|
insertOne(doc: any) {
|
|
337
380
|
return this._enqueue(() => this._exec("insertOne", [doc]));
|
package/src/core/database.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { Index, IndexOptions } from "./index.js";
|
|
|
7
7
|
import { MigrationEngine } from "./migration.js";
|
|
8
8
|
import type { LioranManager } from "../LioranManager.js";
|
|
9
9
|
import type { ZodSchema } from "zod";
|
|
10
|
+
import { decryptData } from "../utils/encryption.js";
|
|
10
11
|
|
|
11
12
|
const exec = promisify(execFile);
|
|
12
13
|
|
|
@@ -25,7 +26,7 @@ type IndexMeta = {
|
|
|
25
26
|
type DBMeta = {
|
|
26
27
|
version: number;
|
|
27
28
|
indexes: Record<string, IndexMeta[]>;
|
|
28
|
-
schemaVersion: string;
|
|
29
|
+
schemaVersion: string; // DB-level schema (not collection schema)
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
const META_FILE = "__db_meta.json";
|
|
@@ -40,7 +41,7 @@ class DBTransactionContext {
|
|
|
40
41
|
constructor(
|
|
41
42
|
private db: LioranDB,
|
|
42
43
|
public readonly txId: number
|
|
43
|
-
) {
|
|
44
|
+
) {}
|
|
44
45
|
|
|
45
46
|
collection(name: string) {
|
|
46
47
|
return new Proxy({}, {
|
|
@@ -79,7 +80,6 @@ export class LioranDB {
|
|
|
79
80
|
private meta!: DBMeta;
|
|
80
81
|
|
|
81
82
|
private migrator: MigrationEngine;
|
|
82
|
-
|
|
83
83
|
private static TX_SEQ = 0;
|
|
84
84
|
|
|
85
85
|
constructor(basePath: string, dbName: string, manager: LioranManager) {
|
|
@@ -133,7 +133,7 @@ export class LioranDB {
|
|
|
133
133
|
this.saveMeta();
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
/* -------------------------
|
|
136
|
+
/* ------------------------- DB MIGRATIONS ------------------------- */
|
|
137
137
|
|
|
138
138
|
migrate(from: string, to: string, fn: (db: LioranDB) => Promise<void>) {
|
|
139
139
|
this.migrator.register(from, to, async db => {
|
|
@@ -158,7 +158,7 @@ export class LioranDB {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
async clearWAL() {
|
|
161
|
-
try { await fs.promises.unlink(this.walPath); } catch {
|
|
161
|
+
try { await fs.promises.unlink(this.walPath); } catch {}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
private async recoverFromWAL() {
|
|
@@ -172,7 +172,6 @@ export class LioranDB {
|
|
|
172
172
|
|
|
173
173
|
for (const line of raw.split("\n")) {
|
|
174
174
|
if (!line.trim()) continue;
|
|
175
|
-
|
|
176
175
|
const entry: WALEntry = JSON.parse(line);
|
|
177
176
|
|
|
178
177
|
if ("commit" in entry) committed.add(entry.tx);
|
|
@@ -201,17 +200,27 @@ export class LioranDB {
|
|
|
201
200
|
|
|
202
201
|
/* ------------------------- COLLECTION ------------------------- */
|
|
203
202
|
|
|
204
|
-
collection<T = any>(
|
|
203
|
+
collection<T = any>(
|
|
204
|
+
name: string,
|
|
205
|
+
schema?: ZodSchema<T>,
|
|
206
|
+
schemaVersion?: number
|
|
207
|
+
): Collection<T> {
|
|
205
208
|
if (this.collections.has(name)) {
|
|
206
209
|
const col = this.collections.get(name)!;
|
|
207
|
-
if (schema)
|
|
210
|
+
if (schema && schemaVersion !== undefined) {
|
|
211
|
+
col.setSchema(schema, schemaVersion);
|
|
212
|
+
}
|
|
208
213
|
return col as Collection<T>;
|
|
209
214
|
}
|
|
210
215
|
|
|
211
216
|
const colPath = path.join(this.basePath, name);
|
|
212
217
|
fs.mkdirSync(colPath, { recursive: true });
|
|
213
218
|
|
|
214
|
-
const col = new Collection<T>(
|
|
219
|
+
const col = new Collection<T>(
|
|
220
|
+
colPath,
|
|
221
|
+
schema,
|
|
222
|
+
schemaVersion ?? 1
|
|
223
|
+
);
|
|
215
224
|
|
|
216
225
|
const metas = this.meta.indexes[name] ?? [];
|
|
217
226
|
for (const m of metas) {
|
|
@@ -236,25 +245,14 @@ export class LioranDB {
|
|
|
236
245
|
|
|
237
246
|
const index = new Index(col.dir, field, options);
|
|
238
247
|
|
|
239
|
-
// for await (const [, enc] of col.db.iterator()) {
|
|
240
|
-
// // const doc = JSON.parse(
|
|
241
|
-
// // Buffer.from(enc, "base64").subarray(32).toString("utf8")
|
|
242
|
-
// // );
|
|
243
|
-
// const payload = Buffer.from(enc, "utf8").subarray(32);
|
|
244
|
-
// const doc = JSON.parse(payload.toString("utf8"));
|
|
245
|
-
// await index.insert(doc);
|
|
246
|
-
// }
|
|
247
|
-
|
|
248
248
|
for await (const [key, enc] of col.db.iterator()) {
|
|
249
249
|
if (!enc) continue;
|
|
250
|
-
|
|
251
250
|
try {
|
|
252
|
-
const doc = decryptData(enc);
|
|
251
|
+
const doc = decryptData(enc);
|
|
253
252
|
await index.insert(doc);
|
|
254
253
|
} catch (err) {
|
|
255
|
-
const
|
|
256
|
-
console.warn(`
|
|
257
|
-
// You can continue, or collect bad keys for later inspection
|
|
254
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
255
|
+
console.warn(`Index build skipped doc ${key}: ${msg}`);
|
|
258
256
|
}
|
|
259
257
|
}
|
|
260
258
|
|
|
@@ -297,12 +295,8 @@ export class LioranDB {
|
|
|
297
295
|
|
|
298
296
|
async close(): Promise<void> {
|
|
299
297
|
for (const col of this.collections.values()) {
|
|
300
|
-
try { await col.close(); } catch {
|
|
298
|
+
try { await col.close(); } catch {}
|
|
301
299
|
}
|
|
302
300
|
this.collections.clear();
|
|
303
301
|
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function decryptData(enc: string) {
|
|
307
|
-
throw new Error("Function not implemented.");
|
|
308
|
-
}
|
|
302
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -1,63 +1,136 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* ============================= MANAGER OPTIONS ============================= */
|
|
2
2
|
|
|
3
3
|
export interface LioranManagerOptions {
|
|
4
|
-
rootPath?: string
|
|
5
|
-
encryptionKey?: string | Buffer
|
|
6
|
-
ipc?: boolean
|
|
4
|
+
rootPath?: string;
|
|
5
|
+
encryptionKey?: string | Buffer;
|
|
6
|
+
ipc?: boolean;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* If true, database auto-applies pending migrations on startup.
|
|
10
|
+
*/
|
|
11
|
+
autoMigrate?: boolean;
|
|
7
12
|
}
|
|
8
13
|
|
|
9
|
-
/*
|
|
14
|
+
/* ============================= UPDATE OPTIONS ============================= */
|
|
10
15
|
|
|
11
16
|
export interface UpdateOptions {
|
|
12
|
-
upsert?: boolean
|
|
17
|
+
upsert?: boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* If true, returns the modified document instead of the original.
|
|
21
|
+
*/
|
|
22
|
+
returnNew?: boolean;
|
|
13
23
|
}
|
|
14
24
|
|
|
15
|
-
/*
|
|
25
|
+
/* ================================ QUERY =================================== */
|
|
16
26
|
|
|
17
|
-
export type Query<T = any> =
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
export type Query<T = any> =
|
|
28
|
+
| Partial<T>
|
|
29
|
+
| {
|
|
30
|
+
[K in keyof T]?: any;
|
|
31
|
+
} & {
|
|
32
|
+
[key: string]: any;
|
|
33
|
+
};
|
|
20
34
|
|
|
21
|
-
/*
|
|
35
|
+
/* ================================ INDEX =================================== */
|
|
22
36
|
|
|
23
|
-
export type IndexType = "hash" | "btree"
|
|
37
|
+
export type IndexType = "hash" | "btree";
|
|
24
38
|
|
|
25
39
|
export interface IndexDefinition<T = any> {
|
|
26
|
-
field: keyof T | string
|
|
27
|
-
unique?: boolean
|
|
28
|
-
sparse?: boolean
|
|
29
|
-
type?: IndexType
|
|
40
|
+
field: keyof T | string;
|
|
41
|
+
unique?: boolean;
|
|
42
|
+
sparse?: boolean;
|
|
43
|
+
type?: IndexType;
|
|
30
44
|
}
|
|
31
45
|
|
|
32
46
|
export interface IndexMetadata {
|
|
33
|
-
field: string
|
|
34
|
-
unique: boolean
|
|
35
|
-
sparse: boolean
|
|
36
|
-
type: IndexType
|
|
37
|
-
createdAt: number
|
|
47
|
+
field: string;
|
|
48
|
+
unique: boolean;
|
|
49
|
+
sparse: boolean;
|
|
50
|
+
type: IndexType;
|
|
51
|
+
createdAt: number;
|
|
38
52
|
}
|
|
39
53
|
|
|
40
|
-
/*
|
|
54
|
+
/* =========================== QUERY PLANNER ================================ */
|
|
41
55
|
|
|
42
56
|
export interface QueryExplainPlan {
|
|
43
|
-
indexUsed?: string
|
|
44
|
-
indexType?: IndexType
|
|
45
|
-
scannedDocuments: number
|
|
46
|
-
returnedDocuments: number
|
|
47
|
-
executionTimeMs: number
|
|
57
|
+
indexUsed?: string;
|
|
58
|
+
indexType?: IndexType;
|
|
59
|
+
scannedDocuments: number;
|
|
60
|
+
returnedDocuments: number;
|
|
61
|
+
executionTimeMs: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ========================== SCHEMA VERSIONING ============================= */
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Per-collection document schema version
|
|
68
|
+
*/
|
|
69
|
+
export type SchemaVersion = number;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Collection-level migration definition
|
|
73
|
+
*/
|
|
74
|
+
export interface CollectionMigration<T = any> {
|
|
75
|
+
from: SchemaVersion;
|
|
76
|
+
to: SchemaVersion;
|
|
77
|
+
migrate: (doc: any) => T;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Database-level migration definition
|
|
82
|
+
*/
|
|
83
|
+
export interface DatabaseMigration {
|
|
84
|
+
from: string;
|
|
85
|
+
to: string;
|
|
86
|
+
migrate: () => Promise<void>;
|
|
48
87
|
}
|
|
49
88
|
|
|
50
|
-
/*
|
|
89
|
+
/* ============================== COLLECTION ================================ */
|
|
90
|
+
|
|
91
|
+
export interface CollectionOptions<T = any> {
|
|
92
|
+
/**
|
|
93
|
+
* Zod schema used for validation
|
|
94
|
+
*/
|
|
95
|
+
schema?: any;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Current document schema version
|
|
99
|
+
*/
|
|
100
|
+
schemaVersion?: SchemaVersion;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Optional migrations for automatic document upgrading
|
|
104
|
+
*/
|
|
105
|
+
migrations?: CollectionMigration<T>[];
|
|
106
|
+
}
|
|
51
107
|
|
|
52
108
|
export interface CollectionIndexAPI<T = any> {
|
|
53
|
-
createIndex(def: IndexDefinition<T>): Promise<void
|
|
54
|
-
dropIndex(field: keyof T | string): Promise<void
|
|
55
|
-
listIndexes(): Promise<IndexMetadata[]
|
|
56
|
-
rebuildIndexes(): Promise<void
|
|
109
|
+
createIndex(def: IndexDefinition<T>): Promise<void>;
|
|
110
|
+
dropIndex(field: keyof T | string): Promise<void>;
|
|
111
|
+
listIndexes(): Promise<IndexMetadata[]>;
|
|
112
|
+
rebuildIndexes(): Promise<void>;
|
|
57
113
|
}
|
|
58
114
|
|
|
59
|
-
/*
|
|
115
|
+
/* =============================== DATABASE ================================= */
|
|
60
116
|
|
|
61
117
|
export interface DatabaseIndexAPI {
|
|
62
|
-
rebuildAllIndexes(): Promise<void
|
|
118
|
+
rebuildAllIndexes(): Promise<void>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Database migration coordination API
|
|
123
|
+
*/
|
|
124
|
+
export interface DatabaseMigrationAPI {
|
|
125
|
+
migrate(
|
|
126
|
+
from: string,
|
|
127
|
+
to: string,
|
|
128
|
+
fn: () => Promise<void>
|
|
129
|
+
): void;
|
|
130
|
+
|
|
131
|
+
applyMigrations(targetVersion: string): Promise<void>;
|
|
132
|
+
|
|
133
|
+
getSchemaVersion(): string;
|
|
134
|
+
|
|
135
|
+
setSchemaVersion(version: string): void;
|
|
63
136
|
}
|