Skip to content

Commit

Permalink
Merge pull request #340 from buttercup/vault_source_entry_search
Browse files Browse the repository at this point in the history
Vault source entry search
  • Loading branch information
perry-mitchell authored Feb 6, 2024
2 parents 1045cfc + 01bbdf5 commit a973868
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 61 deletions.
1 change: 1 addition & 0 deletions source/index.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export { SearchResult } from "./search/BaseSearch.js";
export { VaultEntrySearch as Search } from "./search/VaultEntrySearch.js"; // compat @todo remove
export { VaultEntrySearch } from "./search/VaultEntrySearch.js";
export { VaultFacadeEntrySearch } from "./search/VaultFacadeEntrySearch.js";
export { VaultSourceEntrySearch } from "./search/VaultSourceEntrySearch.js";
export { SearchKey, buildSearcher } from "./search/searcher.js";

export { AppEnv, AppEnvGetPropertyOptions } from "./env/core/appEnv.js";
Expand Down
14 changes: 12 additions & 2 deletions source/search/BaseSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import levenshtein from "fast-levenshtein";
import { StorageInterface } from "../storage/StorageInterface.js";
import { buildSearcher } from "./searcher.js";
import { Vault } from "../core/Vault.js";
import { EntryID, EntryType, GroupID, VaultFacade, VaultID } from "../types.js";
import { extractTagsFromSearchTerm, tagsMatchSearch } from "./tags.js";
import { EntryID, EntryType, GroupID, VaultFacade, VaultID, VaultSourceID } from "../types.js";

