Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / lib / search / match-items.ts
blob864d68e5d44c1e03d7b00793ce728e58b4bca764
1 import type { IdentityValues, ItemRevision, ItemType } from '@proton/pass/types';
2 import { deobfuscate } from '@proton/pass/utils/obfuscate/xor';
3 import { normalize } from '@proton/shared/lib/helpers/string';
5 import { matchEvery } from './match-every';
6 import type { ItemMatchFunc, ItemMatchFuncMap } from './types';
8 /** Matches a single field from the item using a getter function,
9  * enabling lazy evaluation when used with `combineMatchers`. */
10 const matchField =
11     <T extends ItemType>(getter: (item: ItemRevision<T>) => string): ItemMatchFunc<T> =>
12     (item) =>
13     (needles) =>
14         matchEvery(needles)(getter(item));
16 /** Matches any field from an array returned by the getter function. */
17 const matchFields =
18     <T extends ItemType>(getter: (item: ItemRevision<T>) => string[]): ItemMatchFunc<T> =>
19     (item) =>
20     (needles) =>
21         getter(item).some((field) => matchEvery(needles)(field));
23 /** Matches fields from an `IterableIterator` returned by the getter function.
24  * Uses lazy evaluation and early return for efficiency. */
25 const matchFieldsLazy =
26     <T extends ItemType>(getter: (item: ItemRevision<T>) => IterableIterator<string>): ItemMatchFunc<T> =>
27     (item) =>
28     (needles) => {
29         for (const field of getter(item)) if (matchEvery(needles)(field)) return true;
30         return false;
31     };
33 /** Combines multiple matchers and checks if any of them match the needles.
34  * Uses lazy evaluation and early return via `some` for efficiency. */
35 const combineMatchers =
36     <T extends ItemType>(...matchers: ItemMatchFunc<T>[]): ItemMatchFunc<T> =>
37     (item) =>
38     (needles) =>
39         matchers.some((matcher) => matcher(item)(needles));
41 const matchesNoteItem: ItemMatchFunc<'note'> = combineMatchers<'note'>(
42     matchField((item) => item.data.metadata.name),
43     matchField((item) => deobfuscate(item.data.metadata.note))
46 const matchesLoginItem: ItemMatchFunc<'login'> = combineMatchers<'login'>(
47     matchField((item) => item.data.metadata.name),
48     matchField((item) => deobfuscate(item.data.metadata.note)),
49     matchField((item) => deobfuscate(item.data.content.itemEmail)),
50     matchField((item) => deobfuscate(item.data.content.itemUsername)),
51     matchFields((item) => item.data.content.urls),
52     matchFieldsLazy(function* matchExtraFields(item): IterableIterator<string> {
53         for (const field of item.data.extraFields) {
54             if (field.type !== 'totp') yield `${field.fieldName} ${deobfuscate(field.data.content)}`;
55         }
56     })
59 const matchesAliasItem: ItemMatchFunc<'alias'> = combineMatchers<'alias'>(
60     matchField((item) => item.data.metadata.name),
61     matchField((item) => deobfuscate(item.data.metadata.note)),
62     matchField((item) => item.aliasEmail ?? '')
65 const matchesCreditCardItem: ItemMatchFunc<'creditCard'> = combineMatchers<'creditCard'>(
66     matchField((item) => item.data.metadata.name),
67     matchField((item) => deobfuscate(item.data.metadata.note)),
68     matchField((item) => item.data.content.cardholderName),
69     matchField((item) => deobfuscate(item.data.content.number))
72 const matchesIdentityItem: ItemMatchFunc<'identity'> = combineMatchers<'identity'>(
73     matchField((item) => item.data.metadata.name),
74     matchField((item) => deobfuscate(item.data.metadata.note)),
75     matchFieldsLazy(function* matchIdentityFields(item): IterableIterator<string> {
76         for (const key of Object.keys(item.data.content) as (keyof IdentityValues)[]) {
77             const value = item.data.content[key];
78             if (typeof value === 'string') yield value;
79             else {
80                 switch (key) {
81                     case 'extraAddressDetails':
82                     case 'extraContactDetails':
83                     case 'extraPersonalDetails':
84                     case 'extraWorkDetails': {
85                         for (const field of item.data.content[key]) {
86                             if (field.type !== 'totp') yield field.data.content;
87                         }
88                         break;
89                     }
90                     case 'extraSections': {
91                         for (const section of item.data.content[key]) {
92                             yield section.sectionName;
93                             for (const field of section.sectionFields) {
94                                 if (field.type !== 'totp') yield field.data.content;
95                             }
96                         }
97                         break;
98                     }
99                 }
100             }
101         }
102     })
105 /* Each item should expose its own searching mechanism :
106  * we may include/exclude certain fields or add extra criteria
107  * depending on the type of item we're targeting */
108 const itemMatchers: ItemMatchFuncMap = {
109     login: matchesLoginItem,
110     note: matchesNoteItem,
111     alias: matchesAliasItem,
112     creditCard: matchesCreditCardItem,
113     identity: matchesIdentityItem,
116 const matchItem: ItemMatchFunc = <T extends ItemType>(item: ItemRevision<T>) => itemMatchers[item.data.type](item);
118 export const searchItems = <T extends ItemRevision>(items: T[], search?: string) => {
119     if (!search || search.trim() === '') return items;
121     /** split the search term into multiple normalized needles */
122     const needles = normalize(search, true).split(' ');
123     return items.filter((item) => matchItem(item)(needles));