-
Notifications
You must be signed in to change notification settings - Fork 1
Add a new backend filter
To add a new filter to the frontend, see this doc.
Design Doc for filters is in Box here. Note that URL format may have changed since it was written.
There are a couple steps necessary to add a new filter. They differ slightly if the filter value can be looked up in the database directly (a “generic filter”), or if it will need to be transformed into a new value when adding it to the query (a “custom filter”).
Filters are represented as an array of ListingsFilterParams
, each of which has a $comparison
to use for that filter, the filter field, and whether the filter should include listings that are missing data ($include_nulls
). This is read from the URL by the listings controller an passed through to the listings service, which then calls addFilters()
to actually add the filters to the query.
The query is performed in two steps. An initial innerFilteredQuery
gets the listing ids for the listings that match thee filter, sort, and pagination criteria, and then an outer query joins on all the tables necessary to get all the data for a listing.
For either generic or custom filters, make sure you do not introduce a SQL injections vulnerability. SQL Injection vulnerabilities happen when user provided text is inserted directly into a query, without escaping or validating it.
For example:
For a backend URL that filters on the name like ?filter[0][name]=; DROP TABLE Students&limit=10
, we get a filters object that looks like
[{
$comparison: "=",
name: "; DROP TABLE Students",
}]
If we then build the query with a line like
queryBuilder.andWhere(`listing.name = ${filter[0].name}`);
we’ve added a SQL injections vulnerability because we’ve inserted the user provided text directly into the query. Our query will look like
AND WHERE listing.name = ; DROP TABLE Students
A carefully crafted input to the filter could expose data to an attacker or corrupt the database.
Instead, we need to either
- Validate that the provided text is one of a limited whitelist of allowed options, the way we do with comparisons.
- Use TypeORM’s built in string escaping, as detailed here.
A generic filter is one where the value provided to the filter is what will be looked up in the database. For example, with ?filter[0][bedrooms]=3
, the backend query will include something like WHERE unit_type.numBedrooms = 3
.
To add a new generic filter to the backend we:
- Update the
ListingFilterKeys
enum with the name of the new filter. - Add a property to
ListingFilterParams
inlisting.dto.ts
with the new filter key. - Add a line to
filterTypeToFieldMap
inlisting.dto.ts
with a map to the database field this filter will be referencing. - Update the
innerFilteredQuery
inlistings.service.ts
to join on the table we need for the new filter, if it’s not on theListing
entity itself. - If you’re using one of the comparisons already in the
Compare
enum infilter.dto.ts
, then skip to step 8. - If you’re not using one of the comparisons in the
Compare
enum, then add it to theCompare
enum infilter.dto.ts
. - Then, add a case for your new comparison in the switch statement in
addFilters()
inbackend/core/src/shared/filter/index.ts
. - Add your new filter to the switch statement in
getComparisonForFilter()
inui-components/src/helpers/filters.ts
, so the frontend knows which comparison to tell the backend to use. Generally, the rest of the frontend makes assumptions about what comparison to use for a filter (minRent
will use>=
for example) but the backend needs to be told what comparison to use, so this map is where that comparison is looked up. Eventually this map may get moved to the backend directly. - Add tests for your new filter in
listings.e2e-spec.ts
andlistings.service.spec.ts
.
A custom filter is one where the value provided to the filter may not match the value that needs to be looked up in the database. For example, the filter name may be seniorHousing
and takes a boolean, but in the database we actually need to check if the value in the reservedCommunityType
table is "senior". We can’t just insert "senior" directly into the query.
The steps for adding a custom filter are the same as adding a generic filter, with a few exceptions:
- You’ll probably use the
NA
comparison type. Every filter must have a comparison type set, but your custom logic probably doesn’t need to be flexible enough to allow arbitrary comparisons, so you can likely hardcode it. - In
addFilters()
inbackend/core/src/shared/filter/index.ts
, add a check for the filter key here, before it drops into the switch statement that switches on the comparison. If it matches your filter type, then call your own code that adds the type ofWHERE
clause you need.