# Search

There are currently two search types:

* **`Global`** - the one that searches on all pages where the search is built in.
* `Local` - on a specific page and only on the material of that page.

## Backend

On the backend you won't find separate module that responsible for search mechanic. Instead, you will find search methods in inside modules, depending on what we are looking for. Local search - it's a regular "findAll" etc. methods which include search queries to DB, depending on various params such as filters, sorters and pagination.

The **Global** search function is implemented in the following modules:

* Leads
* Businesses
* Users

If talk about **Local** search, you can find it on this pages:

* Leads
* Businesses
* Users (Super admin and admin roles)
* Team

**Global** search method are pretty same in all modules. For example, method from ***leads.service.ts:***

{% code title="leads.service.ts:" %}

```typescript
    async search(search: string, user: JwtUserPayload) {
        const sanitizedSearch = sanitizeString(search);

        const userWithMemberships = await this.userRepository.findOne({
            where: { id: user.id },
            relations: ['memberships', 'memberships.team'],
        });

        const memberships = userWithMemberships.memberships.filter(m => m.team).map(m => m.team.id);

        const leadsQuery = this.leadsRepository
            .createQueryBuilder('lead')
            .leftJoinAndSelect('lead.business', 'business')
            .leftJoinAndSelect('lead.team', 'team')
            .leftJoinAndSelect('lead.status', 'status')
            .leftJoin('team.memberships', 'membership')
            .where('lead.isDeleted = false')
            .andWhere(
                new Brackets(qb => {
                    qb.where('lead.firstName ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere('lead.lastName ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere('lead.state::text ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere('status.name::text ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere('lead.source ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere("ARRAY_TO_STRING(lead.phoneNumber, ',') ILIKE :search", {
                            search: `%${sanitizedSearch}%`,
                        })
                        .orWhere('lead.email::text ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere('lead.description ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere('lead.location ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere('lead.position ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere('business.name ILIKE :search', { search: `%${sanitizedSearch}%` });
                })
            );

        if (!user.roles.includes(Role.Admin) && !user.roles.includes(Role.SuperAdmin)) {
            leadsQuery.andWhere('team.id IN (:...memberships)', { memberships });
        }

        const leads = await leadsQuery.getMany();

        return leads;
    }
```

{% endcode %}

{% hint style="danger" %}
N.B. All search methods used sanitized search string which clear out all unnecessary symbols from the search string:
{% endhint %}

{% code title="sanitize.ts" %}

```typescript
export default function sanitizeString(str) {
    return str.replace(/[&<>"']/g, function (match) {
        const escape = {
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#039;',
        };
        return escape[match];
    });
}
```

{% endcode %}