interface DomainScores {
[domain: string]: number;
Expand All @@ -28,6 +28,7 @@ export interface SearchResult {
id: EntryID;
properties: { [property: string]: string };
tags: Array<string>;
sourceID?: VaultSourceID;
urls: Array<string>;
vaultID: VaultID;
}
Expand Down Expand Up @@ -78,11 +79,20 @@ export class BaseSearch {

/**
* Last search results
* @deprecated Use `getResults` instead
*/
get results(): Array<SearchResult> {
return this._results;
}

/**
* Get last search results
* @returns An array of results
*/
getResults(): Array<SearchResult> {
return this._results;
}

/**
* Increment the score of a URL in an entry
* @param vaultID The vault ID
Expand Down Expand Up @@ -110,7 +120,7 @@ export class BaseSearch {
* Prepare the search instance by processing
* entries
*/
async prepare() {
async prepare(): Promise<void> {
this._entries = [];
this._scores = {};
for (const target of this._targets) {
Expand Down
82 changes: 82 additions & 0 deletions source/search/VaultSourceEntrySearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { SearchResult, SearcherFactory } from "./BaseSearch.js";
import { VaultSource } from "../core/VaultSource.js";
import { Vault } from "../core/Vault.js";
import { StorageInterface } from "../storage/StorageInterface.js";
import { VaultEntrySearch } from "./VaultEntrySearch.js";
import { VaultSourceStatus } from "../types.js";

export class VaultSourceEntrySearch extends VaultEntrySearch {
_sources: Array<VaultSource>;

constructor(
sources: Array<VaultSource>,
memory?: StorageInterface,
searcherFactory?: SearcherFactory
) {
const vaults: Array<Vault> = sources.reduce((output, source) => {
if (source.status === VaultSourceStatus.Unlocked) {
return [...output, source.vault];
}
return output;
}, []);
super(vaults, memory, searcherFactory);
this._sources = [...sources];
}

/**
* Last search results
* @deprecated Use `getResults` instead
*/
get results(): Array<SearchResult> {
return this.getResults();
}

/**
* Get last search results
* @returns An array of results
*/
getResults(): Array<SearchResult> {
return super.getResults().map((res) => {
const output = res;
const source = this._sources.find((src) => src?.vault?.id === output.vaultID);
if (source) {
output.sourceID = source.id;
}
return output;
});
}

/**
* Search for entries by term
* @param term The term to search for
* @returns An array of search results
*/
searchByTerm(term: string): Array<SearchResult> {
const results = super.searchByTerm(term);
return results.map((res) => {
const output = res;
const source = this._sources.find((src) => src?.vault?.id === output.vaultID);
if (source) {
output.sourceID = source.id;
}
return output;
});
}

/**
* Search for entries by URL
* @param url The URL to search with
* @returns An array of search results
*/
searchByURL(url: string): Array<SearchResult> {
const results = super.searchByURL(url);
return results.map((res) => {
const output = res;
const source = this._sources.find((src) => src?.vault?.id === output.vaultID);
if (source) {
output.sourceID = source.id;
}
return output;
});
}
}
62 changes: 3 additions & 59 deletions test/integration/search/VaultEntrySearch.spec.js
Original file line number Diff line number Diff line change
@@ -1,66 +1,10 @@
import { expect } from "chai";
import {
Entry,
EntryType,
Group,
MemoryStorageInterface,
Vault,
VaultEntrySearch
} from "../../../dist/node/index.js";
import { EntryType, MemoryStorageInterface, VaultEntrySearch } from "../../../dist/node/index.js";
import { createSampleVault } from "./helpers.js";

describe("VaultEntrySearch", function () {
beforeEach(function () {
const vault = (this.vault = new Vault());
const groupA = vault.createGroup("Email");
groupA
.createEntry("Personal Mail")
.setProperty("username", "[email protected]")
.setProperty("password", "df98Sm2.109x{91")
.setProperty("url", "https://fastmail.com")
.setAttribute(Entry.Attributes.FacadeType, EntryType.Website);
groupA
.createEntry("Work")
.setProperty("username", "[email protected]")
.setProperty("password", "#f05c.*skU3")
.setProperty("URL", "gmov.edu.au/portal/auth")
.addTags("job");
groupA
.createEntry("Work logs")
.setProperty("username", "[email protected]")
.setProperty("password", "#f05c.*skU3")
.setProperty("URL", "https://logs.gmov.edu.au/sys30/atc.php")
.addTags("job");
const groupB = vault.createGroup("Bank");
groupB
.createEntry("MyBank")
.setProperty("username", "324654356346")
.setProperty("PIN", "1234")
.setAttribute(Entry.Attributes.FacadeType, EntryType.Login)
.addTags("finance", "banking");
groupB
.createEntry("Insurance")
.setProperty("username", "testing-user")
.setProperty("URL", "http://test.org/portal-int/login.aspx")
.addTags("finance");
const groupC = vault.createGroup("General");
groupC
.createEntry("Clipart")
.setProperty("username", "gmonkey123")
.setProperty("password", "test93045")
.setProperty("Url", "clipart.com");
groupC
.createEntry("Wordpress")
.setProperty("username", "gmonkey1234")
.setProperty("password", "passw0rd")
.setProperty("Url", "https://wordpress.com/")
.setProperty("Login URL", "https://wordpress.com/account/login.php");
const trashGroup = vault.createGroup("Trash");
trashGroup
.createEntry("Ebay")
.setProperty("username", "[email protected]")
.setProperty("password", "passw0rd")
.setProperty("Url", "https://ebay.com/");
trashGroup.setAttribute(Group.Attribute.Role, "trash");
this.vault = createSampleVault();
});

it("can be instantiated", function () {
Expand Down
119 changes: 119 additions & 0 deletions test/integration/search/VaultSourceEntrySearch.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { expect } from "chai";
import {
EntryType,
MemoryStorageInterface,
VaultSourceEntrySearch
} from "../../../dist/node/index.js";
import { createSampleManager } from "./helpers.js";

describe("VaultSourceEntrySearch", function () {
beforeEach(async function () {
const [, source] = await createSampleManager();
this.source = source;
this.vault = source.vault;
});

it("can be instantiated", function () {
expect(() => {
new VaultSourceEntrySearch([this.source]);
}).to.not.throw();
});

describe("instance", function () {
beforeEach(function () {
this.storage = new MemoryStorageInterface();
this.search = new VaultSourceEntrySearch([this.source], this.storage);
return this.search.prepare();
});

describe("searchByTerm", function () {
it("finds results by term", function () {
const results = this.search.searchByTerm("work").map((res) => res.properties.title);
expect(results[0]).to.equal("Work");
expect(results[1]).to.equal("Work logs");
expect(results[2]).to.equal("Wordpress");
});

it("excludes trash entries", function () {
const results = this.search.searchByTerm("ebay");
expect(results).to.have.lengthOf(0);
});

it("returns resulting entry type", function () {
const [res] = this.search.searchByTerm("Personal Mail");
expect(res).to.have.property("entryType", EntryType.Website);
});

it("returns results including entry group IDs", function () {
const [res] = this.search.searchByTerm("Personal Mail");
expect(res).to.have.property("groupID").that.is.a("string");
});

it("returns results using a single tag, no search", function () {
const results = this.search.searchByTerm("#job").map((res) => res.properties.title);
expect(results).to.deep.equal(["Work", "Work logs"]);
});

it("returns results using multiple tags, no search", function () {
const results = this.search
.searchByTerm("#finance #banking")
.map((res) => res.properties.title);
expect(results).to.deep.equal(["MyBank"]);
});

it("returns results using tags and search", function () {
const results = this.search
.searchByTerm("#job logs")
.map((res) => res.properties.title);
expect(results).to.deep.equal(["Work logs"]);
});

describe("results", function () {
it("contain source ID", function () {
const [res] = this.search.searchByTerm("Personal Mail");
expect(res).to.have.property("sourceID", this.source.id);
});
});
});

describe("searchByURL", function () {
it("finds results by URL", function () {
const results = this.search.searchByURL("https://wordpress.com/homepage/test/org");
expect(results).to.have.length.above(0);
expect(results[0]).to.have.nested.property("properties.title", "Wordpress");
});

it("excludes trash entries", function () {
const results = this.search.searchByURL("ebay.com");
expect(results).to.have.lengthOf(0);
});

it("finds multiple similar results", function () {
const results = this.search.searchByURL("https://gmov.edu.au/portal/");
expect(results).to.have.lengthOf(2);
expect(results[0].properties.title).to.equal("Work");
expect(results[1].properties.title).to.equal("Work logs");
});

it("supports ordering", function () {
const [entry] = this.vault.findEntriesByProperty("title", "Work logs");
return this.storage
.setValue(
`bcup_search_${this.vault.id}`,
JSON.stringify({
[entry.id]: {
"gmov.edu.au": 1
}
})
)
.then(() => this.search.prepare())
.then(() => {
const results = this.search.searchByURL("https://gmov.edu.au/portal/");
expect(results).to.have.lengthOf(2);
expect(results[0].properties.title).to.equal("Work logs");
expect(results[1].properties.title).to.equal("Work");
});
});
});
});
});
Loading

0 comments on commit a973868

Please sign in to comment.