1
0
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:
Justin Baur
2025-03-20 07:21:14 -04:00
parent 5208206034
commit 1af9e9e2bc
7 changed files with 393 additions and 109 deletions

View File

@@ -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);
});
});
});

View 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;
}
}

View File

@@ -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);
});
});
});

View File

@@ -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,
},
);
});