feat(core-storage): add PouchEnvelope tests and LocalStorageService implementation

- Introduced tests for PouchEnvelope to validate envelope-aware CRUD operations using an in-memory PouchDB.
- Implemented LocalStorageService with encryption support for sensitive keys, including tests for setItem, getItem, removeItem, and clear methods.
- Defined a generic storage interface (IStorageService) to enforce type-safe serialization/deserialization across storage implementations.
This commit is contained in:
Firman Ramdhani
2026-07-06 11:38:37 +07:00
parent 7130eb3fe3
commit d1ce292e3c
17 changed files with 1568 additions and 215 deletions
@@ -116,7 +116,7 @@ const theme = await appStorage.getItem('THEME'); // Plaintext on disk
## 🔄 Offline-First Document Storage (PouchDB & CouchDB)
For complex, document-oriented data that requires fault-tolerance, offline support, and bi-directional cloud synchronization, we use the `PouchDatabaseManager`. This layer is powered by [PouchDB](https://pouchdb.com/) syncing to [CouchDB](https://couchdb.apache.org/).
For complex, document-oriented data that requires fault-tolerance, offline support, and bi-directional cloud synchronization, we use the `PouchDBManager`. This layer is powered by [PouchDB](https://pouchdb.com/) syncing to [CouchDB](https://couchdb.apache.org/).
### Architecture
@@ -134,7 +134,7 @@ graph TD
end
subgraph CoreStorage ["@repo/core-storage Engine"]
MGR[PouchDatabaseManager Factory]
MGR[PouchDBManager Factory]
L_SALES[(Local PouchDB: Sales)]
L_INV[(Local PouchDB: Inventory)]
end
@@ -168,13 +168,13 @@ graph TD
### 1. Initialization (IoC Factory)
The `PouchDatabaseManager` acts as a central singleton. It registers and manages all database instances. If a remote URL is provided, it automatically handles background synchronization.
The `PouchDBManager` acts as a central singleton. It registers and manages all database instances. If a remote URL is provided, it automatically handles background synchronization.
```typescript
import { PouchDatabaseManager } from '@repo/core-storage';
import { PouchDBManager } from '@repo/core-storage';
import type { Item } from './types';
export const dbManager = new PouchDatabaseManager();
export const dbManager = new PouchDBManager();
export const itemDB = dbManager.register<Item>({
localName: 'items_db',
@@ -184,7 +184,7 @@ export const itemDB = dbManager.register<Item>({
### 2. CRUD & MongoDB-style Queries
The registered database returns a `PouchDatabaseWrapper`. This wrapper fully abstracts the raw PouchDB API into clean, Promise-based helpers, auto-handling `_rev` conflicts.
The registered database returns a `PouchService` instance. This wrapper fully abstracts the raw PouchDB API into clean, Promise-based helpers, auto-handling `_rev` conflicts.
| Method | Description |
|---|---|
@@ -231,6 +231,38 @@ export function InventoryList() {
}
```
### 4. Envelope Pattern (`PouchEnvelopeDBManager`)
If you want to store multiple types of entities (e.g. `items`, `bookings`, `activities`) in a single CouchDB/PouchDB database to simplify sync setup, use the **Envelope Pattern**.
Instead of `PouchDBManager`, instantiate a `PouchEnvelopeDBManager`. It provides the exact same `PouchService` API (CRUD + Find), but automatically wraps documents into an envelope format internally: `{ _id: "entityName:businessId", entity: "entityName", data: { ... } }`.
```typescript
import { PouchEnvelopeDBManager } from '@repo/core-storage';
import type { ItemEntity, BookingEntity } from './types';
export const envelopeDbManager = new PouchEnvelopeDBManager();
// Registers to the SAME database 'master_db', but scoped to 'item'
export const itemDB = envelopeDbManager.register<ItemEntity>({
localName: 'master_db',
remoteUrl: 'http://admin:pass@localhost:5984/master_db'
}, 'item');
// Registers to the SAME database 'master_db', but scoped to 'booking'
export const bookingDB = envelopeDbManager.register<BookingEntity>({
localName: 'master_db',
remoteUrl: 'http://admin:pass@localhost:5984/master_db'
}, 'booking');
// API usage remains identical!
await itemDB.create({ _id: '123', name: 'Widget' }); // Stored as "item:123"
const items = await itemDB.getAll(); // Only returns documents where entity === 'item'
// Unique to PouchEnvelopeService: Cross-field keyword searching
const results = await itemDB.search('widget keyword', ['data.name', 'data.sku']);
```
### ✅ Do's and ❌ Don'ts for PouchDB
* **✅ DO use `.onChange()`** to make your UI reactive to background cloud syncs.