@liorandb/core 1.0.4 → 1.0.6

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,163 @@
1
+ import { ClassicLevel } from "classic-level";
2
+ import { matchDocument, applyUpdate } from "./query.js";
3
+ import { v4 as uuid } from "uuid";
4
+ import { encryptData, decryptData } from "../utils/encryption.js";
5
+
6
+ export interface UpdateOptions {
7
+ upsert?: boolean;
8
+ }
9
+
10
+ export class Collection<T = any> {
11
+ dir: string;
12
+ db: ClassicLevel<string, string>;
13
+ private queue: Promise<any>;
14
+
15
+ constructor(dir: string) {
16
+ this.dir = dir;
17
+ this.db = new ClassicLevel(dir);
18
+ this.queue = Promise.resolve();
19
+ }
20
+
21
+ private _enqueue<R>(task: () => Promise<R>): Promise<R> {
22
+ this.queue = this.queue.then(task).catch(console.error);
23
+ return this.queue;
24
+ }
25
+
26
+ async close(): Promise<void> {
27
+ try {
28
+ await this.db.close();
29
+ } catch {}
30
+ }
31
+
32
+ async insertOne(doc: T & { _id?: string }): Promise<T> {
33
+ return this._enqueue(async () => {
34
+ const _id = doc._id ?? uuid();
35
+ const final = { _id, ...doc } as T;
36
+ await this.db.put(String(_id), encryptData(final));
37
+ return final;
38
+ });
39
+ }
40
+
41
+ async insertMany(docs: (T & { _id?: string })[] = []): Promise<T[]> {
42
+ return this._enqueue(async () => {
43
+ const ops: Array<{ type: "put"; key: string; value: string }> = [];
44
+ const out: T[] = [];
45
+
46
+ for (const d of docs) {
47
+ const _id = d._id ?? uuid();
48
+ const final = { _id, ...d } as T;
49
+ ops.push({
50
+ type: "put",
51
+ key: String(_id),
52
+ value: encryptData(final)
53
+ });
54
+ out.push(final);
55
+ }
56
+
57
+ await this.db.batch(ops);
58
+ return out;
59
+ });
60
+ }
61
+
62
+ async find(query: any = {}): Promise<T[]> {
63
+ return this._enqueue(async () => {
64
+ const out: T[] = [];
65
+ for await (const [, enc] of this.db.iterator()) {
66
+ const value = decryptData(enc);
67
+ if (matchDocument(value, query)) out.push(value);
68
+ }
69
+ return out;
70
+ });
71
+ }
72
+
73
+ async findOne(query: any = {}): Promise<T | null> {
74
+ return this._enqueue(async () => {
75
+ for await (const [, enc] of this.db.iterator()) {
76
+ const value = decryptData(enc);
77
+ if (matchDocument(value, query)) return value;
78
+ }
79
+ return null;
80
+ });
81
+ }
82
+
83
+ async updateOne(
84
+ filter: any = {},
85
+ update: any = {},
86
+ options: UpdateOptions = { upsert: false }
87
+ ): Promise<T | null> {
88
+ return this._enqueue(async () => {
89
+ for await (const [key, enc] of this.db.iterator()) {
90
+ const value = decryptData(enc);
91
+ if (matchDocument(value, filter)) {
92
+ const updated = applyUpdate(value, update);
93
+ updated._id = value._id;
94
+ await this.db.put(key, encryptData(updated));
95
+ return updated;
96
+ }
97
+ }
98
+
99
+ if (options.upsert) {
100
+ const doc = applyUpdate(filter, update);
101
+ doc._id ??= uuid();
102
+ await this.db.put(String(doc._id), encryptData(doc));
103
+ return doc;
104
+ }
105
+
106
+ return null;
107
+ });
108
+ }
109
+
110
+ async updateMany(filter: any = {}, update: any = {}): Promise<T[]> {
111
+ return this._enqueue(async () => {
112
+ const updated: T[] = [];
113
+ for await (const [key, enc] of this.db.iterator()) {
114
+ const value = decryptData(enc);
115
+ if (matchDocument(value, filter)) {
116
+ const doc = applyUpdate(value, update);
117
+ doc._id = value._id;
118
+ await this.db.put(key, encryptData(doc));
119
+ updated.push(doc);
120
+ }
121
+ }
122
+ return updated;
123
+ });
124
+ }
125
+
126
+ async deleteOne(filter: any = {}): Promise<boolean> {
127
+ return this._enqueue(async () => {
128
+ for await (const [key, enc] of this.db.iterator()) {
129
+ const value = decryptData(enc);
130
+ if (matchDocument(value, filter)) {
131
+ await this.db.del(key);
132
+ return true;
133
+ }
134
+ }
135
+ return false;
136
+ });
137
+ }
138
+
139
+ async deleteMany(filter: any = {}): Promise<number> {
140
+ return this._enqueue(async () => {
141
+ let count = 0;
142
+ for await (const [key, enc] of this.db.iterator()) {
143
+ const value = decryptData(enc);
144
+ if (matchDocument(value, filter)) {
145
+ await this.db.del(key);
146
+ count++;
147
+ }
148
+ }
149
+ return count;
150
+ });
151
+ }
152
+
153
+ async countDocuments(filter: any = {}): Promise<number> {
154
+ return this._enqueue(async () => {
155
+ let c = 0;
156
+ for await (const [, enc] of this.db.iterator()) {
157
+ const value = decryptData(enc);
158
+ if (matchDocument(value, filter)) c++;
159
+ }
160
+ return c;
161
+ });
162
+ }
163
+ }
@@ -1,9 +1,15 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
3
  import { Collection } from "./collection.js";
