mirror of
https://github.com/bitwarden/browser
synced 2026-03-01 19:11:22 +00:00
Work on Basic Filter Handler
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { BasicFilter, BasicVaultFilterHandler } from "./basic-vault-filter.handler";
|
||||
|
||||
describe("BasicVaultFilterHandler", () => {
|
||||
const sut = new BasicVaultFilterHandler(mock());
|
||||
|
||||
describe("tryParse", () => {
|
||||
it("success", () => {
|
||||
const result = sut.tryParse(
|
||||
'(in:collection:"My Collection" OR in:collection:"Other Collection")',
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
fail("Result is expected to succeed");
|
||||
}
|
||||
|
||||
expect(result.filter).toBe({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("toFilter", () => {
|
||||
const cases: { input: BasicFilter; output: string }[] = [
|
||||
{
|
||||
input: {
|
||||
vaults: [null, "org_vault"],
|
||||
collections: ["collection_one", "collection_two"],
|
||||
fields: ["one", "two"],
|
||||
types: ["Login", "Card"],
|
||||
folders: ["folder_one", "folder_two"],
|
||||
},
|
||||
output:
|
||||
'(in:my_vault OR in:org:"org_vault") AND (in:folder:"folder_one" OR in:folder:"folder_two") AND (in:collection:"collection_one" OR in:collection:"collection_two") AND (type:"Login" OR type:"Card") AND (field:"one" AND field:"two")',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
vaults: [null],
|
||||
collections: [],
|
||||
fields: [],
|
||||
types: [],
|
||||
folders: [],
|
||||
},
|
||||
output: "(in:my_vault)",
|
||||
},
|
||||
{
|
||||
input: {
|
||||
vaults: [],
|
||||
collections: [],
|
||||
fields: ["Banking"],
|
||||
types: [],
|
||||
folders: [],
|
||||
},
|
||||
output: '(field:"Banking")',
|
||||
},
|
||||
];
|
||||
|
||||
it.each(cases)("translates basic filter to $output", ({ input, output }) => {
|
||||
const actualOutput = sut.toFilter(input);
|
||||
|
||||
expect(actualOutput).toEqual(output);
|
||||
});
|
||||
});
|
||||
});
|
||||
104
libs/common/src/vault/filtering/basic-vault-filter.handler.ts
Normal file
104
libs/common/src/vault/filtering/basic-vault-filter.handler.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { parseQuery } from "../search/parse";
|
||||
|
||||
export type BasicFilter = {
|
||||
vaults: string[];
|
||||
folders: string[];
|
||||
collections: string[];
|
||||
types: string[];
|
||||
fields: string[];
|
||||
};
|
||||
|
||||
export class BasicVaultFilterHandler {
|
||||
constructor(private readonly logService: LogService) {}
|
||||
|
||||
tryParse(rawFilter: string): { success: true; filter: BasicFilter } | { success: false } {
|
||||
// Parse into AST
|
||||
const ast = parseQuery(rawFilter, this.logService).ast;
|
||||
|
||||
if (ast.type !== "search") {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
toFilter(basicFilter: BasicFilter) {
|
||||
const buildGroupAdvanced = (
|
||||
items: string[],
|
||||
selector: (item: string) => string,
|
||||
binaryOp: string,
|
||||
) => {
|
||||
if (items == null || items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `(${items.map(selector).join(` ${binaryOp} `)})`;
|
||||
};
|
||||
|
||||
const buildGroup = (items: string[], preamble: string, binaryOp: string) => {
|
||||
// TODO: Maybe only quote item when there is containing whitespace so we create as "pretty" of a filter as possible
|
||||
return buildGroupAdvanced(items, (i) => `${preamble}:"${i}"`, binaryOp);
|
||||
};
|
||||
|
||||
let filter = "";
|
||||
let addedItem = false;
|
||||
|
||||
const vaultGroup = buildGroupAdvanced(
|
||||
basicFilter.vaults,
|
||||
(i) => {
|
||||
if (i == null) {
|
||||
return "in:my_vault";
|
||||
}
|
||||
|
||||
return `in:org:"${i}"`;
|
||||
},
|
||||
"OR",
|
||||
);
|
||||
|
||||
if (vaultGroup != null) {
|
||||
// vault is the first thing we might add, so no need to check if addedItem is already true
|
||||
addedItem = true;
|
||||
filter += vaultGroup;
|
||||
}
|
||||
|
||||
const foldersGroup = buildGroup(basicFilter.folders, "in:folder", "OR");
|
||||
|
||||
if (foldersGroup != null) {
|
||||
if (addedItem) {
|
||||
filter += " AND ";
|
||||
}
|
||||
addedItem = true;
|
||||
filter += foldersGroup;
|
||||
}
|
||||
|
||||
const collectionsGroup = buildGroup(basicFilter.collections, "in:collection", "OR");
|
||||
if (collectionsGroup != null) {
|
||||
if (addedItem) {
|
||||
filter += " AND ";
|
||||
}
|
||||
addedItem = true;
|
||||
filter += collectionsGroup;
|
||||
}
|
||||
|
||||
const typesGroup = buildGroup(basicFilter.types, "type", "OR");
|
||||
if (typesGroup != null) {
|
||||
if (addedItem) {
|
||||
filter += " AND ";
|
||||
}
|
||||
addedItem = true;
|
||||
filter += typesGroup;
|
||||
}
|
||||
|
||||
const fieldsGroup = buildGroup(basicFilter.fields, "field", "AND");
|
||||
if (fieldsGroup != null) {
|
||||
if (addedItem) {
|
||||
filter += " AND ";
|
||||
}
|
||||
addedItem = true;
|
||||
filter += fieldsGroup;
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
@@ -53,12 +53,12 @@ describe("VaultFilterMetadataService", () => {
|
||||
name: "single personal vault cipher",
|
||||
input: [createCipher({ type: CipherType.Card })],
|
||||
output: {
|
||||
vaults: new Set([null]),
|
||||
fieldNames: new Set([]),
|
||||
itemTypes: new Set([CipherType.Card]),
|
||||
folders: new Set([]),
|
||||
collections: new Set([]),
|
||||
anyHaveAttachment: false,
|
||||
vaults: new Map([[null, 1]]),
|
||||
fieldNames: new Map([]),
|
||||
itemTypes: new Map([[CipherType.Card, 1]]),
|
||||
folders: new Map([]),
|
||||
collections: new Map([]),
|
||||
attachmentCount: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -94,12 +94,24 @@ describe("VaultFilterMetadataService", () => {
|
||||
}),
|
||||
],
|
||||
output: {
|
||||
vaults: new Set(["org-one", "org-two"]),
|
||||
fieldNames: new Set(["one", "five"]),
|
||||
itemTypes: new Set([CipherType.Login, CipherType.Card]),
|
||||
folders: new Set([]),
|
||||
collections: new Set(["one", "three"]),
|
||||
anyHaveAttachment: true,
|
||||
vaults: new Map([
|
||||
["org-one", 2],
|
||||
["org-two", 2],
|
||||
]),
|
||||
fieldNames: new Map([
|
||||
["one", 7],
|
||||
["five", 1],
|
||||
]),
|
||||
itemTypes: new Map([
|
||||
[CipherType.Login, 3],
|
||||
[CipherType.Card, 1],
|
||||
]),
|
||||
folders: new Map([]),
|
||||
collections: new Map([
|
||||
["one", 3],
|
||||
["three", 1],
|
||||
]),
|
||||
attachmentCount: 8,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -112,7 +124,7 @@ describe("VaultFilterMetadataService", () => {
|
||||
expect(actualMetadata.itemTypes).toEqual(output.itemTypes);
|
||||
expect(actualMetadata.folders).toEqual(output.folders);
|
||||
expect(actualMetadata.collections).toEqual(output.collections);
|
||||
expect(actualMetadata.anyHaveAttachment).toBe(output.anyHaveAttachment);
|
||||
expect(actualMetadata.attachmentCount).toBe(output.attachmentCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,58 +4,68 @@ import { CipherType } from "../enums";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
|
||||
export type VaultFilterMetadata = {
|
||||
vaults: Set<string | null>;
|
||||
fieldNames: Set<string>;
|
||||
itemTypes: Set<CipherType>;
|
||||
folders: Set<string>;
|
||||
collections: Set<string>;
|
||||
anyHaveAttachment: boolean;
|
||||
vaults: Map<string | null, number>;
|
||||
fieldNames: Map<string, number>;
|
||||
itemTypes: Map<CipherType, number>;
|
||||
folders: Map<string, number>;
|
||||
collections: Map<string, number>;
|
||||
attachmentCount: number;
|
||||
};
|
||||
|
||||
export class VaultFilterMetadataService {
|
||||
collectMetadata() {
|
||||
const setOrIncrement = <T>(map: Map<T, number>, key: T) => {
|
||||
const entry = map.get(key);
|
||||
|
||||
if (entry == undefined) {
|
||||
map.set(key, 1);
|
||||
} else {
|
||||
map.set(key, entry + 1);
|
||||
}
|
||||
};
|
||||
|
||||
return map<CipherView[], VaultFilterMetadata>((ciphers) => {
|
||||
return ciphers.reduce<VaultFilterMetadata>(
|
||||
(metadata, cipher) => {
|
||||
// Track type
|
||||
metadata.itemTypes.add(cipher.type);
|
||||
setOrIncrement(metadata.itemTypes, cipher.type);
|
||||
|
||||
// Track vault
|
||||
metadata.vaults.add(cipher.organizationId ?? null);
|
||||
setOrIncrement(metadata.vaults, cipher.organizationId ?? null);
|
||||
|
||||
// Track all field names
|
||||
if (cipher.fields != null) {
|
||||
for (const field of cipher.fields) {
|
||||
metadata.fieldNames.add(field.name);
|
||||
setOrIncrement(metadata.fieldNames, field.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Track all folder ids
|
||||
if (cipher.folderId != null) {
|
||||
metadata.folders.add(cipher.folderId);
|
||||
setOrIncrement(metadata.folders, cipher.folderId);
|
||||
}
|
||||
|
||||
// Track all collections
|
||||
if (cipher.collectionIds != null) {
|
||||
for (const collectionId of cipher.collectionIds) {
|
||||
metadata.collections.add(collectionId);
|
||||
setOrIncrement(metadata.collections, collectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Track if any have an attachment
|
||||
if (cipher.attachments != null && cipher.attachments.length > 0) {
|
||||
metadata.anyHaveAttachment = true;
|
||||
metadata.attachmentCount = metadata.attachmentCount + cipher.attachments.length;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
},
|
||||
{
|
||||
vaults: new Set<string | null>(),
|
||||
fieldNames: new Set<string>(),
|
||||
itemTypes: new Set<CipherType>(),
|
||||
folders: new Set<string>(),
|
||||
collections: new Set<string>(),
|
||||
anyHaveAttachment: false,
|
||||
vaults: new Map<string | null, number>(),
|
||||
fieldNames: new Map<string, number>(),
|
||||
itemTypes: new Map<CipherType, number>(),
|
||||
folders: new Map<string, number>(),
|
||||
collections: new Map<string, number>(),
|
||||
attachmentCount: 0,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user