@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 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
- registerIndex(index: Index): void;
35
- getIndex(field: string): Index | undefined;
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
- compact(): Promise<void>;
41
- _exec(op: string, args: any[]): Promise<any>;
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<any>;
54
- insertMany(docs: any[]): Promise<any>;
55
- find(query?: any): Promise<any>;
56
- findOne(query?: any): Promise<any>;
57
- updateOne(filter: any, update: any, options?: UpdateOptions$1): Promise<any>;
58
- updateMany(filter: any, update: any): Promise<any>;
59
- deleteOne(filter: any): Promise<any>;
60
- deleteMany(filter: any): Promise<any>;
61
- countDocuments(filter?: any): Promise<any>;
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>): Collection<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
- /* ---------------------- INDEX MANAGEMENT ---------------------- */
380
- registerIndex(index) {
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
- /* -------------------- COMPACTION ENGINE -------------------- */
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
- /* ------------------ INDEX HOOK ------------------ */
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({ _id, ...doc });
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({ _id, ...d });
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
- /* ---------------- QUERY ENGINE (INDEXED) ---------------- */
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 enc = await this.db.get(id);
506
- if (!enc) continue;
507
- const doc = decryptData(enc);
508
- if (matchDocument(doc, query)) out.push(doc);
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
- const enc = await this.db.get(String(query._id));
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 enc = await this.db.get(id);
527
- if (!enc) continue;
528
- const doc = decryptData(enc);
529
- if (matchDocument(doc, query)) return doc;
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 enc = await this.db.get(id);
541
- if (!enc) continue;
542
- if (matchDocument(decryptData(enc), filter)) count++;
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
- /* ---------------- UPDATE ---------------- */
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
- try {
553
- const enc = await this.db.get(id);
554
- if (!enc) continue;
555
- const value = decryptData(enc);
556
- if (matchDocument(value, filter)) {
557
- const updated = this.validate(applyUpdate(value, update));
558
- updated._id = value._id;
559
- await this.db.put(id, encryptData(updated));
560
- await this._updateIndexes(value, updated);
561
- return updated;
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
- const doc = this.validate({
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
- try {
582
- const enc = await this.db.get(id);
583
- if (!enc) continue;
584
- const value = decryptData(enc);
585
- if (matchDocument(value, filter)) {
586
- const updated = this.validate(applyUpdate(value, update));
587
- updated._id = value._id;
588
- await this.db.put(id, encryptData(updated));
589
- await this._updateIndexes(value, updated);
590
- out.push(updated);
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
- /* ---------------- DELETE ---------------- */
632
+ /* ===================== DELETE ===================== */
598
633
  async _deleteOne(filter) {
599
634
  const ids = await this._getCandidateIds(filter);
600
635
  for (const id of ids) {
601
- try {
602
- const enc = await this.db.get(id);
603
- if (!enc) continue;
604
- const value = decryptData(enc);
605
- if (matchDocument(value, filter)) {
606
- await this.db.del(id);
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
- try {
620
- const enc = await this.db.get(id);
621
- if (!enc) continue;
622
- const value = decryptData(enc);
623
- if (matchDocument(value, filter)) {
624
- await this.db.del(id);
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
- /* ---------------- PUBLIC API (Mongo-style) ---------------- */
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
- /* ------------------------- MIGRATION API ------------------------- */
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) col2.setSchema(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(colPath, schema);
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 = decryptData2(enc);
991
+ const doc = decryptData(enc);
959
992
  await index.insert(doc);
960
993
  } catch (err) {
961
- const errorMessage = err instanceof Error ? err.message : String(err);
962
- console.warn(`Could not decrypt document ${key} during index build: ${errorMessage}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liorandb/core",
3
- "version": "1.0.17",
3
+ "version": "1.0.18",
4
4
  "description": "LioranDB Core Module – Lightweight, local-first, peer-to-peer database management for Node.js.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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(dir: string, schema?: ZodSchema<T>) {
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
- /* ---------------------- INDEX MANAGEMENT ---------------------- */
48
+ /* ===================== SCHEMA ===================== */
32
49
 
33
- registerIndex(index: Index) {
34
- this.indexes.set(index.field, index);
50
+ setSchema(schema: ZodSchema<T>, version: number) {
51
+ this.schema = schema;
52
+ this.schemaVersion = version;
35
53
  }
36
54
 
37
- getIndex(field: string) {
38
- return this.indexes.get(field);
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
- /* -------------------- COMPACTION ENGINE -------------------- */
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
- /* ------------------ INDEX HOOK ------------------ */
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({ _id, ...doc });
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: Array<{ type: "put"; key: string; value: string }> = [];
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({ _id, ...d });
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
- /* ---------------- QUERY ENGINE (INDEXED) ---------------- */
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 enc = await this.db.get(id);
175
- if (!enc) continue;
176
-
177
- const doc = decryptData(enc);
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
- const enc = await this.db.get(String(query._id));
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 enc = await this.db.get(id);
198
- if (!enc) continue;
199
-
200
- const doc = decryptData(enc);
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 enc = await this.db.get(id);
215
- if (!enc) continue;
216
-
217
- if (matchDocument(decryptData(enc), filter)) count++;
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
- /* ---------------- UPDATE ---------------- */
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
- try {
231
- const enc = await this.db.get(id);
232
- if (!enc) continue;
291
+ const existing = await this._readAndMigrate(id);
292
+ if (!existing) continue;
233
293
 
234
- const value = decryptData(enc);
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
- if (matchDocument(value, filter)) {
237
- const updated = this.validate(applyUpdate(value, update)) as any;
238
- updated._id = value._id;
301
+ await this.db.put(id, encryptData(updated));
302
+ await this._updateIndexes(existing, updated);
239
303
 
240
- await this.db.put(id, encryptData(updated));
241
- await this._updateIndexes(value, updated);
242
-
243
- return updated;
244
- }
245
- } catch {}
304
+ return updated;
305
+ }
246
306
  }
247
307
 
248
308
  if (options?.upsert) {
249
- const doc = this.validate({
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
- try {
269
- const enc = await this.db.get(id);
270
- if (!enc) continue;
271
-
272
- const value = decryptData(enc);
320
+ const existing = await this._readAndMigrate(id);
321
+ if (!existing) continue;
273
322
 
274
- if (matchDocument(value, filter)) {
275
- const updated = this.validate(applyUpdate(value, update)) as any;
276
- updated._id = value._id;
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
- await this.db.put(id, encryptData(updated));
279
- await this._updateIndexes(value, updated);
330
+ await this.db.put(id, encryptData(updated));
331
+ await this._updateIndexes(existing, updated);
280
332
 
281
- out.push(updated);
282
- }
283
- } catch {}
333
+ out.push(updated);
334
+ }
284
335
  }
285
336
 
286
337
  return out;
287
338
  }
288
339
 
289
- /* ---------------- DELETE ---------------- */
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
- try {
296
- const enc = await this.db.get(id);
297
- if (!enc) continue;
298
-
299
- const value = decryptData(enc);
346
+ const existing = await this._readAndMigrate(id);
347
+ if (!existing) continue;
300
348
 
301
- if (matchDocument(value, filter)) {
302
- await this.db.del(id);
303
- await this._updateIndexes(value, null);
304
- return true;
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
- try {
318
- const enc = await this.db.get(id);
319
- if (!enc) continue;
364
+ const existing = await this._readAndMigrate(id);
365
+ if (!existing) continue;
320
366
 
321
- const value = decryptData(enc);
322
-
323
- if (matchDocument(value, filter)) {
324
- await this.db.del(id);
325
- await this._updateIndexes(value, null);
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
- /* ---------------- PUBLIC API (Mongo-style) ---------------- */
377
+ /* ===================== PUBLIC API ===================== */
335
378
 
336
379
  insertOne(doc: any) {
337
380
  return this._enqueue(() => this._exec("insertOne", [doc]));
@@ -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
- /* ------------------------- MIGRATION API ------------------------- */
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>(name: string, schema?: ZodSchema<T>): Collection<T> {
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) col.setSchema(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>(colPath, schema);
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); // ← this does base64 → AES-GCM → JSON
251
+ const doc = decryptData(enc);
253
252
  await index.insert(doc);
254
253
  } catch (err) {
255
- const errorMessage = err instanceof Error ? err.message : String(err);
256
- console.warn(`Could not decrypt document ${key} during index build: ${errorMessage}`);
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
+ }
@@ -1,63 +1,136 @@
1
- /* ----------------------------- MANAGER OPTIONS ----------------------------- */
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
- /* ----------------------------- UPDATE OPTIONS ----------------------------- */
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
- /* --------------------------------- QUERY --------------------------------- */
25
+ /* ================================ QUERY =================================== */
16
26
 
17
- export type Query<T = any> = Partial<T> & {
18
- [key: string]: any
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
- /* --------------------------------- INDEX --------------------------------- */
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
- /* ----------------------------- QUERY PLANNER ------------------------------ */
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
- /* ------------------------------ COLLECTION -------------------------------- */
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
- /* ------------------------------- DATABASE --------------------------------- */
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
  }