import ErrorException from "../../exceptions/ErrorException.js"; import { ILoadResult } from "../ILoadResult.js"; import { openDB, IDBPDatabase, IDBPTransaction } from 'idb'; import { IRepositoryIndex } from "./IRepositoryIndex.js"; import { IRepository } from "../IRepository.js"; /** * Abstração para simplificar o uso do indexed DB * * Além de facilitar os comandos de incluir, remover alterar e obter * objetos, esse repositório preserva a ordem de inserção na lista, * além de permitir filtros e ordenações compostas * (impossíveis com uso de índices). * */ export class IDBRepository implements IRepository{ private _dbName: string; private _dbVersion: number; private _storeName: string; private _addedStoreName: string; private _indexKeyPath: string; private _indexes?: Array; private _db?: IDBPDatabase; private _operating: boolean = true; /** * Construtor padrão * * @param dbName - Nome do DB * @param dbVersion - Versão do DB * @param storeName - Nome da store * @param indexKeyPath - Um atributo que identifique os objetos guardados */ constructor(dbName: string, dbVersion: number, storeName: string, indexKeyPath: string, indexes?: Array){ this._dbName = dbName; this._dbVersion = dbVersion; this._storeName = storeName; this._addedStoreName = `${this._storeName}_ADDED__[REPOSITORY]`; this._indexKeyPath = indexKeyPath; this._indexes = indexes; } public async load(filterFunction?: (value: T) => boolean, sortingFunction?: (itemA: T, itemB: T) => number, offset?: number, limit?: number): Promise>{ try{ const db = await this.openDB(); const addedItems: Array> = await this.getAddedItems(db); const tx = db.transaction(this._storeName); let cursor = await tx.objectStore(this._storeName).openCursor(); const collectedValues: Array = []; let itemsCount: number = 0; while (cursor) { appendResult( addedItems .filter(addedItem => addedItem.position === itemsCount) .map(addedItems => addedItems.item), collectedValues, filterFunction ); appendResult(cursor.value, collectedValues, filterFunction); itemsCount++; cursor = await cursor.continue(); } const returnValue = sortingFunction != undefined ? collectedValues.sort(sortingFunction) : collectedValues; const start = offset||0; const end = limit ? start + limit : returnValue.length; return Promise.resolve({result: returnValue.slice(start, end), count: collectedValues.length}); } catch (error){ console.error(error); throw new ErrorException("Erro ao buscar itens", `Não foi possível listar a store "${this._storeName}". Verifique o console para maiores detalhes.`); } } public async distict(itemProcessor: (item: T) => {key: string, value: any}): Promise>{ try{ const db = await this.openDB(); const tx = db.transaction([this._storeName, this._addedStoreName]); const itemCollector = { result: new Map, collect: (item: T) => { const processedItem = itemProcessor(item); if(processedItem != undefined){ itemCollector.result.set(processedItem.key, processedItem.value); } } }; await iterateStore(this._storeName, tx, value => itemCollector.collect(value)); await iterateStore(this._addedStoreName, tx, value => itemCollector.collect(value.item)); return Promise.resolve(itemCollector.result); } catch (error){ console.error(error); throw new ErrorException("Erro ao buscar distinct", `Não foi possível listar a store "${this._storeName}". Verifique o console para maiores detalhes.`); } } public async push(items: Array): Promise{ if(items == undefined || items.length === 0){ return Promise.resolve(); } try{ const db = await this.openDB(); return new Promise((accept, reject) =>{ Promise.all(items.map(item => db.add(this._storeName, item))) .then(()=>accept()) .catch(reason => { console.error(reason); reject(new ErrorException("Erro ao adicionar registro", `Não foi possível adicionar registros no repositório ${this._storeName}. Verifique o console para maiores detalhes.`)); }) }); } catch (error){ console.error(error); throw new ErrorException("Erro ao adicionar registro", `Não foi possível adicionar registros no repositório ${this._storeName}. Verifique o console para maiores detalhes.`); } } public async clear(): Promise{ try{ const db = await this.openDB(); [this._storeName, this._addedStoreName].forEach(storeName => { if(db.objectStoreNames.contains(storeName)){ db.clear(storeName); } }); return Promise.resolve(); } catch (error){ console.error(error); throw new ErrorException("Erro ao limpar repositório", `Limpar o repositório ${this._storeName}. Verifique o console para maiores detalhes.`); } } public async delete(items: Array): Promise{ try{ const db = await this.openDB(); const tx = db.transaction([this._storeName, this._addedStoreName], "readwrite"); const addedStore = tx.objectStore(this._addedStoreName); const store = tx.objectStore(this._storeName); items.forEach(async item => { let key = await addedStore.index(this._indexKeyPath).getKey(IDBKeyRange.only((item as any)[this._indexKeyPath])); if(key != undefined){ addedStore.delete(key); } else { key = await store.index(this._indexKeyPath).getKey(IDBKeyRange.only((item as any)[this._indexKeyPath])); if(key == undefined){ throw new ErrorException("Erro interno: Erro ao remover itens", "Tentando remover um item que não está no repositório."); } store.delete(key); } }); return Promise.resolve(); } catch (error){ console.error(error); throw new ErrorException("Erro ao remover itens", `Verifique o console para maiores detalhes.`); } } public async update(items: Array): Promise{ try{ const db = await this.openDB(); const tx = db.transaction([this._storeName, this._addedStoreName], "readwrite"); const addedStore = tx.objectStore(this._addedStoreName); const store = tx.objectStore(this._storeName); items.forEach(async item => { let key = await addedStore.index(this._indexKeyPath).getKey(IDBKeyRange.only((item as any)[this._indexKeyPath])); if(key != undefined){ const {position} = await addedStore.get(IDBKeyRange.only(key)) as IAddedItem; addedStore.put({position, item}, key); } else { key = await store.index(this._indexKeyPath).getKey(IDBKeyRange.only((item as any)[this._indexKeyPath])); if(key == undefined){ throw new ErrorException("Erro interno: Erro ao alterar itens", "Tentando modificar um item que não está no repositório."); } store.put(item, key); } }); return Promise.resolve(); } catch (error){ console.error(error); throw new ErrorException("Erro ao alterar itens", `Verifique o console para maiores detalhes.`); } } public async insert(itemReference: T, items: Array): Promise{ try{ const db = await this.openDB(); const position = await this.getItemPosition(db, item => item[this._indexKeyPath] === (itemReference as any)[this._indexKeyPath]); if(position == -1){ console.error(itemReference); return Promise.reject("Erro interno: Item de referência não localizado na inclusão"); } items.forEach(item =>{ const tx = db.transaction(this._addedStoreName, "readwrite"); tx.objectStore(this._addedStoreName).add({item, position}); }); return Promise.resolve(); } catch (error){ console.error(error); throw new ErrorException("Falha ao tentar inserir item", `Verifique o console para maiores detalhes.`); } } public isOperating(): boolean{ return this._operating; } public async isEmpty(): Promise{ try{ if((await indexedDB.databases()).find(db => db.name === this._dbName) == undefined){ return Promise.resolve(false); } const db = await this.openDB(); if(!db.objectStoreNames.contains(this._storeName)){ return Promise.resolve(false); } const tx = db.transaction(this._storeName); const store = tx.objectStore(this._storeName); return Promise.resolve(await store.count() === 0); } catch (error){ console.error(error); throw new ErrorException("Falha ao verificar se o repositório está vazio", `Verifique o console para maiores detalhes.`); } } public async count(): Promise{ try{ if((await indexedDB.databases()).find(db => db.name === this._dbName) == undefined){ return Promise.resolve(0); } const db = await this.openDB(); const tx = db.transaction([this._storeName, this._addedStoreName]); return Promise.resolve(await tx.objectStore(this._storeName).count() + await tx.objectStore(this._addedStoreName).count()); } catch (error){ console.error(error); throw new ErrorException("Falha ao contar itens do repositório", `Verifique o console para maiores detalhes.`); } } private async openDB(): Promise{ try{ if(this._db){ return this._db; } this._db = await openDB(this._dbName, this._dbVersion, { upgrade: (db) => { this.createStore(db, this._storeName); this.createStore(db, this._addedStoreName, "item."); }, terminated: () => { this._operating = false; } }); return this._db; } catch (error){ console.error(error); throw new ErrorException("Erro interno: Falha ao tentar abrir o indexedDB", `Verifique o console para maiores detalhes.`); } } private createStore(db: IDBPDatabase, name: string, indexPrefix: string = "") { if(!db.objectStoreNames.contains(name)){ const store = db.createObjectStore(name, {autoIncrement: true}); store.createIndex(this._indexKeyPath, `${indexPrefix}${this._indexKeyPath}`, {unique: true}); if(this._indexes == undefined || this._indexes.length === 0){ return; } this._indexes.forEach(index => { store.createIndex(index.name, `${indexPrefix}${index.path}`, index); }); } } private async getItemPosition(db: IDBPDatabase, itemTest: (item: any) => boolean): Promise{ const addedIndex = await this.getItemPositionByAdded(db, itemTest); if(addedIndex > -1){ return Promise.resolve(addedIndex); } const tx = db.transaction(this._storeName); let cursor = await tx.objectStore(this._storeName).openCursor(); let found: boolean = false; let position = 0; while (cursor) { if(itemTest(cursor.value)){ found = true; break; } position++; cursor = await cursor.continue(); } return Promise.resolve(found ? position : -1); } private async getItemPositionByAdded(db: IDBPDatabase, itemTest: (item: any) => boolean): Promise{ const addedItems: Array> = await this.getAddedItems(db); if(addedItems && addedItems.length > 0){ const foundItem = addedItems.find(addedItem => itemTest(addedItem.item)) if(foundItem != null){ return Promise.resolve(foundItem.position); } } return Promise.resolve(-1); } private async getAddedItems(db: IDBPDatabase): Promise>>{ const tx = db.transaction(this._addedStoreName); return tx.objectStore(this._addedStoreName).getAll(); } public getFromCache(): Array | undefined{ return undefined; } } /** * Metodo utilitário usado para iterar uma store. */ const iterateStore = async (storeName: string, tx: IDBPTransaction, callBack: (value: any) => void) =>{ let cursor = await tx.objectStore(storeName).openCursor(); while (cursor) { callBack(cursor.value); cursor = await cursor.continue(); } } /** * Metodo utilitário usado para simplificar a lógica de coletar valores considerando um filtro. */ const appendResult = (toAppend: Array|T, collectedValues: Array, filterFunction?: (value: T) => boolean) => { if(toAppend != undefined){ (Array.isArray(toAppend) ? toAppend : [toAppend]).forEach(item => { if(filterFunction == undefined || filterFunction(item)){ collectedValues.push(item); } }); } } /** * Interface usada para armazenar items adicionados e a posição onde eles devem ficar. */ interface IAddedItem{ item: T; position: number }