@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.
- package/dist/index.d.ts +87 -19
- package/dist/index.js +335 -180
- package/package.json +1 -1
- package/src/core/collection.ts +150 -107
- package/src/core/database.ts +64 -56
- package/src/core/migration.store.ts +40 -0
- package/src/core/migration.ts +162 -0
- package/src/core/migration.types.ts +22 -0
- package/src/types/index.ts +107 -34
|
@@ -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
|
+
}
|
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
|
}
|