Initial TrueGrowth source import
This commit is contained in:
342
packages/utils/src/indexeddb/index.test.ts
Normal file
342
packages/utils/src/indexeddb/index.test.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import 'fake-indexeddb/auto';
|
||||
import {
|
||||
openIndexedDB,
|
||||
getById,
|
||||
getAll,
|
||||
getAllWithCursor,
|
||||
put,
|
||||
putMany,
|
||||
deleteById,
|
||||
deleteMany,
|
||||
clearStore,
|
||||
countRecords,
|
||||
hasStore,
|
||||
} from './operations';
|
||||
|
||||
// Test data type
|
||||
interface TestRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
value: number;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
describe('IndexedDB Operations', () => {
|
||||
const DB_NAME = 'test-db';
|
||||
const STORE_NAME = 'test-store';
|
||||
let db: IDBDatabase;
|
||||
|
||||
// Helper to create a test database with store
|
||||
async function createTestDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, 1);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const database = request.result;
|
||||
if (!database.objectStoreNames.contains(STORE_NAME)) {
|
||||
const store = database.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
||||
store.createIndex('timestamp', 'timestamp', { unique: false });
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
// Reset fake-indexeddb
|
||||
indexedDB.deleteDatabase(DB_NAME);
|
||||
db = await createTestDB();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (db) {
|
||||
db.close();
|
||||
}
|
||||
indexedDB.deleteDatabase(DB_NAME);
|
||||
});
|
||||
|
||||
describe('openIndexedDB', () => {
|
||||
it('should open an existing database', async () => {
|
||||
const openedDb = await openIndexedDB(DB_NAME);
|
||||
|
||||
expect(openedDb).toBeInstanceOf(IDBDatabase);
|
||||
expect(openedDb.name).toBe(DB_NAME);
|
||||
|
||||
openedDb.close();
|
||||
});
|
||||
|
||||
it('should call onUpgradeNeeded when provided', async () => {
|
||||
const newDbName = 'new-test-db';
|
||||
let upgradeCalled = false;
|
||||
|
||||
const openedDb = await openIndexedDB(newDbName, {
|
||||
version: 1,
|
||||
onUpgradeNeeded: (database) => {
|
||||
upgradeCalled = true;
|
||||
database.createObjectStore('new-store', { keyPath: 'id' });
|
||||
},
|
||||
});
|
||||
|
||||
expect(upgradeCalled).toBe(true);
|
||||
expect(openedDb.objectStoreNames.contains('new-store')).toBe(true);
|
||||
|
||||
openedDb.close();
|
||||
indexedDB.deleteDatabase(newDbName);
|
||||
});
|
||||
});
|
||||
|
||||
describe('put and getById', () => {
|
||||
it('should store and retrieve a record', async () => {
|
||||
const record: TestRecord = { id: 'test-1', name: 'Test', value: 42 };
|
||||
|
||||
await put(db, STORE_NAME, record);
|
||||
const retrieved = await getById<TestRecord>(db, STORE_NAME, 'test-1');
|
||||
|
||||
expect(retrieved).toEqual(record);
|
||||
});
|
||||
|
||||
it('should return null for non-existent record', async () => {
|
||||
const result = await getById<TestRecord>(db, STORE_NAME, 'non-existent');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should update existing record', async () => {
|
||||
const record: TestRecord = { id: 'test-1', name: 'Original', value: 1 };
|
||||
await put(db, STORE_NAME, record);
|
||||
|
||||
const updated: TestRecord = { id: 'test-1', name: 'Updated', value: 2 };
|
||||
await put(db, STORE_NAME, updated);
|
||||
|
||||
const retrieved = await getById<TestRecord>(db, STORE_NAME, 'test-1');
|
||||
expect(retrieved?.name).toBe('Updated');
|
||||
expect(retrieved?.value).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should retrieve all records', async () => {
|
||||
const records: TestRecord[] = [
|
||||
{ id: '1', name: 'First', value: 1 },
|
||||
{ id: '2', name: 'Second', value: 2 },
|
||||
{ id: '3', name: 'Third', value: 3 },
|
||||
];
|
||||
|
||||
for (const record of records) {
|
||||
await put(db, STORE_NAME, record);
|
||||
}
|
||||
|
||||
const all = await getAll<TestRecord>(db, STORE_NAME);
|
||||
|
||||
expect(all).toHaveLength(3);
|
||||
expect(all.map((r) => r.id).sort()).toEqual(['1', '2', '3']);
|
||||
});
|
||||
|
||||
it('should return empty array for empty store', async () => {
|
||||
const all = await getAll<TestRecord>(db, STORE_NAME);
|
||||
|
||||
expect(all).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllWithCursor', () => {
|
||||
beforeEach(async () => {
|
||||
const records: TestRecord[] = [
|
||||
{ id: '1', name: 'First', value: 10, timestamp: 100 },
|
||||
{ id: '2', name: 'Second', value: 20, timestamp: 200 },
|
||||
{ id: '3', name: 'Third', value: 30, timestamp: 300 },
|
||||
{ id: '4', name: 'Fourth', value: 40, timestamp: 400 },
|
||||
{ id: '5', name: 'Fifth', value: 50, timestamp: 500 },
|
||||
];
|
||||
|
||||
for (const record of records) {
|
||||
await put(db, STORE_NAME, record);
|
||||
}
|
||||
});
|
||||
|
||||
it('should retrieve all records with cursor', async () => {
|
||||
const all = await getAllWithCursor<TestRecord>(db, STORE_NAME);
|
||||
|
||||
expect(all).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('should limit results', async () => {
|
||||
const limited = await getAllWithCursor<TestRecord>(db, STORE_NAME, {
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
expect(limited).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should skip with offset', async () => {
|
||||
const skipped = await getAllWithCursor<TestRecord>(db, STORE_NAME, {
|
||||
offset: 2,
|
||||
});
|
||||
|
||||
expect(skipped).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should combine limit and offset', async () => {
|
||||
const paged = await getAllWithCursor<TestRecord>(db, STORE_NAME, {
|
||||
offset: 1,
|
||||
limit: 2,
|
||||
});
|
||||
|
||||
expect(paged).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should filter records', async () => {
|
||||
const filtered = await getAllWithCursor<TestRecord>(db, STORE_NAME, {
|
||||
filter: (r) => r.value > 25,
|
||||
});
|
||||
|
||||
expect(filtered).toHaveLength(3);
|
||||
expect(filtered.every((r) => r.value > 25)).toBe(true);
|
||||
});
|
||||
|
||||
it('should use index for sorting', async () => {
|
||||
const sorted = await getAllWithCursor<TestRecord>(db, STORE_NAME, {
|
||||
indexName: 'timestamp',
|
||||
direction: 'prev', // Descending
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
expect(sorted).toHaveLength(3);
|
||||
expect(sorted[0].timestamp).toBe(500);
|
||||
expect(sorted[1].timestamp).toBe(400);
|
||||
expect(sorted[2].timestamp).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('putMany', () => {
|
||||
it('should insert multiple records', async () => {
|
||||
const records: TestRecord[] = [
|
||||
{ id: '1', name: 'First', value: 1 },
|
||||
{ id: '2', name: 'Second', value: 2 },
|
||||
{ id: '3', name: 'Third', value: 3 },
|
||||
];
|
||||
|
||||
await putMany(db, STORE_NAME, records);
|
||||
|
||||
const all = await getAll<TestRecord>(db, STORE_NAME);
|
||||
expect(all).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should handle empty array', async () => {
|
||||
await putMany(db, STORE_NAME, []);
|
||||
|
||||
const all = await getAll<TestRecord>(db, STORE_NAME);
|
||||
expect(all).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteById', () => {
|
||||
it('should delete a record', async () => {
|
||||
await put(db, STORE_NAME, { id: 'to-delete', name: 'Delete Me', value: 0 });
|
||||
|
||||
await deleteById(db, STORE_NAME, 'to-delete');
|
||||
|
||||
const result = await getById<TestRecord>(db, STORE_NAME, 'to-delete');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should not throw for non-existent record', async () => {
|
||||
await expect(deleteById(db, STORE_NAME, 'non-existent')).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteMany', () => {
|
||||
beforeEach(async () => {
|
||||
const records: TestRecord[] = [
|
||||
{ id: '1', name: 'First', value: 1 },
|
||||
{ id: '2', name: 'Second', value: 2 },
|
||||
{ id: '3', name: 'Third', value: 3 },
|
||||
];
|
||||
|
||||
for (const record of records) {
|
||||
await put(db, STORE_NAME, record);
|
||||
}
|
||||
});
|
||||
|
||||
it('should delete multiple records', async () => {
|
||||
const count = await deleteMany(db, STORE_NAME, ['1', '2']);
|
||||
|
||||
expect(count).toBe(2);
|
||||
|
||||
const remaining = await getAll<TestRecord>(db, STORE_NAME);
|
||||
expect(remaining).toHaveLength(1);
|
||||
expect(remaining[0].id).toBe('3');
|
||||
});
|
||||
|
||||
it('should handle empty array', async () => {
|
||||
const count = await deleteMany(db, STORE_NAME, []);
|
||||
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearStore', () => {
|
||||
it('should remove all records', async () => {
|
||||
const records: TestRecord[] = [
|
||||
{ id: '1', name: 'First', value: 1 },
|
||||
{ id: '2', name: 'Second', value: 2 },
|
||||
];
|
||||
|
||||
for (const record of records) {
|
||||
await put(db, STORE_NAME, record);
|
||||
}
|
||||
|
||||
await clearStore(db, STORE_NAME);
|
||||
|
||||
const all = await getAll<TestRecord>(db, STORE_NAME);
|
||||
expect(all).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should work on empty store', async () => {
|
||||
await expect(clearStore(db, STORE_NAME)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('countRecords', () => {
|
||||
it('should count records', async () => {
|
||||
const records: TestRecord[] = [
|
||||
{ id: '1', name: 'First', value: 1 },
|
||||
{ id: '2', name: 'Second', value: 2 },
|
||||
{ id: '3', name: 'Third', value: 3 },
|
||||
];
|
||||
|
||||
for (const record of records) {
|
||||
await put(db, STORE_NAME, record);
|
||||
}
|
||||
|
||||
const count = await countRecords(db, STORE_NAME);
|
||||
|
||||
expect(count).toBe(3);
|
||||
});
|
||||
|
||||
it('should return 0 for empty store', async () => {
|
||||
const count = await countRecords(db, STORE_NAME);
|
||||
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasStore', () => {
|
||||
it('should return true for existing store', () => {
|
||||
expect(hasStore(db, STORE_NAME)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-existent store', () => {
|
||||
expect(hasStore(db, 'non-existent-store')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
37
packages/utils/src/indexeddb/index.ts
Normal file
37
packages/utils/src/indexeddb/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* IndexedDB 工具模块
|
||||
*
|
||||
* 提供通用的 IndexedDB 操作工具函数,减少重复代码
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import {
|
||||
* openIndexedDB,
|
||||
* getById,
|
||||
* getAll,
|
||||
* getAllWithCursor,
|
||||
* put,
|
||||
* deleteById,
|
||||
* } from '@aitu/utils/indexeddb';
|
||||
*
|
||||
* // 打开数据库
|
||||
* const db = await openIndexedDB('my-database', { logPrefix: 'MyService' });
|
||||
*
|
||||
* // CRUD 操作
|
||||
* const task = await getById<Task>(db, 'tasks', 'task-123');
|
||||
* const allTasks = await getAll<Task>(db, 'tasks');
|
||||
* await put(db, 'tasks', newTask);
|
||||
* await deleteById(db, 'tasks', 'task-123');
|
||||
*
|
||||
* // 高级查询
|
||||
* const recentLogs = await getAllWithCursor<Log>(db, 'logs', {
|
||||
* indexName: 'timestamp',
|
||||
* direction: 'prev',
|
||||
* limit: 10,
|
||||
* filter: (log) => log.level === 'error',
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './operations';
|
||||
388
packages/utils/src/indexeddb/operations.ts
Normal file
388
packages/utils/src/indexeddb/operations.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
/**
|
||||
* IndexedDB 操作工具函数
|
||||
*
|
||||
* 提供通用的 IndexedDB CRUD 操作,减少重复代码
|
||||
*/
|
||||
|
||||
import type { OpenDBOptions, CursorOptions } from './types';
|
||||
|
||||
/**
|
||||
* 打开 IndexedDB 数据库
|
||||
*
|
||||
* @param dbName 数据库名称
|
||||
* @param options 打开选项
|
||||
* @returns Promise<IDBDatabase>
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const db = await openIndexedDB('my-database');
|
||||
* const db = await openIndexedDB('my-database', { version: 2, logPrefix: 'MyService' });
|
||||
* ```
|
||||
*/
|
||||
export function openIndexedDB(
|
||||
dbName: string,
|
||||
options: OpenDBOptions = {}
|
||||
): Promise<IDBDatabase> {
|
||||
const { version, logPrefix = 'IndexedDB', onUpgradeNeeded } = options;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = version ? indexedDB.open(dbName, version) : indexedDB.open(dbName);
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`[${logPrefix}] Failed to open IndexedDB: ${request.error?.message}`));
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result);
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
if (onUpgradeNeeded) {
|
||||
onUpgradeNeeded(request.result, event);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 ID 获取单条记录
|
||||
*
|
||||
* @param db 数据库连接
|
||||
* @param storeName store 名称
|
||||
* @param id 记录 ID
|
||||
* @returns Promise<T | null>
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const task = await getById<Task>(db, 'tasks', 'task-123');
|
||||
* ```
|
||||
*/
|
||||
export function getById<T>(
|
||||
db: IDBDatabase,
|
||||
storeName: string,
|
||||
id: string | number
|
||||
): Promise<T | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([storeName], 'readonly');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve((request.result as T) || null);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 store 中的所有记录
|
||||
*
|
||||
* @param db 数据库连接
|
||||
* @param storeName store 名称
|
||||
* @returns Promise<T[]>
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const tasks = await getAll<Task>(db, 'tasks');
|
||||
* ```
|
||||
*/
|
||||
export function getAll<T>(db: IDBDatabase, storeName: string): Promise<T[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([storeName], 'readonly');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve((request.result as T[]) || []);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用游标遍历记录(支持索引、方向、过滤和分页)
|
||||
*
|
||||
* @param db 数据库连接
|
||||
* @param storeName store 名称
|
||||
* @param options 游标选项
|
||||
* @returns Promise<T[]>
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 按时间倒序获取最近 10 条记录
|
||||
* const logs = await getAllWithCursor<Log>(db, 'logs', {
|
||||
* indexName: 'timestamp',
|
||||
* direction: 'prev',
|
||||
* limit: 10,
|
||||
* });
|
||||
*
|
||||
* // 过滤特定状态的记录
|
||||
* const pendingTasks = await getAllWithCursor<Task>(db, 'tasks', {
|
||||
* filter: (task) => task.status === 'pending',
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function getAllWithCursor<T>(
|
||||
db: IDBDatabase,
|
||||
storeName: string,
|
||||
options: CursorOptions<T> = {}
|
||||
): Promise<T[]> {
|
||||
const { indexName, direction = 'next', filter, limit, offset = 0 } = options;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([storeName], 'readonly');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const source = indexName ? store.index(indexName) : store;
|
||||
const request = source.openCursor(null, direction);
|
||||
|
||||
const results: T[] = [];
|
||||
let skipped = 0;
|
||||
|
||||
request.onsuccess = () => {
|
||||
const cursor = request.result;
|
||||
if (cursor) {
|
||||
const value = cursor.value as T;
|
||||
|
||||
// 应用过滤器
|
||||
const shouldInclude = !filter || filter(value);
|
||||
|
||||
if (shouldInclude) {
|
||||
// 处理 offset
|
||||
if (skipped < offset) {
|
||||
skipped++;
|
||||
} else {
|
||||
results.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否达到 limit
|
||||
if (limit && results.length >= limit) {
|
||||
resolve(results);
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.continue();
|
||||
} else {
|
||||
resolve(results);
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入或更新记录
|
||||
*
|
||||
* @param db 数据库连接
|
||||
* @param storeName store 名称
|
||||
* @param value 要保存的值
|
||||
* @returns Promise<void>
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await put(db, 'tasks', { id: 'task-123', name: 'My Task' });
|
||||
* ```
|
||||
*/
|
||||
export function put<T>(db: IDBDatabase, storeName: string, value: T): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([storeName], 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.put(value);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入或更新记录
|
||||
*
|
||||
* @param db 数据库连接
|
||||
* @param storeName store 名称
|
||||
* @param values 要保存的值数组
|
||||
* @returns Promise<void>
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await putMany(db, 'tasks', [task1, task2, task3]);
|
||||
* ```
|
||||
*/
|
||||
export function putMany<T>(db: IDBDatabase, storeName: string, values: T[]): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([storeName], 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
transaction.onerror = () => {
|
||||
reject(transaction.error);
|
||||
};
|
||||
|
||||
for (const value of values) {
|
||||
store.put(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除单条记录
|
||||
*
|
||||
* @param db 数据库连接
|
||||
* @param storeName store 名称
|
||||
* @param id 记录 ID
|
||||
* @returns Promise<void>
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await deleteById(db, 'tasks', 'task-123');
|
||||
* ```
|
||||
*/
|
||||
export function deleteById(
|
||||
db: IDBDatabase,
|
||||
storeName: string,
|
||||
id: string | number
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([storeName], 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除记录
|
||||
*
|
||||
* @param db 数据库连接
|
||||
* @param storeName store 名称
|
||||
* @param ids 记录 ID 数组
|
||||
* @returns Promise<number> 删除的记录数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const deletedCount = await deleteMany(db, 'tasks', ['task-1', 'task-2']);
|
||||
* ```
|
||||
*/
|
||||
export function deleteMany(
|
||||
db: IDBDatabase,
|
||||
storeName: string,
|
||||
ids: (string | number)[]
|
||||
): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([storeName], 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
let deletedCount = 0;
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
resolve(deletedCount);
|
||||
};
|
||||
|
||||
transaction.onerror = () => {
|
||||
reject(transaction.error);
|
||||
};
|
||||
|
||||
for (const id of ids) {
|
||||
const request = store.delete(id);
|
||||
request.onsuccess = () => {
|
||||
deletedCount++;
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空整个 store
|
||||
*
|
||||
* @param db 数据库连接
|
||||
* @param storeName store 名称
|
||||
* @returns Promise<void>
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await clearStore(db, 'tasks');
|
||||
* ```
|
||||
*/
|
||||
export function clearStore(db: IDBDatabase, storeName: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([storeName], 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.clear();
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取记录数量
|
||||
*
|
||||
* @param db 数据库连接
|
||||
* @param storeName store 名称
|
||||
* @returns Promise<number>
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const count = await count(db, 'tasks');
|
||||
* ```
|
||||
*/
|
||||
export function countRecords(db: IDBDatabase, storeName: string): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([storeName], 'readonly');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.count();
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 store 是否存在
|
||||
*
|
||||
* @param db 数据库连接
|
||||
* @param storeName store 名称
|
||||
* @returns boolean
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* if (hasStore(db, 'tasks')) {
|
||||
* // store exists
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function hasStore(db: IDBDatabase, storeName: string): boolean {
|
||||
return db.objectStoreNames.contains(storeName);
|
||||
}
|
||||
56
packages/utils/src/indexeddb/types.ts
Normal file
56
packages/utils/src/indexeddb/types.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* IndexedDB 工具类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 打开数据库的选项
|
||||
*/
|
||||
export interface OpenDBOptions {
|
||||
/** 数据库版本,默认不指定 */
|
||||
version?: number;
|
||||
/** 日志前缀 */
|
||||
logPrefix?: string;
|
||||
/** 升级回调 */
|
||||
onUpgradeNeeded?: (db: IDBDatabase, event: IDBVersionChangeEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store 配置(用于创建 store)
|
||||
*/
|
||||
export interface StoreConfig {
|
||||
name: string;
|
||||
keyPath: string;
|
||||
autoIncrement?: boolean;
|
||||
indexes?: IndexConfig[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 索引配置
|
||||
*/
|
||||
export interface IndexConfig {
|
||||
name: string;
|
||||
keyPath: string | string[];
|
||||
unique?: boolean;
|
||||
multiEntry?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游标遍历选项
|
||||
*/
|
||||
export interface CursorOptions<T = unknown> {
|
||||
/** 索引名称,不指定则使用 store 主键 */
|
||||
indexName?: string;
|
||||
/** 游标方向 */
|
||||
direction?: IDBCursorDirection;
|
||||
/** 过滤函数 */
|
||||
filter?: (value: T) => boolean;
|
||||
/** 限制返回数量 */
|
||||
limit?: number;
|
||||
/** 跳过前 N 条记录 */
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务模式
|
||||
*/
|
||||
export type TransactionMode = 'readonly' | 'readwrite';
|
||||
Reference in New Issue
Block a user