Skip to content

Commit

Permalink
feat: added namespace support
Browse files Browse the repository at this point in the history
  • Loading branch information
vwh committed Sep 24, 2024
1 parent 1db814d commit aa41702
Show file tree
Hide file tree
Showing 6 changed files with 359 additions and 42 deletions.
52 changes: 41 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Fast and lightweight key-value database library.
---

## 📖 Contents

- [Features](#-features)
- [Installation](#-installation)
- [Usage](#usage)
Expand Down Expand Up @@ -401,6 +402,35 @@ Ensures all the changes are written to disk.
db.flush();
```

### `Namespace`

Creates a namespaced database instance.

- **Parameters**:
- `name`: The name of the namespace.
- **Returns**:
- A new database instance with the namespace applied.

```javascript
// Create a new database instance with a namespace
const db = new MiftahDB(":memory:");
const users = db.namespace("users");

// Set a value with a namespace
users.set("852335", { name: "Ahmad" });
console.log(users.get("852335"));

// Other examples:
// Will count the keys only on the "users" namespace
users.count();

// Will remove expired keys only on the "users" namespace
users.cleanup();

// Will remove all keys only on the "users" namespace
users.flush();
```

---

### `Execute`
Expand Down Expand Up @@ -486,17 +516,17 @@ try {

**MiftahDB** supports various value types:

|No | Type |
| - | ---------- |
| 1 | String |
| 2 | Number |
| 3 | Boolean |
| 4 | Array |
| 5 | Record (Object) |
| 6 | Date |
| 7 | Buffer (Binary Data) |
| 8 | Uint8Array (Binary Data) |
| 9 | Null |
| No | Type |
| --- | ------------------------ |
| 1 | String |
| 2 | Number |
| 3 | Boolean |
| 4 | Array |
| 5 | Record (Object) |
| 6 | Date |
| 7 | Buffer (Binary Data) |
| 8 | Uint8Array (Binary Data) |
| 9 | Null |

**Example for each type**:

Expand Down
88 changes: 66 additions & 22 deletions src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { writeFileSync, readFileSync } from "node:fs";
export abstract class BaseMiftahDB implements IMiftahDB {
protected declare db: Database;
protected statements: Record<string, Statement>;
private nameSpacePrefix: string | null = null;

constructor(path = ":memory:") {
this.initDatabase(path);
Expand All @@ -20,6 +21,16 @@ export abstract class BaseMiftahDB implements IMiftahDB {
this.statements = this.prepareStatements();
}

private addNamespacePrefix(k: string): string {
return this.nameSpacePrefix ? `${this.nameSpacePrefix}:${k}` : k;
}

private removeNamespacePrefix(key: string): string {
return this.nameSpacePrefix && key.startsWith(`${this.nameSpacePrefix}:`)
? key.slice(this.nameSpacePrefix.length + 1)
: key;
}

protected abstract initDatabase(path: string | ":memory:"): void;

protected prepareStatements(): Record<string, Statement> {
Expand All @@ -42,10 +53,12 @@ export abstract class BaseMiftahDB implements IMiftahDB {
}

get<T>(key: string): T | null {
const result = this.statements.get.get(key) as MiftahDBItem | undefined;
const result = this.statements.get.get(this.addNamespacePrefix(key)) as
| MiftahDBItem
| undefined;
if (!result) return null;
if (result?.expires_at && result.expires_at <= Date.now()) {
this.delete(key);
this.delete(this.addNamespacePrefix(key));
return null;
}
return decodeValue(result.value);
Expand All @@ -54,29 +67,40 @@ export abstract class BaseMiftahDB implements IMiftahDB {
set<T extends MiftahValue>(key: string, value: T, expiresAt?: Date): void {
const encodedValue = encodeValue(value);
const expiresAtMs = expiresAt?.getTime() ?? null;
this.statements.set.run(key, encodedValue, expiresAtMs);
this.statements.set.run(
this.addNamespacePrefix(key),
encodedValue,
expiresAtMs
);
}

exists(key: string): boolean {
const result = this.statements.exists.get(key) as { [key: string]: number };
const result = this.statements.exists.get(this.addNamespacePrefix(key)) as {
[key: string]: number;
};
return Boolean(Object.values(result)[0]);
}

delete(key: string): void {
this.statements.delete.run(key);
this.statements.delete.run(this.addNamespacePrefix(key));
}

rename(oldKey: string, newKey: string): void {
this.statements.rename.run(newKey, oldKey);
this.statements.rename.run(
this.addNamespacePrefix(newKey),
this.addNamespacePrefix(oldKey)
);
}

setExpire(key: string, expiresAt: Date): void {
const expiresAtMs = expiresAt.getTime();
this.statements.setExpire.run(expiresAtMs, key);
this.statements.setExpire.run(expiresAtMs, this.addNamespacePrefix(key));
}

getExpire(key: string): Date | null {
const result = this.statements.getExpire.get(key) as
const result = this.statements.getExpire.get(
this.addNamespacePrefix(key)
) as
| {
expires_at: number | null;
}
Expand All @@ -85,27 +109,36 @@ export abstract class BaseMiftahDB implements IMiftahDB {
}

keys(pattern = "%"): string[] {
const result = this.statements.keys.all(pattern) as {
const result = this.statements.keys.all(
this.addNamespacePrefix(pattern)
) as {
key: string;
}[];
return result.map((r) => r.key);
return result.map((r) => this.removeNamespacePrefix(r.key));
}

pagination(limit: number, page: number, pattern = "%"): string[] {
const offset = (page - 1) * limit;
const result = this.statements.pagination.all(pattern, limit, offset) as {
key: string;
}[];
return result.map((r) => r.key);
const result = this.statements.pagination.all(
this.addNamespacePrefix(pattern),
limit,
offset
) as { key: string }[];
return result.map((r) => this.removeNamespacePrefix(r.key));
}

count(pattern = "%"): number {
const result = this.statements.countKeys.get(pattern) as { count: number };
const result = this.statements.countKeys.get(
this.nameSpacePrefix ? `${this.nameSpacePrefix}:${pattern}` : pattern
) as { count: number };
return result.count;
}

countExpired(pattern = "%"): number {
const result = this.statements.countExpired.get(pattern) as {
const result = this.statements.countExpired.get(
Date.now(),
this.nameSpacePrefix ? `${this.nameSpacePrefix}:${pattern}` : pattern
) as {
count: number;
};
return result.count;
Expand All @@ -114,8 +147,9 @@ export abstract class BaseMiftahDB implements IMiftahDB {
multiGet<T>(keys: string[]): Record<string, T | null> {
const result: Record<string, T | null> = {};
this.db.transaction(() => {
for (const key of keys) {
result[key] = this.get<T>(key);
for (const k of keys) {
const value = this.get<T>(this.addNamespacePrefix(k));
result[this.removeNamespacePrefix(k)] = value;
}
})();
return result;
Expand All @@ -126,14 +160,16 @@ export abstract class BaseMiftahDB implements IMiftahDB {
): void {
this.db.transaction(() => {
for (const entry of entries) {
this.set(entry.key, entry.value, entry.expiresAt);
const key = this.addNamespacePrefix(entry.key);
this.set(key, entry.value, entry.expiresAt);
}
})();
}

multiDelete(keys: string[]): void {
this.db.transaction(() => {
for (const key of keys) {
for (const k of keys) {
const key = this.addNamespacePrefix(k);
this.delete(key);
}
})();
Expand All @@ -149,11 +185,11 @@ export abstract class BaseMiftahDB implements IMiftahDB {
}

cleanup(): void {
this.statements.cleanup.run(Date.now());
this.statements.cleanup.run(Date.now(), this.addNamespacePrefix("%"));
}

flush(): void {
this.statements.flush.run();
this.statements.flush.run(this.addNamespacePrefix("%"));
}

backup(path: string): void {
Expand All @@ -178,4 +214,12 @@ export abstract class BaseMiftahDB implements IMiftahDB {
this.db = new DB(file);
this.statements = this.prepareStatements();
}

namespace(name: string): IMiftahDB {
const namespacedDB = Object.create(this);
namespacedDB.nameSpacePrefix = this.nameSpacePrefix
? `${this.nameSpacePrefix}:${name}`
: name;
return namespacedDB;
}
}
6 changes: 3 additions & 3 deletions src/statements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const SQL_STATEMENTS = {

// Deletes rows that have expired
CLEANUP:
"DELETE FROM miftahDB WHERE expires_at IS NOT NULL AND expires_at <= ?",
"DELETE FROM miftahDB WHERE expires_at IS NOT NULL AND expires_at <= ? AND key LIKE ?",

// Renames a key
RENAME: "UPDATE miftahDB SET key = ? WHERE key = ?",
Expand All @@ -32,7 +32,7 @@ export const SQL_STATEMENTS = {
VACUUM: "VACUUM",

// Deletes all rows from the table
FLUSH: "DELETE FROM miftahDB",
FLUSH: "DELETE FROM miftahDB WHERE key LIKE ?",

// Returns true if the key exists, false otherwise
EXISTS: "SELECT EXISTS (SELECT 1 FROM miftahDB WHERE key = ? LIMIT 1)",
Expand All @@ -54,7 +54,7 @@ export const SQL_STATEMENTS = {

// Counts the number of expired rows in the table
COUNT_EXPIRED:
"SELECT COUNT(*) as count FROM miftahDB WHERE (expires_at IS NOT NULL AND expires_at <= strftime('%s', 'now') * 1000) AND key LIKE ?",
"SELECT COUNT(*) as count FROM miftahDB WHERE (expires_at IS NOT NULL AND expires_at <= ?) AND key LIKE ?",

// Creates the PRAGMA statements
CREATE_PRAGMA: `
Expand Down
26 changes: 20 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface IMiftahDB<T extends MiftahValue = MiftahValue> {
* @example
* const value = db.get('user:1234');
*/
get(key: string): T | null;
get<K extends T>(key: string): K | null;

/**
* Sets a value in the database with an optional expiration.
Expand All @@ -35,7 +35,7 @@ export interface IMiftahDB<T extends MiftahValue = MiftahValue> {
* @example
* db.set('user:1234', { name: 'Ahmad' }, new Date('2023-12-31'));
*/
set(key: string, value: T, expiresAt?: Date): void;
set<K extends T>(key: string, value: K, expiresAt?: Date): void;

/**
* Gets the expiration date of a key.
Expand Down Expand Up @@ -107,7 +107,7 @@ export interface IMiftahDB<T extends MiftahValue = MiftahValue> {
* // Get keys starting with "log", followed by exactly two characters, and ending with any number of characters
* const logKeys = db.keys('log__:%');
*/
keys(pattern: string): string[];
keys(pattern?: string): string[];

/**
* Retrieves a paginated list of keys matching a pattern.
Expand All @@ -126,7 +126,7 @@ export interface IMiftahDB<T extends MiftahValue = MiftahValue> {
* // Get the next 10 keys matching "user:%" (keys starting with "user:")
* const secondUsersPage = db.pagination(10, 2, "user:%");
*/
pagination(limit: number, page: number, pattern: string): string[];
pagination(limit: number, page: number, pattern?: string): string[];

/**
* Counts the number of keys in the database.
Expand Down Expand Up @@ -164,7 +164,7 @@ export interface IMiftahDB<T extends MiftahValue = MiftahValue> {
* @example
* const values = db.multiGet(['user:1234', 'user:5678']);
*/
multiGet(keys: string[]): Record<string, T | null>;
multiGet<K extends T>(keys: string[]): Record<string, K | null>;

/**
* Sets multiple key-value pairs in the database with optional expirations.
Expand All @@ -176,7 +176,9 @@ export interface IMiftahDB<T extends MiftahValue = MiftahValue> {
* { key: 'user:5678', value: { name: 'Fatima' } }
* ]);
*/
multiSet(entries: Array<{ key: string; value: T; expiresAt?: Date }>): void;
multiSet<K extends T>(
entries: Array<{ key: string; value: K; expiresAt?: Date }>
): void;

/**
* Deletes multiple key-value pairs from the database.
Expand Down Expand Up @@ -255,6 +257,18 @@ export interface IMiftahDB<T extends MiftahValue = MiftahValue> {
* console.log(db.get("key"));
*/
restore(path: string): void;

/**
* Creates a namespaced database instance.
* - https://miftahdb.sqlite3.online/docs/api-reference/namespace
* @param name - The name of the namespace.
* @returns A new database instance with the namespace applied.
* @example
* const users = db.namespace("users");
* users.set("123", "value1");
* console.log(users.get("123"));
*/
namespace(name: string): IMiftahDB<T>;
}

/**
Expand Down
Loading

0 comments on commit aa41702

Please sign in to comment.