4
+ import type { LioranManager } from "../LioranManager.js";
4
5
 
5
6
  export class LioranDB {
6
- constructor(basePath, dbName, manager) {
7
+ basePath: string;
8
+ dbName: string;
9
+ manager: LioranManager;
10
+ collections: Map<string, Collection>;
11
+
12
+ constructor(basePath: string, dbName: string, manager: LioranManager) {
7
13
  this.basePath = basePath;
8
14
  this.dbName = dbName;
9
15
  this.manager = manager;
@@ -14,66 +20,51 @@ export class LioranDB {
14
20
  }
15
21
  }
16
22
 
17
- /** Get or auto-create collection object */
18
- collection(name) {
23
+ collection<T = any>(name: string): Collection<T> {
19
24
  if (this.collections.has(name)) {
20
- return this.collections.get(name);
25
+ return this.collections.get(name)!;
21
26
  }
22
27
 
23
28
  const colPath = path.join(this.basePath, name);
24
29
 
25
- // auto-create directory
26
30
  if (!fs.existsSync(colPath)) {
27
31
  fs.mkdirSync(colPath, { recursive: true });
28
32
  }
29
33
 
30
- const col = new Collection(colPath);
34
+ const col = new Collection<T>(colPath);
31
35
  this.collections.set(name, col);
32
36
  return col;
33
37
  }
34
38
 
35
- /** Create collection manually */
36
- async createCollection(name) {
39
+ async createCollection(name: string): Promise<boolean> {
37
40
  const colPath = path.join(this.basePath, name);
38
41
 
39
42
  if (fs.existsSync(colPath)) {
40
43
  throw new Error("Collection already exists");
41
44
  }
42
45
 
43
- // create folder
44
46
  await fs.promises.mkdir(colPath, { recursive: true });
45
-
46
- // load into memory map
47
- const col = new Collection(colPath);
48
- this.collections.set(name, col);
49
-
47
+ this.collections.set(name, new Collection(colPath));
50
48
  return true;
51
49
  }
52
50
 
53
- /** Delete collection fully */
54
- async deleteCollection(name) {
51
+ async deleteCollection(name: string): Promise<boolean> {
55
52
  const colPath = path.join(this.basePath, name);
56
53
 
57
54
  if (!fs.existsSync(colPath)) {
58
55
  throw new Error("Collection does not exist");
59
56
  }
60
57
 
61
- // close LevelDB instance if opened
62
58
  if (this.collections.has(name)) {
63
- const col = this.collections.get(name);
64
- await col.close().catch(() => {});
65
- await col.db.clear().catch(() => {});
59
+ await this.collections.get(name)!.close();
66
60
  this.collections.delete(name);
67
61
  }
68
62
 
69
- // force delete directory
70
63
  await fs.promises.rm(colPath, { recursive: true, force: true });
71
-
72
64
  return true;
73
65
  }
74
66
 
75
- /** Rename collection */
76
- async renameCollection(oldName, newName) {
67
+ async renameCollection(oldName: string, newName: string): Promise<boolean> {
77
68
  const oldPath = path.join(this.basePath, oldName);
78
69
  const newPath = path.join(this.basePath, newName);
79
70
 
@@ -81,29 +72,24 @@ export class LioranDB {
81
72
  if (fs.existsSync(newPath)) throw new Error("New collection name exists");
82
73
 
83
74
  if (this.collections.has(oldName)) {
84
- await this.collections.get(oldName).close().catch(() => {});
75
+ await this.collections.get(oldName)!.close();
85
76
  this.collections.delete(oldName);
86
77
  }
87
78
 
88
79
  await fs.promises.rename(oldPath, newPath);
89
-
90
- const newCol = new Collection(newPath);
91
- this.collections.set(newName, newCol);
92
-
80
+ this.collections.set(newName, new Collection(newPath));
93
81
  return true;
94
82
  }
95
83
 
96
- /** Drop a collection (alias deleteCollection) */
97
- async dropCollection(name) {
84
+ async dropCollection(name: string): Promise<boolean> {
98
85
  return this.deleteCollection(name);
99
86
  }
100
87
 
101
- /** List all collections */
102
- async listCollections() {
103
- const dirs = await fs.promises.readdir(this.basePath, { withFileTypes: true });
88
+ async listCollections(): Promise<string[]> {
89
+ const dirs = await fs.promises.readdir(this.basePath, {
90
+ withFileTypes: true
91
+ });
104
92
 
105
- return dirs
106
- .filter(dirent => dirent.isDirectory())
107
- .map(dirent => dirent.name);
93
+ return dirs.filter(d => d.isDirectory()).map(d => d.name);
108
94
  }
109
95
  }
@@ -1,8 +1,8 @@
1
- function getByPath(obj, path) {
1
+ function getByPath(obj: any, path: string): any {
2
2
  return path.split(".").reduce((o, p) => (o ? o[p] : undefined), obj);
3
3
  }
4
4
 
5
- export function matchDocument(doc, query) {
5
+ export function matchDocument(doc: any, query: any): boolean {
6
6
  for (const key of Object.keys(query)) {
7
7
  const cond = query[key];
8
8
  const val = getByPath(doc, key);
@@ -14,9 +14,10 @@ export function matchDocument(doc, query) {
14
14
  if (op === "$gte" && !(val >= v)) return false;
15
15
  if (op === "$lt" && !(val < v)) return false;
16
16
  if (op === "$lte" && !(val <= v)) return false;
17
- if (op === "$ne" && (val === v)) return false;
17
+ if (op === "$ne" && val === v) return false;
18
18
  if (op === "$eq" && val !== v) return false;
19
- if (op === "$in" && (!Array.isArray(v) || !v.includes(val))) return false;
19
+ if (op === "$in" && (!Array.isArray(v) || !v.includes(val)))
20
+ return false;
20
21
  }
21
22
  } else {
22
23
  if (val !== cond) return false;
@@ -25,38 +26,35 @@ export function matchDocument(doc, query) {
25
26
  return true;
26
27
  }
27
28
 
28
- export function applyUpdate(oldDoc, update) {
29
+ export function applyUpdate(oldDoc: any, update: any): any {
29
30
  const doc = structuredClone(oldDoc);
30
31
 
31
- // $set
32
32
  if (update.$set) {
33
33
  for (const k in update.$set) {
34
34
  const parts = k.split(".");
35
35
  let cur = doc;
36
36
  for (let i = 0; i < parts.length - 1; i++) {
37
- cur[parts[i]] = cur[parts[i]] ?? {};
37
+ cur[parts[i]] ??= {};
38
38
  cur = cur[parts[i]];
39
39
  }
40
- cur[parts.at(-1)] = update.$set[k];
40
+ cur[parts.at(-1)!] = update.$set[k];
41
41
  }
42
42
  }
43
43
 
44
- // $inc
45
44
  if (update.$inc) {
46
45
  for (const k in update.$inc) {
47
46
  const val = getByPath(doc, k) ?? 0;
48
47
  const parts = k.split(".");
49
48
  let cur = doc;
50
49
  for (let i = 0; i < parts.length - 1; i++) {
51
- cur[parts[i]] = cur[parts[i]] ?? {};
50
+ cur[parts[i]] ??= {};
52
51
  cur = cur[parts[i]];
53
52
  }
54
- cur[parts.at(-1)] = val + update.$inc[k];
53
+ cur[parts.at(-1)!] = val + update.$inc[k];
55
54
  }
56
55
  }
57
56
 
58
- // Direct merge (no operators)
59
- const hasOp = Object.keys(update).some((k) => k.startsWith("$"));
57
+ const hasOp = Object.keys(update).some(k => k.startsWith("$"));
60
58
  if (!hasOp) {
61
59
  return { ...doc, ...update };
62
60
  }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { LioranManager } from "./LioranManager.js"
2
+ export { LioranDB } from "./core/database.js"
3
+ export { getBaseDBFolder } from "./utils/rootpath.js"
4
+
5
+ export * from "./types/index.js"
@@ -0,0 +1,12 @@
1
+ export interface LioranManagerOptions {
2
+ rootPath?: string
3
+ encryptionKey?: string | Buffer
4
+ }
5
+
6
+ export interface UpdateOptions {
7
+ upsert?: boolean
8
+ }
9
+
10
+ export type Query<T = any> = Partial<T> & {
11
+ [key: string]: any
12
+ }
@@ -2,14 +2,9 @@ import crypto from "crypto";
2
2
  import { getMasterKey } from "./secureKey.js";
3
3
 
4
4
  const algorithm = "aes-256-gcm";
5
+ let ACTIVE_KEY: Buffer = getMasterKey();
5
6
 
6
- // 🔐 Runtime-configurable key
7
- let ACTIVE_KEY = getMasterKey();
8
-
9
- /**
10
- * Allows LioranManager to inject a custom encryption key
11
- */
12
- export function setEncryptionKey(key) {
7
+ export function setEncryptionKey(key: string | Buffer): void {
13
8
  if (!key) return;
14
9
 
15
10
  if (typeof key === "string") {
@@ -19,7 +14,7 @@ export function setEncryptionKey(key) {
19
14
 
20
15
  if (Buffer.isBuffer(key)) {
21
16
  if (key.length !== 32) {
22
- throw new Error("Encryption key must be 32 bytes (256-bit)");
17
+ throw new Error("Encryption key must be 32 bytes");
23
18
  }
24
19
  ACTIVE_KEY = key;
25
20
  return;
@@ -28,9 +23,9 @@ export function setEncryptionKey(key) {
28
23
  throw new Error("Invalid encryption key format");
29
24
  }
30
25
 
31
- export function encryptData(plainObj) {
26
+ export function encryptData(obj: any): string {
32
27
  const iv = crypto.randomBytes(16);
33
- const data = Buffer.from(JSON.stringify(plainObj), "utf8");
28
+ const data = Buffer.from(JSON.stringify(obj), "utf8");
34
29
 
35
30
  const cipher = crypto.createCipheriv(algorithm, ACTIVE_KEY, iv);
36
31
  const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
@@ -39,11 +34,11 @@ export function encryptData(plainObj) {
39
34
  return Buffer.concat([iv, tag, encrypted]).toString("base64");
40
35
  }
41
36
 
42
- export function decryptData(encStr) {
43
- const buf = Buffer.from(encStr, "base64");
44
- const iv = buf.slice(0, 16);
45
- const tag = buf.slice(16, 32);
46
- const encrypted = buf.slice(32);
37
+ export function decryptData(enc: string): any {
38
+ const buf = Buffer.from(enc, "base64");
39
+ const iv = buf.subarray(0, 16);
40
+ const tag = buf.subarray(16, 32);
41
+ const encrypted = buf.subarray(32);
47
42
 
48
43
  const decipher = crypto.createDecipheriv(algorithm, ACTIVE_KEY, iv);
49
44
  decipher.setAuthTag(tag);
@@ -0,0 +1,54 @@
1
+ import os from "os";
2
+ import path from "path";
3
+ import fs from "fs";
4
+
5
+ export function getDefaultRootPath(): string {
6
+ let dbPath = process.env.LIORANDB_PATH;
7
+
8
+ if (!dbPath) {
9
+ const homeDir = os.homedir();
10
+ dbPath = path.join(homeDir, "LioranDB", "db");
11
+
12
+ if (!fs.existsSync(dbPath)) {
13
+ fs.mkdirSync(dbPath, { recursive: true });
14
+ }
15
+
16
+ process.env.LIORANDB_PATH = dbPath;
17
+ }
18
+
19
+ return dbPath;
20
+ }
21
+
22
+ export function getBaseDBFolder(): string {
23
+ return getDefaultRootPath();
24
+ }
25
+
26
+ export function installSystemEnv(dbPath = getDefaultRootPath()) {
27
+ const platform = os.platform();
28
+ const scriptsDir = path.join(os.tmpdir(), "lioran_env_setup");
29
+
30
+ if (!fs.existsSync(scriptsDir)) {
31
+ fs.mkdirSync(scriptsDir, { recursive: true });
32
+ }
33
+
34
+ if (platform === "win32") {
35
+ const script = path.join(scriptsDir, "set-lioran-env.ps1");
36
+ fs.writeFileSync(
37
+ script,
38
+ `[System.Environment]::SetEnvironmentVariable("LIORANDB_PATH","${dbPath}","Machine")`
39
+ );
40
+ return { platform: "windows", script };
41
+ }
42
+
43
+ if (platform === "linux" || platform === "darwin") {
44
+ const script = path.join(scriptsDir, "set-lioran-env.sh");
45
+ fs.writeFileSync(
46
+ script,
47
+ `export LIORANDB_PATH="${dbPath}"`
48
+ );
49
+ fs.chmodSync(script, 0o755);
50
+ return { platform, script };
51
+ }
52
+
53
+ return null;
54
+ }
@@ -0,0 +1,15 @@
1
+ import crypto from "crypto";
2
+ import os from "os";
3
+
4
+ export function getMasterKey(): Buffer {
5
+ const fingerprint = [
6
+ os.hostname(),
7
+ os.platform(),
8
+ os.arch(),
9
+ os.cpus()?.[0]?.model ?? "unknown",
10
+ os.cpus()?.length ?? 0,
11
+ os.totalmem()
12
+ ].join("|");
13
+
14
+ return crypto.createHash("sha256").update(fingerprint).digest();
15
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Node",
6
+ "declaration": true,
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["src"]
13
+ }
@@ -1,160 +0,0 @@
1
- import { ClassicLevel } from "classic-level";
2
- import { matchDocument, applyUpdate } from "./query.js";
3
- import { v4 as uuid } from "uuid";
4
- import { encryptData, decryptData } from "../utils/encryption.js";
5
-
6
- export class Collection {
7
- constructor(dir) {
8
- this.dir = dir;
9
- this.db = new ClassicLevel(dir, { valueEncoding: "json" });
10
-
11
- // Queue system
12
- this.queue = Promise.resolve(); // start with a resolved promise
13
- }
14
-
15
- // Queue wrapper
16
- _enqueue(task) {
17
- // Add task to the queue
18
- this.queue = this.queue.then(() => task()).catch(console.error);
19
- return this.queue;
20
- }
21
-
22
- async close() {
23
- if (this.db) {
24
- try {
25
- await this.db.close();
26
- } catch (err) {
27
- console.warn("Warning: close() failed", err);
28
- }
29
- }
30
- }
31
-
32
- async insertOne(doc) {
33
- return this._enqueue(async () => {
34
- const _id = doc._id ?? uuid();
35
- const final = { _id, ...doc };
36
- const encrypted = encryptData(final);
37
- await this.db.put(String(_id), encrypted);
38
- return final;
39
- });
40
- }
41
-
42
- async insertMany(docs = []) {
43
- return this._enqueue(async () => {
44
- const ops = [];
45
- const out = [];
46
-
47
- for (const d of docs) {
48
- const _id = d._id ?? uuid();
49
- const final = { _id, ...d };
50
- const encrypted = encryptData(final);
51
- ops.push({ type: "put", key: String(_id), value: encrypted });
52
- out.push(final);
53
- }
54
-
55
- await this.db.batch(ops);
56
- return out;
57
- });
58
- }
59
-
60
- async find(query = {}) {
61
- return this._enqueue(async () => {
62
- const out = [];
63
- for await (const [, encValue] of this.db.iterator()) {
64
- const value = decryptData(encValue);
65
- if (matchDocument(value, query)) out.push(value);
66
- }
67
- return out;
68
- });
69
- }
70
-
71
- async findOne(query = {}) {
72
- return this._enqueue(async () => {
73
- for await (const [, encValue] of this.db.iterator()) {
74
- const value = decryptData(encValue);
75
- if (matchDocument(value, query)) return value;
76
- }
77
- return null;
78
- });
79
- }
80
-
81
- async updateOne(filter = {}, update = {}, options = { upsert: false }) {
82
- return this._enqueue(async () => {
83
- for await (const [key, encValue] of this.db.iterator()) {
84
- const value = decryptData(encValue);
85
- if (matchDocument(value, filter)) {
86
- const updated = applyUpdate(value, update);
87
- updated._id = value._id;
88
- const encrypted = encryptData(updated);
89
- await this.db.put(key, encrypted);
90
- return updated;
91
- }
92
- }
93
-
94
- if (options.upsert) {
95
- const newDoc = applyUpdate(filter, update);
96
- newDoc._id = newDoc._id ?? uuid();
97
- const encrypted = encryptData(newDoc);
98
- await this.db.put(String(newDoc._id), encrypted);
99
- return newDoc;
100
- }
101
-
102
- return null;
103
- });
104
- }
105
-
106
- async updateMany(filter = {}, update = {}) {
107
- return this._enqueue(async () => {
108
- const updated = [];
109
- for await (const [key, encValue] of this.db.iterator()) {
110
- const value = decryptData(encValue);
111
- if (matchDocument(value, filter)) {
112
- const newDoc = applyUpdate(value, update);
113
- newDoc._id = value._id;
114
- const encrypted = encryptData(newDoc);
115
- await this.db.put(key, encrypted);
116
- updated.push(newDoc);
117
- }
118
- }
119
- return updated;
120
- });
121
- }
122
-
123
- async deleteOne(filter = {}) {
124
- return this._enqueue(async () => {
125
- for await (const [key, encValue] of this.db.iterator()) {
126
- const value = decryptData(encValue);
127
- if (matchDocument(value, filter)) {
128
- await this.db.del(key);
129
- return true;
130
- }
131
- }
132
- return false;
133
- });
134
- }
135
-
136
- async deleteMany(filter = {}) {
137
- return this._enqueue(async () => {
138
- let count = 0;
139
- for await (const [key, encValue] of this.db.iterator()) {
140
- const value = decryptData(encValue);
141
- if (matchDocument(value, filter)) {
142
- await this.db.del(key);
143
- count++;
144
- }
145
- }
146
- return count;
147
- });
148
- }
149
-
150
- async countDocuments(filter = {}) {
151
- return this._enqueue(async () => {
152
- let c = 0;
153
- for await (const [, encValue] of this.db.iterator()) {
154
- const value = decryptData(encValue);
155
- if (matchDocument(value, filter)) c++;
156
- }
157
- return c;
158
- });
159
- }
160
- }