Skip to content

Commit d580146

Browse files
authored
Merge pull request #10820 from opencrvs/ocrvs-10617
Add multi-field advanced search support
2 parents 6992fb5 + 40b7ab2 commit d580146

File tree

10 files changed

+860
-120
lines changed

10 files changed

+860
-120
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- Add Import/Export system client and `record.export` scope to enable data migrations [#10415](https://github.com/opencrvs/opencrvs-core/issues/10415)
1212
- Add an Alpha version of configurable "Print" button that will be refactored in a later release - this button can be used to print certificates during declaration/correction flow. [#10039](https://github.com/opencrvs/opencrvs-core/issues/10039)
1313
- Add bulk import endpoint [#10590](https://github.com/opencrvs/opencrvs-core/pull/10590)
14+
- Add multi-field search with a single component [#10617](https://github.com/opencrvs/opencrvs-core/issues/10617)
1415

1516
### Improvements
1617

packages/client/src/v2-events/features/events/Search/SearchResultIndex.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ export const SearchResultIndex = () => {
7171
* re-evaluated, which leads to an infinite loop.
7272
*/
7373
const queryData = searchEvent.useQuery({
74-
query: toAdvancedSearchQueryType(searchQuery, eventType),
74+
query: toAdvancedSearchQueryType(
75+
searchQuery,
76+
eventConfig.advancedSearch.flatMap((section) => section.fields),
77+
eventType
78+
),
7579
...typedSearchParams
7680
}).data ?? {
7781
results: [],

packages/client/src/v2-events/features/events/Search/utils.test.ts

Lines changed: 299 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
import {
1313
EventStatus,
14+
QueryInputType,
15+
SearchField,
1416
tennisClubMembershipEvent
1517
} from '@opencrvs/commons/client'
1618
import {
@@ -20,7 +22,8 @@ import {
2022
deserializeSearchParams,
2123
buildQuickSearchQuery,
2224
resolveAdvancedSearchConfig,
23-
getAdvancedSearchFieldErrors
25+
getAdvancedSearchFieldErrors,
26+
toAdvancedSearchQueryType
2427
} from './utils'
2528

2629
describe('getAdvancedSearchFieldErrors', () => {
@@ -235,3 +238,298 @@ describe('buildQuickSearchQuery', () => {
235238
})
236239
})
237240
})
241+
242+
describe('Nested Query Generation with searchFields', () => {
243+
it('creates OR clauses for fields with multiple searchFields', () => {
244+
const searchParams = {
245+
'person-name': 'Bob',
246+
'child.dob': '1985-01-01'
247+
}
248+
const searchFieldConfigs: SearchField[] = [
249+
{
250+
fieldId: 'person-name',
251+
fieldType: 'field',
252+
type: 'NAME',
253+
config: {
254+
type: 'fuzzy',
255+
searchFields: [
256+
'child.name.firstname',
257+
'child.name.surname',
258+
'mother.name.firstname',
259+
'father.name.firstname'
260+
]
261+
}
262+
},
263+
{
264+
fieldId: 'child.dob',
265+
fieldType: 'field',
266+
config: { type: 'exact' }
267+
}
268+
]
269+
270+
const result = toAdvancedSearchQueryType(
271+
searchParams as unknown as QueryInputType,
272+
searchFieldConfigs,
273+
'birth'
274+
)
275+
276+
expect(result).toEqual({
277+
type: 'and',
278+
clauses: [
279+
{
280+
eventType: 'birth'
281+
},
282+
{
283+
type: 'or',
284+
clauses: [
285+
{
286+
data: { 'child.name.firstname': 'Bob' }
287+
},
288+
{
289+
data: { 'child.name.surname': 'Bob' }
290+
},
291+
{
292+
data: { 'mother.name.firstname': 'Bob' }
293+
},
294+
{
295+
data: { 'father.name.firstname': 'Bob' }
296+
}
297+
]
298+
},
299+
{
300+
data: { 'child.dob': '1985-01-01' }
301+
}
302+
]
303+
})
304+
})
305+
306+
it('creates individual clauses for fields without searchFields', () => {
307+
const searchParams = {
308+
'child.name.firstname': 'Alice',
309+
'child.dob': '1990-01-01'
310+
}
311+
const searchFieldConfigs: SearchField[] = [
312+
{
313+
fieldId: 'child.name.firstname',
314+
fieldType: 'field',
315+
config: { type: 'fuzzy' }
316+
},
317+
{
318+
fieldId: 'child.dob',
319+
fieldType: 'field',
320+
config: { type: 'exact' }
321+
}
322+
]
323+
324+
const result = toAdvancedSearchQueryType(
325+
searchParams as unknown as QueryInputType,
326+
searchFieldConfigs,
327+
'birth'
328+
)
329+
330+
expect(result).toEqual({
331+
type: 'and',
332+
clauses: [
333+
{
334+
eventType: 'birth'
335+
},
336+
{
337+
data: { 'child.name.firstname': 'Alice' }
338+
},
339+
{
340+
data: { 'child.dob': '1990-01-01' }
341+
}
342+
]
343+
})
344+
})
345+
346+
it('handles single searchField as individual clause', () => {
347+
const searchParams = {
348+
'custom-field': 'test-value'
349+
}
350+
const searchFieldConfigs: SearchField[] = [
351+
{
352+
fieldId: 'custom-field',
353+
fieldType: 'field',
354+
config: {
355+
type: 'exact',
356+
searchFields: ['mapped.database.field'] // Single field
357+
}
358+
}
359+
]
360+
361+
const result = toAdvancedSearchQueryType(
362+
searchParams as unknown as QueryInputType,
363+
searchFieldConfigs,
364+
'birth'
365+
)
366+
367+
expect(result).toEqual({
368+
type: 'and',
369+
clauses: [
370+
{
371+
eventType: 'birth'
372+
},
373+
{
374+
type: 'or',
375+
clauses: [
376+
{
377+
data: { 'mapped.database.field': 'test-value' }
378+
}
379+
]
380+
}
381+
]
382+
})
383+
})
384+
385+
it('handles metadata fields correctly', () => {
386+
const searchParams = {
387+
'event.trackingId': 'ABC123',
388+
'person-name': 'Bob'
389+
}
390+
391+
const searchFieldConfigs: SearchField[] = [
392+
{
393+
fieldId: 'event.trackingId',
394+
fieldType: 'event',
395+
config: { type: 'exact' }
396+
},
397+
{
398+
fieldId: 'person-name',
399+
fieldType: 'field',
400+
config: {
401+
type: 'fuzzy',
402+
searchFields: ['child.name.firstname', 'mother.name.firstname']
403+
}
404+
}
405+
]
406+
407+
const result = toAdvancedSearchQueryType(
408+
searchParams as unknown as QueryInputType,
409+
searchFieldConfigs,
410+
'birth'
411+
)
412+
413+
expect(result).toEqual({
414+
type: 'and',
415+
clauses: [
416+
{
417+
trackingId: 'ABC123',
418+
eventType: 'birth'
419+
},
420+
{
421+
type: 'or',
422+
clauses: [
423+
{
424+
data: { 'child.name.firstname': 'Bob' }
425+
},
426+
{
427+
data: { 'mother.name.firstname': 'Bob' }
428+
}
429+
]
430+
}
431+
]
432+
})
433+
})
434+
435+
it('handles complex multi-field scenario', () => {
436+
const searchParams = {
437+
'applicant-name': 'John',
438+
'contact-info': '[email protected]',
439+
'event.status': 'REGISTERED',
440+
'birth.date': '1980-01-01'
441+
}
442+
443+
const searchFieldConfigs: SearchField[] = [
444+
{
445+
fieldId: 'applicant-name',
446+
fieldType: 'field',
447+
config: {
448+
type: 'fuzzy',
449+
searchFields: [
450+
'child.name.firstname',
451+
'child.name.surname',
452+
'informant.name.firstname',
453+
'informant.name.surname'
454+
]
455+
}
456+
},
457+
{
458+
fieldId: 'contact-info',
459+
fieldType: 'field',
460+
config: {
461+
type: 'exact',
462+
searchFields: [
463+
'child.email',
464+
'informant.email',
465+
'child.phone',
466+
'informant.phone'
467+
]
468+
}
469+
},
470+
{
471+
fieldId: 'event.status',
472+
fieldType: 'event',
473+
config: { type: 'exact' }
474+
},
475+
{
476+
fieldId: 'birth.date',
477+
fieldType: 'field',
478+
config: { type: 'exact' }
479+
}
480+
]
481+
482+
const result = toAdvancedSearchQueryType(
483+
searchParams as unknown as QueryInputType,
484+
searchFieldConfigs,
485+
'birth'
486+
)
487+
488+
expect(result).toEqual({
489+
type: 'and',
490+
clauses: [
491+
{
492+
status: 'REGISTERED',
493+
eventType: 'birth'
494+
},
495+
{
496+
type: 'or',
497+
clauses: [
498+
{
499+
data: { 'child.name.firstname': 'John' }
500+
},
501+
{
502+
data: { 'child.name.surname': 'John' }
503+
},
504+
{
505+
data: { 'informant.name.firstname': 'John' }
506+
},
507+
{
508+
data: { 'informant.name.surname': 'John' }
509+
}
510+
]
511+
},
512+
{
513+
type: 'or',
514+
clauses: [
515+
{
516+
data: { 'child.email': '[email protected]' }
517+
},
518+
{
519+
data: { 'informant.email': '[email protected]' }
520+
},
521+
{
522+
data: { 'child.phone': '[email protected]' }
523+
},
524+
{
525+
data: { 'informant.phone': '[email protected]' }
526+
}
527+
]
528+
},
529+
{
530+
data: { 'birth.date': '1980-01-01' }
531+
}
532+
]
533+
})
534+
})
535+
})

0 commit comments

Comments
 (0)