generated from helsingborg-stad/gdi-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* handling free text search * user param prevalent in advert repository situations * search restrictions * sorting
- Loading branch information
Showing
15 changed files
with
304 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import {createAdvertFilterPredicate} from './advert-filter-predicate' | ||
import {createEmptyAdvert} from './../mappers' | ||
import { Advert } from '../types' | ||
import { HaffaUser } from '../../login/types' | ||
describe('createAdvertFilterPredicate', () => { | ||
|
||
const createTestUser = (user?: Partial<HaffaUser>): HaffaUser => ({ | ||
id: 'test@testerson.com', | ||
roles: [], | ||
...user | ||
}) | ||
|
||
const createSampleAdverts = ( | ||
count: number, | ||
patches?: Record<string, Partial<Advert>> | ||
): Advert[] => [...Array(count)].map(createEmptyAdvert) | ||
.map((advert, index) => ({...advert, id: `advert-${index}`})) | ||
.map(advert => ({ | ||
...advert, | ||
...patches?.[advert.id] | ||
})) | ||
|
||
it('matches all by default', () => { | ||
const adverts = createSampleAdverts(10); | ||
expect(adverts.filter(createAdvertFilterPredicate(createTestUser()))) | ||
.toMatchObject(adverts) | ||
}) | ||
|
||
it('does free text search in {title, description}', () => { | ||
const p = createAdvertFilterPredicate(createTestUser(), {search: 'unicorn'}) | ||
|
||
const adverts = createSampleAdverts(100, { | ||
'advert-10': {title: 'I like my unicorn!'}, | ||
'advert-20': {description: ' UniCorns are the best'} | ||
}); | ||
|
||
expect( | ||
adverts.filter(p) | ||
.map(({id}) => id)) | ||
.toMatchObject(['advert-10', 'advert-20']) | ||
}) | ||
|
||
it('treats search text as a list of separate words combined with or to match {title, description}', () => { | ||
const p = createAdvertFilterPredicate(createTestUser(), {search: 'unicorn orange banana'}) | ||
|
||
const adverts = createSampleAdverts(100, { | ||
'advert-10': {title: 'I like my unicorn!'}, | ||
'advert-20': {description: 'Oranges are the best'}, | ||
'advert-30': {description: 'my oranges brings unicorns to my yard'}, | ||
'advert-40': {description: 'the amazing banana bender'}}) | ||
|
||
expect( | ||
adverts.filter(p) | ||
.map(({id}) => id)) | ||
.toMatchObject(['advert-10', 'advert-20', 'advert-30', 'advert-40']) | ||
}) | ||
|
||
it('combines search text with filter predicates with logical AND', () => { | ||
const p = createAdvertFilterPredicate(createTestUser(), {search: 'unicorn', fields: { | ||
description: { | ||
'contains': 'orange' | ||
} | ||
}}) | ||
|
||
const adverts = createSampleAdverts(100, { | ||
'advert-10': {title: 'I like my unicorn!'}, | ||
'advert-20': {description: 'Unicorns are the best'}, | ||
'advert-30': {title: 'I have an unicorn', description: 'I loves oranges'}}) | ||
|
||
expect( | ||
adverts.filter(p) | ||
.map(({id}) => id)) | ||
.toMatchObject(['advert-30']) | ||
}) | ||
|
||
it('restricts to creator', () => { | ||
const p = createAdvertFilterPredicate(createTestUser({id: 'a@b.com'}), {search: 'unicorn', restrictions: {createdByMe: true}}) | ||
|
||
const adverts = createSampleAdverts(100, { | ||
'advert-10': {title: 'I like my unicorn!'}, | ||
'advert-20': {description: ' UniCorns are the best'}, | ||
'advert-30': {description: ' My very own little unicorn', createdBy: 'a@b.com'} | ||
}); | ||
|
||
expect( | ||
adverts.filter(p) | ||
.map(({id}) => id)) | ||
.toMatchObject(['advert-30']) | ||
}) | ||
|
||
it('restricts to reserved by', () => { | ||
const p = createAdvertFilterPredicate(createTestUser({id: 'a@b.com'}), {search: 'unicorn', restrictions: {reservedByMe: true}}) | ||
|
||
const adverts = createSampleAdverts(100, { | ||
'advert-10': {title: 'I like my unicorn!'}, | ||
'advert-20': {description: ' UniCorns are the best'}, | ||
'advert-30': {description: ' My very own little unicorn', reservations: [{ | ||
reservedBy: 'a@b.com', | ||
reservedAt: new Date().toISOString(), | ||
quantity: 1 | ||
}]} | ||
}); | ||
|
||
expect( | ||
adverts.filter(p) | ||
.map(({id}) => id)) | ||
.toMatchObject(['advert-30']) | ||
}) | ||
|
||
it('restricts to can be reserved', () => { | ||
const p = createAdvertFilterPredicate(createTestUser({id: 'a@b.com'}), {search: 'unicorn', restrictions: {canBeReserved: true}}) | ||
|
||
let adverts = createSampleAdverts(100, { | ||
'advert-10': {title: 'I like my unicorn!'}, | ||
'advert-20': {description: ' UniCorns are the best'}, | ||
'advert-30': {description: ' My very own little unicorn'}}) | ||
|
||
// reserve all adverts except advert-30 | ||
adverts = adverts.map(advert => | ||
advert.id === 'advert-30' | ||
? advert | ||
: ({ | ||
...advert, | ||
reservations: [{ | ||
reservedBy: 'someone@else', | ||
reservedAt: new Date().toISOString(), | ||
quantity: advert.quantity | ||
}] | ||
})) | ||
|
||
expect( | ||
adverts.filter(p) | ||
.map(({id}) => id)) | ||
.toMatchObject(['advert-30']) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import type { HaffaUser } from "../../login/types" | ||
import { getAdvertMeta } from "../advert-meta" | ||
import type { Advert, AdvertFilterInput, AdvertRestrictionsFilterInput } from "../types" | ||
import { createFieldFilterPredicate } from "./field-filter-predicate" | ||
import type { Predicate } from "./types" | ||
|
||
const regexpEscape = (s: string): string => s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') | ||
|
||
const createFreeTextPredicate = (search: string): Predicate<Advert> => { | ||
// extract individual words from search | ||
|
||
const matchers = ((search||'').match(/(\w+)/g) || []) | ||
.filter(v => v) | ||
.filter(v => v.length >= 3) | ||
.map(regexpEscape) | ||
.map(re => new RegExp(re, 'ig')) | ||
|
||
return matchers.length > 0 | ||
? advert => matchers.some(re => re.test(advert.title) || re.test(advert.description)) | ||
: () => true | ||
} | ||
|
||
const createRestrictionsPredicate = (user: HaffaUser, restrictions: AdvertRestrictionsFilterInput): Predicate<Advert> => { | ||
const makeMatcher = (test: boolean|undefined, p: Predicate<Advert>): Predicate<Advert>|null => | ||
// eslint-disable-next-line no-nested-ternary | ||
test === true ? p : test === false ? advert => !p(advert) : null | ||
|
||
const matchers: (Predicate<Advert>)[] = [ | ||
makeMatcher(restrictions?.createdByMe, ({createdBy}) => createdBy === user.id), | ||
makeMatcher(restrictions?.reservedByMe, ({reservations}) => reservations.some(({reservedBy}) => reservedBy === user.id)), | ||
makeMatcher(restrictions?.canBeReserved, advert => getAdvertMeta(advert, user).canReserve) | ||
] | ||
.filter(p => p) as Predicate<Advert>[] | ||
|
||
return matchers.length > 0 | ||
? advert => matchers.every(m => m(advert)) | ||
: () => true | ||
} | ||
|
||
export const createAdvertFilterPredicate = (user: HaffaUser, input?: AdvertFilterInput): Predicate<Advert> => { | ||
const matchers = [ | ||
createFreeTextPredicate(input?.search || ''), | ||
createFieldFilterPredicate(input?.fields), | ||
createRestrictionsPredicate(user, input?.restrictions || {}) | ||
] | ||
// logical AND on all matchers | ||
return (advert) => matchers.every(matcher => matcher(advert)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import type { HaffaUser } from "../../login/types"; | ||
import type { Advert, AdvertFilterInput } from "../types"; | ||
import type { Func2 } from "./types"; | ||
|
||
export const createAdvertFilterComparer = (user: HaffaUser, input?: AdvertFilterInput): Func2<Advert, Advert, number> => { | ||
const field = input?.sorting?.field || 'createdAt' | ||
const ascending = input?.sorting?.ascending !== false | ||
|
||
// NOTE: Array.toSorted() would be a better approach in the future | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSorted | ||
|
||
// eslint-disable-next-line no-nested-ternary | ||
const asc = (a: any, b: any): number => (a === b) ? 0 : a < b ? -1 : 1 | ||
// eslint-disable-next-line no-nested-ternary | ||
const desc = (a: any, b: any): number => (a === b) ? 0 : a < b ? 1 : -1 | ||
|
||
return ascending | ||
? (a: Advert, b: Advert) => asc(a[field], b[field]) | ||
: (a: Advert, b: Advert) => desc(a[field], b[field]) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.