Files
trackgo-fe/packages/core-storage/tests/pouch.test.ts
T

258 lines
9.0 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import PouchDB from 'pouchdb-core';
import PouchDBAdapterMemory from 'pouchdb-adapter-memory';
import PouchDBFind from 'pouchdb-find';
import PouchDBMapReduce from 'pouchdb-mapreduce';
// Build a minimal PouchDB for testing: core + memory adapter + find
PouchDB.plugin(PouchDBAdapterMemory);
PouchDB.plugin(PouchDBFind);
PouchDB.plugin(PouchDBMapReduce);
/**
* Since the tests run in Node (not a browser), we cannot use PouchDatabaseManager
* directly because it imports `pouchdb-browser` which requires `self`.
* Instead, we test the CRUD logic by creating a lightweight test wrapper
* that mirrors PouchDatabaseWrapper's methods using the memory-backed PouchDB.
*/
function createTestDB(name: string) {
const raw = new PouchDB(name, { adapter: 'memory' });
return {
raw,
async create<T extends object>(data: T) {
return raw.put(data as PouchDB.Core.Document<T>);
},
async update<T extends object>(id: string, data: Partial<T>) {
const existing = await raw.get(id);
const merged = { ...existing, ...data };
return raw.put(merged);
},
async delete(id: string) {
const doc = await raw.get(id);
return raw.remove(doc);
},
async getOne<T>(id: string) {
return raw.get<T>(id) as Promise<T & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta>;
},
async getAll<T>() {
const result = await raw.allDocs({ include_docs: true });
return result.rows
.filter((row) => !row.id.startsWith('_design/'))
.map((row) => row.doc as unknown as T);
},
async getSome<T>(ids: string[]) {
const result = await raw.allDocs({ keys: ids, include_docs: true });
return result.rows
.filter((row): row is any => !('error' in row) && !!(row as any).doc)
.map((row: any) => row.doc as T);
},
async find<T extends object>(options: PouchDB.Find.FindRequest<T>) {
const result = await raw.find(options as PouchDB.Find.FindRequest<object>);
return result.docs as unknown as T[];
},
async cleanAllData() {
const result = await raw.allDocs();
const deletions = result.rows
.filter((row) => !row.id.startsWith('_design/'))
.map((row) => ({
_id: row.id,
_rev: row.value.rev,
_deleted: true as const,
}));
if (deletions.length > 0) {
await raw.bulkDocs(deletions);
}
},
async destroy() {
await raw.destroy();
},
};
}
describe('PouchDatabaseWrapper CRUD Operations', () => {
let db: ReturnType<typeof createTestDB>;
beforeEach(() => {
// Use a unique name per test to avoid cross-contamination
db = createTestDB(`test_db_${Date.now()}_${Math.random().toString(36).slice(2)}`);
});
afterEach(async () => {
try {
await db.destroy();
} catch {
// Already destroyed in some tests
}
});
// ─── create ───────────────────────────────────────────────────
describe('create', () => {
it('should create a document with a given _id', async () => {
const res = await db.create({ _id: 'doc-001', name: 'Alice', age: 30 });
expect(res.ok).toBe(true);
expect(res.id).toBe('doc-001');
});
it('should throw a conflict if creating with a duplicate _id', async () => {
await db.create({ _id: 'dup-001', name: 'First' });
await expect(db.create({ _id: 'dup-001', name: 'Second' })).rejects.toThrow();
});
});
// ─── getOne ───────────────────────────────────────────────────
describe('getOne', () => {
it('should retrieve a document by id', async () => {
await db.create({ _id: 'fetch-001', product: 'Widget', price: 9.99 });
const doc = await db.getOne<{ product: string; price: number }>('fetch-001');
expect(doc._id).toBe('fetch-001');
expect(doc.product).toBe('Widget');
expect(doc.price).toBe(9.99);
});
it('should throw for a non-existent document', async () => {
await expect(db.getOne('non-existent')).rejects.toThrow();
});
});
// ─── update ───────────────────────────────────────────────────
describe('update', () => {
it('should merge new fields into an existing document', async () => {
await db.create({ _id: 'upd-001', name: 'Original', count: 1 });
const res = await db.update('upd-001', { count: 42, extra: 'field' });
expect(res.ok).toBe(true);
const updated = await db.getOne<{ name: string; count: number; extra: string }>('upd-001');
expect(updated.name).toBe('Original'); // untouched
expect(updated.count).toBe(42); // updated
expect(updated.extra).toBe('field'); // newly added
});
it('should throw when updating a non-existent document', async () => {
await expect(db.update('ghost', { name: 'nope' })).rejects.toThrow();
});
});
// ─── delete ───────────────────────────────────────────────────
describe('delete', () => {
it('should remove a document by id', async () => {
await db.create({ _id: 'del-001', name: 'ToBeDeleted' });
const res = await db.delete('del-001');
expect(res.ok).toBe(true);
await expect(db.getOne('del-001')).rejects.toThrow();
});
});
// ─── getAll ───────────────────────────────────────────────────
describe('getAll', () => {
it('should return all documents as a flat array', async () => {
await db.create({ _id: 'a', val: 1 });
await db.create({ _id: 'b', val: 2 });
await db.create({ _id: 'c', val: 3 });
const all = await db.getAll<{ val: number }>();
expect(all).toHaveLength(3);
expect(all.map((d: any) => d.val).sort()).toEqual([1, 2, 3]);
});
it('should return empty array for empty database', async () => {
const all = await db.getAll();
expect(all).toEqual([]);
});
});
// ─── getSome ──────────────────────────────────────────────────
describe('getSome', () => {
it('should return only the requested documents', async () => {
await db.create({ _id: 'x1', v: 10 });
await db.create({ _id: 'x2', v: 20 });
await db.create({ _id: 'x3', v: 30 });
const some = await db.getSome<{ v: number }>(['x1', 'x3']);
expect(some).toHaveLength(2);
expect(some.map((d: any) => d.v).sort()).toEqual([10, 30]);
});
it('should silently skip missing ids', async () => {
await db.create({ _id: 'exists', v: 1 });
const some = await db.getSome<{ v: number }>(['exists', 'ghost']);
expect(some).toHaveLength(1);
expect((some[0] as any).v).toBe(1);
});
});
// ─── find (pouchdb-find selectors) ────────────────────────────
describe('find', () => {
it('should filter documents using selectors', async () => {
await db.create({ _id: 'p1', category: 'electronics', price: 100 });
await db.create({ _id: 'p2', category: 'clothing', price: 50 });
await db.create({ _id: 'p3', category: 'electronics', price: 200 });
const results = await db.find<{ category: string; price: number }>({
selector: { category: 'electronics' },
});
expect(results).toHaveLength(2);
expect(results.every((r) => r.category === 'electronics')).toBe(true);
});
it('should support $gt comparisons', async () => {
await db.create({ _id: 'i1', price: 10 });
await db.create({ _id: 'i2', price: 50 });
await db.create({ _id: 'i3', price: 100 });
const results = await db.find<{ price: number }>({
selector: { price: { $gt: 40 } },
});
expect(results).toHaveLength(2);
expect(results.every((r) => r.price > 40)).toBe(true);
});
});
// ─── cleanAllData ─────────────────────────────────────────────
describe('cleanAllData', () => {
it('should remove all documents but keep the database intact', async () => {
await db.create({ _id: 'c1', name: 'One' });
await db.create({ _id: 'c2', name: 'Two' });
await db.create({ _id: 'c3', name: 'Three' });
let all = await db.getAll();
expect(all).toHaveLength(3);
await db.cleanAllData();
all = await db.getAll();
expect(all).toHaveLength(0);
// Database should still be functional after cleaning
await db.create({ _id: 'c4', name: 'Four' });
all = await db.getAll();
expect(all).toHaveLength(1);
});
});
});