import { CRUDEngine , error as StoreEngineError } from '@wireapp/store-engine' ; import * as fs from 'bro-fs' ; import * as path from 'path' ; export interface FileSystemEngineOptions { fileExtension : string ; size : number ; type : number ; } const TEN_MEGABYTES = 1024 * 1024 * 10 ; export class FileSystemEngine implements CRUDEngine { public storeName = '' ; private autoIncrementedPrimaryKey : number = 1 ; private config : FileSystemEngineOptions = { fileExtension : '.dat' , size : TEN_MEGABYTES , type : typeof window === 'undefined' ? 0 : window . TEMPORARY , } ; constructor ( ) { } async isSupported ( ) { if ( typeof window === 'undefined' || ! fs . isSupported ( ) ) { const message = `File and Directory Entries API is not available on your platform.` ; throw new StoreEngineError . UnsupportedError ( message ) ; } } async init ( storeName = '' , options ? ) { await this . isSupported ( ) ; this . config = { ... this . config , ... options } ; this . storeName = storeName ; const fileSystem = await fs . init ( { bytes : this . config . size , type : this . config . type } ) ; await fs . mkdir ( this . storeName ) ; return fileSystem ; } public async clearTables ( ) { const files = await fs . readdir ( this . storeName ) ; const tableNames = files . map ( file => path . join ( this . storeName , file . name ) ) ; await Promise . all ( tableNames . map ( tableName => this . deleteAll ( tableName ) ) ) ; } private createDirectoryPath ( tableName ) { return `${ this . storeName }/${ tableName }` ; } private createFilePath < PrimaryKey = string > ( tableName , primaryKey ) { const directory = this . createDirectoryPath ( tableName ) ; return `${ directory }/${ primaryKey }${ this . config . fileExtension }` ; } async create < EntityType = Object , PrimaryKey = string > ( tableName , primaryKey , entity ) { if ( ! entity ) { const message = `Record "${ primaryKey }" cannot be saved in "${ tableName }" because it's "undefined" or "null".` ; throw new StoreEngineError . RecordTypeError ( message ) ; } if ( primaryKey === undefined ) { primaryKey = this . autoIncrementedPrimaryKey as unknown as PrimaryKey ; this . autoIncrementedPrimaryKey += 1 ; } const filePath = this . createFilePath ( tableName , primaryKey ) ; const isExistent = await fs . exists ( filePath ) ; if ( isExistent ) { const message = `Record "${ primaryKey }" already exists in "${ tableName }". You need to delete the record first if you want to overwrite it.` ; throw new StoreEngineError . RecordAlreadyExistsError ( message ) ; } else { let data ; try { data = JSON . stringify ( entity ) ; } catch ( error ) { data = String ( entity ) ; } await fs . writeFile ( filePath , data ) ; return primaryKey ; } } async delete < PrimaryKey = string > ( tableName , primaryKey ) { const filePath = this . createFilePath ( tableName , primaryKey ) ; await fs . unlink ( filePath ) ; return primaryKey ; } async deleteAll ( tableName ) { const primaryKeys = await this . readAllPrimaryKeys ( tableName ) ; const promises = primaryKeys . map ( primaryKey => this . delete ( tableName , primaryKey ) ) ; try { await Promise . all ( promises ) ; return true ; } catch ( error ) { return false ; } } async read < EntityType = Object , PrimaryKey = string > ( tableName , primaryKey ) { const filePath = this . createFilePath ( tableName , primaryKey ) ; let data ; try { data = await fs . readFile ( filePath , { type : 'Text' } ) ; } catch ( error ) { const message = `Record "${ primaryKey }" in "${ tableName }" could not be found.` ; throw new StoreEngineError . RecordNotFoundError ( message ) ; } try { const parsed = JSON . parse ( data ) ; return parsed ; } catch ( error ) { return data as any ; } } async readAll < T > ( tableName ) { const primaryKeys = await this . readAllPrimaryKeys ( tableName ) ; const promises = [ ] ; for ( const primaryKey of primaryKeys ) { promises . push ( this . read ( tableName , primaryKey ) ) ; } return Promise . all ( promises ) ; } async readAllPrimaryKeys ( tableName ) { const directoryPath = this . createDirectoryPath ( tableName ) ; let entries ; try { entries = await fs . readdir ( directoryPath , { deep : true } ) ; } catch ( error ) { entries = [ ] ; } const names = entries . map ( ( entry ) => `${ entry . name }` ) ; const primaryKeys = [ ] ; for ( const name of names . sort ( ) ) { const nameWithoutExtension = name . substr ( 0 , name . indexOf ( '.' ) ) ; primaryKeys . push ( nameWithoutExtension ) ; } return primaryKeys ; } async update < PrimaryKey = string , ChangesType = Object > ( tableName , primaryKey , changes ) { const filePath = this . createFilePath ( tableName , primaryKey ) ; let record = await this . read ( tableName , primaryKey ) ; if ( typeof record === 'string' ) { record = JSON . parse ( record ) ; } const updatedRecord = { ... record , ... changes } ; const updatedRecord_1 = JSON . stringify ( updatedRecord ) ; await fs . writeFile ( filePath , updatedRecord_1 ) ; return primaryKey ; } async purge ( ) { await fs . rmdir ( this . storeName ) ; } async updateOrCreate < PrimaryKey = string , ChangesType = Object > ( tableName , primaryKey , changes ) { let internalPrimaryKey ; try { internalPrimaryKey = await this . update ( tableName , primaryKey , changes ) ; } catch ( error ) { if ( error instanceof StoreEngineError . RecordNotFoundError || ( error as Error ) . constructor . name === StoreEngineError . RecordNotFoundError . name ) { internalPrimaryKey = await this . create ( tableName , primaryKey , changes ) ; } else { throw error ; } } return internalPrimaryKey ; } }