import levenshtein from "fast-levenshtein" ; import StorageInterface from "../storage/StorageInterface" ; import { buildSearcher } from "./searcher" ; import Vault from "../core/Vault" ; import { EntryID , EntryType , GroupID , VaultFacade , VaultID } from "../types" ; interface DomainScores { [ domain ] : number ; } type EntryFetcher = ( target : Vault | VaultFacade , memory : StorageInterface ) => Promise < Array < ProcessedSearchEntry > > ; export interface ProcessedSearchEntry extends SearchResult { domainScores : DomainScores ; } export interface SearcherFactory { ( items ) : any ; } export interface SearchResult { id : EntryID ; groupID : GroupID ; properties : { [ property ] : string ; } ; entryType : EntryType ; urls : Array < string > ; vaultID : VaultID ; } interface SearchScores { [ vaultID ] : { [ entryID ] : DomainScores ; } ; } function domainsRelated ( domain1 , domain2 ) { if ( domain1 === domain2 ) return true ; if ( domain1 . length > domain2 . length ) { const ind = domain1 . indexOf ( domain2 ) ; return ind === domain1 . length - domain2 . length ; } else { const ind = domain2 . indexOf ( domain1 ) ; return ind === domain2 . length - domain1 . length ; } } function extractDomain ( str ) { const match = /^((https?|ftp):\/\/)?([^\/]+)/i . exec ( str ) ; return ( match && match [ 3 ] ) || "" ; } export class BaseSearch { _entries : Array < ProcessedSearchEntry > = [ ] ; _entryFetcher : EntryFetcher ; _fuse : any = null ; _memory : StorageInterface ; _results : Array < SearchResult > = [ ] ; _searcherFactory : SearcherFactory ; _scores : SearchScores = { } ; _targets : Array < Vault > | Array < VaultFacade > ; constructor ( targets , entryFetcher , memory = new StorageInterface ( ) , searcherFactory = buildSearcher ) { this . _targets = targets ; this . _entryFetcher = entryFetcher ; this . _memory = memory ; this . _searcherFactory = searcherFactory ; } /**
     * Last search results
     */ get results ( ) : Array < SearchResult > { return this . _results ; } param vaultID param entryID param url async incrementScore ( vaultID , entryID , url ) { const scoresRaw = await this . _memory . getValue ( `bcup_search_${ vaultID }` ) ; let vaultScore = { } ; if ( scoresRaw ) { try { vaultScore = JSON . parse ( scoresRaw ) ; } catch ( err ) { } } const domain = extractDomain ( url ) ; if ( ! domain ) return ; vaultScore [ entryID ] = vaultScore [ entryID ] || { } ; vaultScore [ entryID ] [ domain ] = vaultScore [ entryID ] [ domain ] ? vaultScore [ entryID ] [ domain ] + 1 : 1 ; await this . _memory . setValue ( `bcup_search_${ vaultID }` , JSON . stringify ( vaultScore ) ) ; } /**
     * Prepare the search instance by processing
     * entries
     */ async prepare ( ) { this . _entries = [ ] ; this . _scores = { } ; for ( const target of this . _targets ) { const scoresRaw = await this . _memory . getValue ( `bcup_search_${ target . id }` ) ; if ( scoresRaw ) { try { const scores = JSON . parse ( scoresRaw ) ; this . _scores [ target . id ] = scores ; } catch ( err ) { } } const entries = await this . _entryFetcher ( target , this . _memory ) ; this . _entries . push ( ... entries ) ; } this . _fuse = this . _searcherFactory ( this . _entries ) ; } param term returns searchByTerm ( term ) { if ( ! this . _fuse ) { throw new Error ( "Searching interface not prepared" ) ; } this . _results = this . _fuse . search ( term ) . map ( result => result . item ) ; return this . _results ; } param url returns searchByURL ( url ) { const incomingDomain = extractDomain ( url ) ; if ( ! incomingDomain ) { this . _results = [ ] ; return this . _results ; } const results = this . _entries . reduce ( ( output , entryItem ) => { let bestScore = Infinity ; const bestURL = entryItem . urls . reduce ( ( best , next ) => { const thisDomain = extractDomain ( next ) ; if ( ! thisDomain ) return best ; if ( ! domainsRelated ( thisDomain , incomingDomain ) ) return best ; const score = levenshtein . get ( next , url ) ; if ( score < bestScore ) { bestScore = score ; return next ; } return best ; } , null ) ; if ( ! bestURL ) return output ; const resultDomain = extractDomain ( bestURL ) ; return [ ... output , { item : entryItem , score : bestScore , url : bestURL , domainScore : Math . max ( entryItem . domainScores [ incomingDomain ] || 0 , entryItem . domainScores [ resultDomain ] || 0 ) } ] ; } , [ ] ) ; this . _results = results . sort ( ( a , b ) => { if ( a . domainScore > b . domainScore ) { return - 1 ; } else if ( b . domainScore > a . domainScore ) { return 1 ; } if ( a . score > b . score ) { return 1 ; } else if ( b . score > a . score ) { return - 1 ; } return 0 ; } ) . map ( result => result . item ) ; return this . _results ; } }