Skip to content

Commit

Permalink
Merge pull request #25 from jaynetics/support_alternate_comparators_i…
Browse files Browse the repository at this point in the history
…n_search

feat: improve search customizability, efficiency
  • Loading branch information
nicgirault authored Jun 20, 2020
2 parents 30357f0 + 2eefb13 commit 76fff29
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 22 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ app.use(
...findOptions,
where: {
[Op.or]: [
{ address: { [Op.ilike]: `${q}%` } },
{ zipCode: { [Op.ilike]: `${q}%` } },
{ city: { [Op.ilike]: `${q}%` } },
{ address: { [Op.iLike]: `${q}%` } },
{ zipCode: { [Op.iLike]: `${q}%` } },
{ city: { [Op.iLike]: `${q}%` } },
],
},
})
Expand All @@ -107,7 +107,17 @@ When searching `some stuff`, the following records will be returned in this orde
2. records that have searchable fields that contain both `some` and `stuff`
3. records that have searchable fields that contain one of `some` or `stuff`

The search is case insensitive.
The search is case insensitive by default. You can customize the search to make it case sensitive or use a scope:

```ts
import { Op } from 'sequelize'

const search = searchFields(User, ['address', 'zipCode', 'city'], Op.like)

crud('/admin/users', User, {
search: (q, limit) => search(q, limit, { ownerId: req.user.id }),
})
```

#### Filters

Expand Down
44 changes: 44 additions & 0 deletions src/getList/searchList.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,48 @@ describe('crud', () => {
},
])
})

it('supports alternate comparators', () => {
expect(prepareQueries(['field1'])('some mustach', Op.like)).toEqual([
{
[Op.or]: [
{
field1: { [Op.like]: '%some mustach%' },
},
],
},
{
[Op.and]: [
{
[Op.or]: [{ field1: { [Op.like]: '%some%' } }],
},
{
[Op.or]: [{ field1: { [Op.like]: '%mustach%' } }],
},
],
},
{
[Op.or]: [
{
[Op.or]: [{ field1: { [Op.like]: '%some%' } }],
},
{
[Op.or]: [{ field1: { [Op.like]: '%mustach%' } }],
},
],
},
])
})

it('does only one lookup for single tokens', () => {
expect(prepareQueries(['field1'])('mustach')).toEqual([
{
[Op.or]: [
{
field1: { [Op.iLike]: '%mustach%' },
},
],
},
])
})
})
47 changes: 29 additions & 18 deletions src/getList/searchList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ export type GetSearchList = (

export const searchFields = (
model: { findAll: (findOptions: FindOptions) => Promise<any> },
searchableFields: string[]
) => async (q: string, limit: number) => {
searchableFields: string[],
comparator: symbol = Op.iLike
) => async (q: string, limit: number, scope: FindOptions = {}) => {
const resultChunks = await Promise.all(
prepareQueries(searchableFields)(q).map(filters =>
prepareQueries(searchableFields)(q, comparator).map(query =>
model.findAll({
limit,
where: filters,
where: { ...query, ...scope },
raw: true,
})
)
Expand All @@ -25,7 +26,10 @@ export const searchFields = (
return { rows, count: rows.length }
}

export const prepareQueries = (searchableFields: string[]) => (q: string) => {
export const prepareQueries = (searchableFields: string[]) => (
q: string,
comparator: symbol = Op.iLike
) => {
if (!searchableFields) {
// TODO: we could propose a default behavior based on model rawAttributes
// or (maybe better) based on existing indexes. This can be complexe
Expand All @@ -34,33 +38,40 @@ export const prepareQueries = (searchableFields: string[]) => (q: string) => {
'You must provide searchableFields option to use the "q" filter in express-sequelize-crud'
)
}
const splittedQuery = q.split(' ')

const defaultQuery = {
[Op.or]: searchableFields.map(field => ({
[field]: {
[comparator]: `%${q}%`,
},
})),
}

const tokens = q.split(/\s+/).filter(token => token !== '')
if (tokens.length < 2) return [defaultQuery]

// query consists of multiple tokens => do multiple searches
return [
// priority to unsplit match
{
[Op.or]: searchableFields.map(field => ({
[field]: {
[Op.iLike]: `%${q}%`,
},
})),
},
defaultQuery,

// then search records with all tokens
{
[Op.and]: splittedQuery.map(token => ({
[Op.and]: tokens.map(token => ({
[Op.or]: searchableFields.map(field => ({
[field]: {
[Op.iLike]: `%${token}%`,
[comparator]: `%${token}%`,
},
})),
})),
},
// // then search records with at least one token

// then search records with at least one token
{
[Op.or]: splittedQuery.map(token => ({
[Op.or]: tokens.map(token => ({
[Op.or]: searchableFields.map(field => ({
[field]: {
[Op.iLike]: `%${token}%`,
[comparator]: `%${token}%`,
},
})),
})),
Expand Down

0 comments on commit 76fff29

Please sign in to comment.