@liorandb/core 1.0.16 → 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.
@@ -0,0 +1,162 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+ import type { LioranDB } from "./database.js";
5
+
6
+ export type MigrationFn = (db: LioranDB) => Promise<void>;
7
+
8
+ type MigrationRecord = {
9
+ from: string;
10
+ to: string;
11
+ checksum: string;
12
+ appliedAt: number;
13
+ };
14
+
15
+ const LOCK_FILE = "__migration.lock";
16
+ const HISTORY_FILE = "__migration_history.json";
17
+
18
+ export class MigrationEngine {
19
+ private migrations = new Map<string, MigrationFn>();
20
+
21
+ constructor(private db: LioranDB) {}
22
+
23
+ /* ------------------------------------------------------------ */
24
+ /* Public API */
25
+ /* ------------------------------------------------------------ */
26
+
27
+ register(from: string, to: string, fn: MigrationFn) {
28
+ const key = `${from}→${to}`;
29
+
30
+ if (this.migrations.has(key)) {
31
+ throw new Error(`Duplicate migration: ${key}`);
32
+ }
33
+
34
+ this.migrations.set(key, fn);
35
+ }
36
+
37
+ async migrate(from: string, to: string, fn: MigrationFn) {
38
+ this.register(from, to, fn);
39
+ await this.execute();
40
+ }
41
+
42
+ async upgradeToLatest() {
43
+ await this.execute();
44
+ }
45
+
46
+ /* ------------------------------------------------------------ */
47
+ /* Core Execution Logic */
48
+ /* ------------------------------------------------------------ */
49
+
50
+ private async execute() {
51
+ let current = this.db.getSchemaVersion();
52
+
53
+ while (true) {
54
+ const next = this.findNext(current);
55
+ if (!next) break;
56
+
57
+ const fn = this.migrations.get(`${current}→${next}`)!;
58
+ await this.runMigration(current, next, fn);
59
+ current = next;
60
+ }
61
+ }
62
+
63
+ private findNext(current: string): string | null {
64
+ for (const key of this.migrations.keys()) {
65
+ const [from, to] = key.split("→");
66
+ if (from === current) return to;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ /* ------------------------------------------------------------ */
72
+ /* Atomic Migration Execution */
73
+ /* ------------------------------------------------------------ */
74
+
75
+ private async runMigration(from: string, to: string, fn: MigrationFn) {
76
+ const current = this.db.getSchemaVersion();
77
+ if (current !== from) {
78
+ throw new Error(
79
+ `Schema mismatch: DB=${current}, expected=${from}`
80
+ );
81
+ }
82
+
83
+ const lockPath = path.join(this.db.basePath, LOCK_FILE);
84
+
85
+ if (fs.existsSync(lockPath)) {
86
+ throw new Error(
87
+ "Previous migration interrupted. Resolve manually before continuing."
88
+ );
89
+ }
90
+
91
+ this.acquireLock(lockPath);
92
+
93
+ try {
94
+ await this.db.transaction(async () => {
95
+ await fn(this.db);
96
+ this.writeHistory(from, to, fn);
97
+ this.db.setSchemaVersion(to);
98
+ });
99
+ } finally {
100
+ this.releaseLock(lockPath);
101
+ }
102
+ }
103
+
104
+ /* ------------------------------------------------------------ */
105
+ /* Locking */
106
+ /* ------------------------------------------------------------ */
107
+
108
+ private acquireLock(file: string) {
109
+ const token = crypto.randomBytes(16).toString("hex");
110
+
111
+ fs.writeFileSync(
112
+ file,
113
+ JSON.stringify({
114
+ pid: process.pid,
115
+ token,
116
+ time: Date.now(),
117
+ })
118
+ );
119
+ }
120
+
121
+ private releaseLock(file: string) {
122
+ if (fs.existsSync(file)) fs.unlinkSync(file);
123
+ }
124
+
125
+ /* ------------------------------------------------------------ */
126
+ /* Migration History */
127
+ /* ------------------------------------------------------------ */
128
+
129
+ private historyPath() {
130
+ return path.join(this.db.basePath, HISTORY_FILE);
131
+ }
132
+
133
+ private readHistory(): MigrationRecord[] {
134
+ if (!fs.existsSync(this.historyPath())) return [];
135
+ return JSON.parse(fs.readFileSync(this.historyPath(), "utf8"));
136
+ }
137
+
138
+ private writeHistory(from: string, to: string, fn: MigrationFn) {
139
+ const history = this.readHistory();
140
+
141
+ history.push({
142
+ from,
143
+ to,
144
+ checksum: this.hash(fn.toString()),
145
+ appliedAt: Date.now(),
146
+ });
147
+
148
+ fs.writeFileSync(this.historyPath(), JSON.stringify(history, null, 2));
149
+ }
150
+
151
+ private hash(data: string) {
152
+ return crypto.createHash("sha256").update(data).digest("hex");
153
+ }
154
+
155
+ /* ------------------------------------------------------------ */
156
+ /* Diagnostics */
157
+ /* ------------------------------------------------------------ */
158
+
159
+ getHistory(): MigrationRecord[] {
160
+ return this.readHistory();
161
+ }
162
+ }
@@ -0,0 +1,22 @@
1
+ import type { LioranDB } from "./database.js";
2
+
3
+ export type MigrationVersion = string;
4
+
5
+ export type MigrationFn = (db: LioranDB) => Promise<void>;
6
+
7
+ export interface Migration {
8
+ from: MigrationVersion;
9
+ to: MigrationVersion;
10
+ run: MigrationFn;
11
+ }
12
+
13
+ export interface MigrationRecord {
14
+ version: MigrationVersion;
15
+ appliedAt: number;
16
+ }
17
+
18
+ export interface MigrationMeta {
19
+ id?: string;
20
+ currentVersion: MigrationVersion;
21
+ history: MigrationRecord[];
22
+ }
@@ -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
  }