This utility you will find in ***api/common/helpers/***

{% hint style="danger" %}
N.B. Also such methods must use queryBuilder and Brackets from TypeORM package which are very important to use. It's prevent bugs to occur.
{% endhint %}

***

"FindAll" method with search query: (same leads service)

{% code title="leads.service.ts" %}

```typescript
    async findAll({ limit, offset, search, business, state, status, sortField, sortOrder, isDeleted }, teamId) {
        const queryBuilder = this.leadsRepository
            .createQueryBuilder('lead')
            .leftJoinAndSelect('lead.business', 'business')
            .leftJoinAndSelect('lead.status', 'status')
            .andWhere('lead.teamId = :teamId', { teamId })
            .andWhere('lead.isDeleted = :isDeleted', { isDeleted })
            .withDeleted();

        if (search) {
            const sanitizedSearch = sanitizeString(search);
            queryBuilder.andWhere(
                new Brackets(qb => {
                    qb.where(`CONCAT(lead.firstName, ' ', lead.lastName) ILIKE :fullName`, {
                        fullName: `%${sanitizedSearch}%`,
                    })
                        .orWhere('lead.state::text ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere('status.name::text ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere('lead.source ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere("ARRAY_TO_STRING(lead.phoneNumber, ',') ILIKE :search", {
                            search: `%${sanitizedSearch}%`,
                        })
                        .orWhere('lead.email::text ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere('lead.description ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere('lead.location ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere('lead.position ILIKE :search', { search: `%${sanitizedSearch}%` })
                        .orWhere('business.name ILIKE :search', { search: `%${sanitizedSearch}%` });
                })
            );
        }

        if (business) {
            if (business.includes('No business')) {
                queryBuilder.andWhere('(business.name IN (:...business) OR business.name IS NULL)', { business });
            } else {
                queryBuilder.andWhere('business.name IN (:...business)', { business });
            }
        }

        if (state) {
            queryBuilder.andWhere('lead.state IN (:...state)', { state });
        }

        if (status) {
            queryBuilder.andWhere('lead.statusId IN (:...status)', { status });
        }

        if (sortField && sortOrder) {
            queryBuilder.orderBy(`lead.${sortField}`, sortOrder);
            const [sortedLeads, leadsCount] = await queryBuilder.skip(offset).take(limit).getManyAndCount();

            return { count: leadsCount, leads: sortedLeads };
        }

        const [list, count] = await queryBuilder.skip(offset).take(limit).getManyAndCount();

        return { count, leads: list };
    }
```

{% endcode %}

{% hint style="warning" %}
N.B. Please notice how filters and sorter from frontend are used here
{% endhint %}

All Global Search Methods use Guard which will limit the number of requests from a single address. This is done to protect against excessive requests to the server and to avoid unforeseen errors. This protection will send us an error if the number of search queries reaches the limit.

If we talk about Guard , then it is still used not only on search methods. Everything is done with the same purpose.

**Controller with Guard:**

{% code title="leads.controller.ts" %}

```typescript
    @Get('/search')
    @UseGuards(JwtAuthenticationGuard, new RateLimitGuard(), AbilitiesGuard)
    @CheckAbilities([Actions.read, LeadsCasl])
    search(@Query() searchParams: SearchLeadDto, @GetUser() user: JwtUserPayload) {
        const { search } = searchParams;

        return this.leadsService.search(search, user);
    }
```

{% endcode %}

**Guard  function:**

{% code title="rate-limit.guard.ts" %}

```typescript
@Injectable()
export class RateLimitGuard implements CanActivate {
    private limiter;

    constructor(
        private readonly windowMs: number = 60 * 1000, // 1 minute
        private readonly max: number = 10 // 10 requests per windowMs
    ) {
        this.limiter = rateLimit({
            windowMs: this.windowMs,
            max: this.max,
            message: 'Too many requests, please try again later.',
        });
    }

    canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
        const req = context.switchToHttp().getRequest();
        const res = context.switchToHttp().getResponse();
        return new Promise((resolve, reject) => {
            this.limiter(req, res, err => {
                if (err) {
                    reject(new HttpException(err.message, HttpStatus.TOO_MANY_REQUESTS));
                } else {
                    resolve(true);
                }
            });
        });
    
```

{% endcode %}

Here you can configure how many requests can pass during how long:

```typescript
private readonly windowMs: number = 60 * 1000, // 1 minute
private readonly max: number = 10 // 10 requests per windowMs
```

{% hint style="warning" %}
N.B. You should be noted that this method basically uses a package (version which is used in the time when search functionality was made):
{% endhint %}

```typescript
"express-rate-limit": "^7.4.0",
```

## <mark style="color:blue;">Frontend</mark>

This part is made within several global components, which ends with one defined. They lie along the following path:

***.../ui/common/components/GlobalSearch/***

There you can find one "global" component that stores the input and various modal windows that are responsible for finding and displaying the result.

If we talk about the search on the pages - there will be used only the input that is used in the Global Search.

Let's look at certain features of the search.

**A service that processes a search request to the backend:&#x20;*****(.../ui/serivces/globalSearchService.js)***

{% code title="globalSearchService.js" %}

```javascript
export const useGlobalSearchService = () => {
    const { axios } = useAxios();

    const globalSearch = async query => {
        let results = {};
        await Promise.all([
            axios.get(`/leads/search`, { params: { search: query } }).then(response => {
                results.leads = response.data;
            }),
            axios.get(`/businesses/search`, { params: { search: query } }).then(response => {
                results.businesses = response.data;
            }),
            axios.get(`/users/search`, { params: { search: query } }).then(response => {
                results.users = response.data;
            }),
        ]).catch(err => {
            console.error(err); // just log the error. Axios service will show notification.
        });

        return results;
    };

    return {
        globalSearch,
    };
};
```

{% endcode %}

Since we have search methods lie each in its own module, then the query here is formed a little in its own way, all understandable.

**Search function in the main component:**

{% code title="GlobalSearch.js" %}

```javascript
    const onSearch = searchText => {
        setLoading(true);
        debounce(
            () =>
                globalSearch(searchText)
                    .then(response => {
                        setResults({ leads: response.leads, businesses: response.businesses, users: response.users });
                    })
                    .finally(() => {
                        setLoading(false);
                        setOpenAllResults(true);
                    }),
            500
        )();
    };
```

{% endcode %}

{% hint style="warning" %}
N.B. Please note that this feature also uses the debouce package for additional protection against excessive requests.
{% endhint %}

Search Input are written a bit different in Global Search and in the pages.

**Global Search component:**

{% code title="GlobalSearch.js" fullWidth="false" %}

```javascript
<SearchInput
                loading={loading}
                onSearch={onSearch}
                searchText={searchText}
                setSearchText={setSearchText}
                style={{
                    width: isSmallDevice ? '100%' : '300px',
                    backgroundColor: isSmallDevice ? backgroundColor.page : 'inherit',
                }}
            />
```

{% endcode %}

**One that is used in Leads Page:**

{% code title="Leads.js" %}

```javascript
<SearchInput
    placeholder={t('search')}
    value={searchText}
    style={{ width: isMediumDevice ? 220 : 250 }}
    onChange={e => {
        setSearchText(e.target.value);
        if (e.target.value === '') {
                handleResetSearch();
        }
    }}
    onPressEnter={e => {
        e.preventDefault();
        handleSearchLead(searchText);
    }}
    prefix={<SearchIcon onClick={() => handleSearchLead(searchText)} />}
    suffix={
    searchText && (
        <ClearInputIcon
            setSearchText={setSearchText}
            handleReset={handleResetSearch}
        />
        )
    }
/>
```

{% endcode %}

The difference is not big, but it has its own specifics depending on page.

{% hint style="warning" %}
N.B. The result on the pages that have their own, Local, search, will be displayed in the table.
{% endhint %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://intercode.gitbook.io/intercode-saas-kit/features/search.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
