diff --git a/index.html b/index.html index 7236d42..6d4f724 100644 --- a/index.html +++ b/index.html @@ -1,15 +1,29 @@ diff --git a/package.json b/package.json index 9babd1c..7ba44db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ztimson/utils", - "version": "0.25.2", + "version": "0.25.3", "description": "Utility library", "author": "Zak Timson", "license": "MIT", diff --git a/src/cache.ts b/src/cache.ts index 15ad5c8..b6c0446 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -1,11 +1,11 @@ -import {Collection} from './collection.ts'; +import {Table} from './database.ts'; import {deepCopy, JSONSanitize} from './objects.ts'; export type CacheOptions = { /** Delete keys automatically after x amount of seconds */ ttl?: number; /** Storage to persist cache */ - storage?: Storage | Collection; + storage?: Storage | Table; /** Key cache will be stored under */ storageKey?: string; /** Keep or delete cached items once expired, defaults to delete */ @@ -33,10 +33,11 @@ export class Cache { constructor(public readonly key?: keyof T, public readonly options: CacheOptions = {}) { if(options.storageKey && !options.storage && typeof(Storage) !== 'undefined') options.storage = localStorage; if(options.storage) { - if(options.storage instanceof Collection) { - (async () => { - (await options.storage?.getAll()).forEach((v: any) => this.add(v)); - })() + if(options.storage instanceof Table) { + (async () => (await options.storage?.getAll()).forEach((v: any) => { + console.log(v); + this.add(v) + }))() } else if(options.storageKey) { const stored = options.storage?.getItem(options.storageKey); if(stored != null) try { Object.assign(this.store, JSON.parse(stored)); } catch { } @@ -62,7 +63,7 @@ export class Cache { private save(key: K) { if(this.options.storage) { - if(this.options.storage instanceof Collection) { + if(this.options.storage instanceof Table) { this.options.storage.put(key, this.store[key]); } else if(this.options.storageKey) { this.options.storage.setItem(this.options.storageKey, JSONSanitize(this.store)); @@ -138,14 +139,17 @@ export class Cache { */ expire(key: K): this { this.complete = false; - if(this.options.expiryPolicy == 'keep') (this.store[key])._expired = true; - else this.delete(key); + if(this.options.expiryPolicy == 'keep') { + (this.store[key])._expired = true; + this.save(key); + } else this.delete(key); return this; } /** * Get item from the cache * @param {K} key Key to lookup + * @param expired Include expired items * @return {T} Cached item */ get(key: K, expired?: boolean): CachedValue | null { diff --git a/src/collection.ts b/src/collection.ts deleted file mode 100644 index c458417..0000000 --- a/src/collection.ts +++ /dev/null @@ -1,59 +0,0 @@ -export class Collection { - private database: Promise; - - constructor(public readonly db: string, public readonly collection: string, public readonly version?: number, private setup?: (db: IDBDatabase, event: IDBVersionChangeEvent) => void) { - this.database = new Promise((resolve, reject) => { - const req = indexedDB.open(this.db, this.version); - req.onerror = () => reject(req.error); - req.onsuccess = () => resolve(req.result); - req.onupgradeneeded = (event) => { - const db = req.result; - if(!db.objectStoreNames.contains(collection)) db.createObjectStore(collection, {keyPath: undefined}); - if (this.setup) this.setup(db, event as IDBVersionChangeEvent); - }; - }); - } - - async tx(collection: string, fn: ((store: IDBObjectStore) => IDBRequest), readonly?: boolean): Promise { - const db = await this.database; - const tx = db.transaction(collection, readonly ? 'readonly' : 'readwrite'); - const store = tx.objectStore(collection); - return new Promise((resolve, reject) => { - const request = fn(store); - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); - }); - } - - add(value: T, key?: K): Promise { - return this.tx(this.collection, store => store.add(value, key)); - } - - count(): Promise { - return this.tx(this.collection, store => store.count(), true); - } - - put(key: K, value: T): Promise { - return this.tx(this.collection, store => store.put(value, key)); - } - - getAll(): Promise { - return this.tx(this.collection, store => store.getAll(), true); - } - - getAllKeys(): Promise { - return this.tx(this.collection, store => store.getAllKeys(), true); - } - - get(key: K): Promise { - return this.tx(this.collection, store => store.get(key), true); - } - - delete(key: K): Promise { - return this.tx(this.collection, store => store.delete(key)); - } - - clear(): Promise { - return this.tx(this.collection, store => store.clear()); - } -} diff --git a/src/database.ts b/src/database.ts new file mode 100644 index 0000000..90020db --- /dev/null +++ b/src/database.ts @@ -0,0 +1,94 @@ +export type TableOptions = { + name: string; + key?: string; +}; + +export class Database { + connection!: Promise; + + constructor(public readonly database: string, public readonly tables: (string | TableOptions)[], public version?: number) { + this.connection = new Promise((resolve, reject) => { + const req = indexedDB.open(this.database, this.version); + + req.onerror = () => reject(req.error); + + req.onsuccess = () => { + const db = req.result; + if(tables.find(s => !db.objectStoreNames.contains(typeof s === 'string' ? s : s.name))) { + db.close(); + Object.assign(this, new Database(this.database, this.tables, db.version + 1)); + } else { + this.version = db.version; + resolve(db); + } + }; + + req.onupgradeneeded = () => { + const db = req.result; + Array.from(db.objectStoreNames) + .filter(s => !this.tables.find(t => typeof t === 'string' ? t : t.name == s)) + .forEach(name => db.deleteObjectStore(name)); + tables.filter(t => !db.objectStoreNames.contains(typeof t === 'string' ? t : t.name)) + .forEach(t => {db.createObjectStore(typeof t === 'string' ? t : t.name, { + keyPath: typeof t === 'string' ? undefined : t.key + }); + }); + }; + }); + } + + includes(name: string): boolean { + return this.tables.some(t => (typeof t === 'string' ? name === t : name === t.name)); + } + + table(name: string): Table { + return new Table(this, name); + } +} + +export class Table { + constructor(private readonly database: Database, public readonly name: string) {} + + async tx(schema: string, fn: (store: IDBObjectStore) => IDBRequest, readonly = false): Promise { + const db = await this.database.connection; + const tx = db.transaction(schema, readonly ? 'readonly' : 'readwrite'); + const store = tx.objectStore(schema); + return new Promise((resolve, reject) => { + const request = fn(store); + request.onsuccess = () => resolve(request.result as R); // ✅ explicit cast + request.onerror = () => reject(request.error); + }); + } + + add(value: T, key?: K): Promise { + return this.tx(this.name, store => store.add(value, key)); + } + + count(): Promise { + return this.tx(this.name, store => store.count(), true); + } + + put(key: K, value: T): Promise { + return this.tx(this.name, store => store.put(value, key)); + } + + getAll(): Promise { + return this.tx(this.name, store => store.getAll(), true); + } + + getAllKeys(): Promise { + return this.tx(this.name, store => store.getAllKeys(), true); + } + + get(key: K): Promise { + return this.tx(this.name, store => store.get(key), true); + } + + delete(key: K): Promise { + return this.tx(this.name, store => store.delete(key)); + } + + clear(): Promise { + return this.tx(this.name, store => store.clear()); + } +} diff --git a/src/index.ts b/src/index.ts index aeae0bf..4655739 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ export * from './aset'; export * from './cache'; export * from './color'; export * from './csv'; -export * from './collection'; +export * from './database'; export * from './files'; export * from './emitter'; export * from './errors'; diff --git a/tests/collection.spec.ts b/tests/collection.spec.ts deleted file mode 100644 index b759274..0000000 --- a/tests/collection.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import {Collection} from '../src'; -import 'fake-indexeddb/auto'; - -describe('IndexedDb and Collection', () => { - const data = {id: 1, name: 'Alice', age: 30, email: ''}; - const data2 = {id: 2, name: 'Bob', age: 39, email: ''}; - let collection = new Collection('database', 'collection'); - - afterEach(() => collection.clear()); - - test('can set and get an item', async () => { - await collection.put(data.id, data); - const result = await collection.get(data.id); - expect(result).toEqual(data); - }); - - test('can remove an item', async () => { - await collection.put(data.id, data); - await collection.put(data2.id, data2); - await collection.delete(data.id); - const result = await collection.get(data.id); - const result2 = await collection.get(data2.id); - expect(result).toBeUndefined(); - expect(result2).toEqual(data2); - }); - - test('can clear all items', async () => { - await collection.put(data.id, data); - await collection.put(data2.id, data2); - await collection.clear(); - const result = await collection.get(data.id); - const result2 = await collection.get(data2.id); - expect(result).toBeUndefined(); - expect(result2).toBeUndefined(); - }); - - test('getItem on missing key returns undefined', async () => { - const result = await collection.get(-1); - expect(result).toBeUndefined(); - }); - - test('count returns number of items', async () => { - expect(await collection.count()).toBe(0); - await collection.put(data.id, data); - expect(await collection.count()).toBe(1); - await collection.delete(data.id); - expect(await collection.count()).toBe(0); - }); - - test('can get all items', async () => { - await collection.put(data.id, data); - await collection.put(data2.id, data2); - const allItems = await collection.getAll(); - expect(allItems).toEqual(expect.arrayContaining([data, data2])); - expect(allItems.length).toBe(2); - }); - - test('can get all keys', async () => { - await collection.put(data.id, data); - await collection.put(data2.id, data2); - const keys = await collection.getAllKeys(); - expect(keys).toEqual(expect.arrayContaining([data.id, data2.id])); - expect(keys.length).toBe(2); - }); -});