From a8a0cd0b7953bf1486d844cc632d38920ffc52ad Mon Sep 17 00:00:00 2001 From: William Belle Date: Thu, 13 Jul 2023 15:28:03 +0200 Subject: [PATCH] Add multi words search by name (People API) (#67) --- src/services/people.service.js | 16 +++----- src/utils/helper.util.js | 29 +++++++++++++++ src/utils/ldap.util.js | 37 +++++++++++++++++++ tests/people.test.js | 9 ++++- tests/resources/ldap/directory.json | 22 +++++++++++ .../resources/people/json-name-kryze-fr.json | 21 +++++++++++ 6 files changed, 122 insertions(+), 12 deletions(-) create mode 100644 tests/resources/people/json-name-kryze-fr.json diff --git a/src/services/people.service.js b/src/services/people.service.js index 12fa394..3b38a44 100644 --- a/src/services/people.service.js +++ b/src/services/people.service.js @@ -1,3 +1,5 @@ +const util = require('../utils/helper.util'); +const ldapUtil = require('../utils/ldap.util'); const ldapConfig = require('../configs/ldap.config'); const ldapService = require('../services/ldap.service'); @@ -25,17 +27,9 @@ function getPersonByPhone (number) { } function getPersonByName (name) { - const term = name.split(/\s+/); - - if (term.length === 2) { - const filter = `(displayName=*${term.join('*')}*)`; - term.reverse(); - return getPerson( - `(|${filter}(displayName=*${term.join('*')}*))` - ); - } else { - return getPerson(`(|(displayName=*${term[0]}*))`); - } + const terms = name.split(/\s+/); + const ldapQuery = ldapUtil.buildLdapQueryForPerson(util.permutations(terms)); + return getPerson(ldapQuery); } module.exports = { diff --git a/src/utils/helper.util.js b/src/utils/helper.util.js index cde73b5..b5ca485 100644 --- a/src/utils/helper.util.js +++ b/src/utils/helper.util.js @@ -4,6 +4,34 @@ * @module utils/helper */ +/** + * Generates all permutations of an array's elements. + * + * @example + * const util = require('../utils/helper.util'); + * util.permutations([ 'Bo', 'Katan', 'Kryze' ]) + * // => [[ 'Bo', 'Katan', 'Kryze' ], [ 'Bo', 'Kryze', 'Katan' ], + * [ 'Katan', 'Bo', 'Kryze' ], [ 'Katan', 'Kryze', 'Bo' ], + * [ 'Kryze', 'Bo', 'Katan' ], [ 'Kryze', 'Katan', 'Bo' ]] + * + * @param {array} arr An array of terms. + * @returns {array} Return the array of array with all permutations. + */ +function permutations (arr) { + if (arr.length <= 2) { + return arr.length === 2 ? [arr, [arr[1], arr[0]]] : [arr]; + } + return arr.reduce( + (acc, item, i) => + acc.concat( + permutations([...arr.slice(0, i), ...arr.slice(i + 1)]).map( + val => [item, ...val] + ) + ), + [] + ); +}; + /** * Validate environment variables. * @@ -25,5 +53,6 @@ function validateEnv (key) { } module.exports = { + permutations, validateEnv }; diff --git a/src/utils/ldap.util.js b/src/utils/ldap.util.js index 3ff1142..8c3f82b 100644 --- a/src/utils/ldap.util.js +++ b/src/utils/ldap.util.js @@ -56,6 +56,42 @@ function sortPersons (obj, q) { ); } +/** + * Build the LDAP query to search persons by name. + * + * @example + * const ldapUtil = require('../utils/ldap.util'); + * const array = [ [ 'Jango' ]]; + * ldapUtil.buildLdapQueryForPerson(array); + * // => '(|(displayName=*Jango*)))' + * + * @example + * const array = [ [ 'Fett', 'Boba' ], [ 'Boba', 'Fett' ] ]; + * ldapUtil.buildLdapQueryForPerson(array); + * // => '(|(displayName=*Fett*Boba*)(displayName=*Boba*Fett*))' + * + * @example + * const array = [ + * [ 'Bo', 'Katan', 'Kryze' ], [ 'Bo', 'Kryze', 'Katan' ], + * [ 'Katan', 'Bo', 'Kryze' ], [ 'Katan', 'Kryze', 'Bo' ], + * [ 'Kryze', 'Bo', 'Katan' ], [ 'Kryze', 'Katan', 'Bo' ] + * ]; + * ldapUtil.buildLdapQueryForPerson(array); + * // => '(|(displayName=*Bo*Katan*Kryze*)(displayName=*Bo*Kryze*Katan*) \ + * (displayName=*Katan*Bo*Kryze*)(displayName=*Katan*Kryze*Bo*) \ + * (displayName=*Kryze*Bo*Katan*)(displayName=*Kryze*Katan*Bo*))' + * + * @param {array} array An array of array with terms permutations. + * @returns {string} Return the LDAP query. + */ +function buildLdapQueryForPerson (array) { + let ldapQuery = ''; + for (const terms of array) { + ldapQuery += `(displayName=*${terms.join('*')}*)`; + } + return `(|${ldapQuery})`; +} + /** * Transform dn to acronym (unit). * @@ -165,6 +201,7 @@ function ldap2api (ldapResults, q, hl) { } module.exports = { + buildLdapQueryForPerson, dn2acronym, dn2path, getProfile, diff --git a/tests/people.test.js b/tests/people.test.js index 77ae606..ba4b2c7 100644 --- a/tests/people.test.js +++ b/tests/people.test.js @@ -65,7 +65,7 @@ describe('Test API People ("/api/ldap")', () => { expect(JSON.parse(response.text)).toStrictEqual(jsonResult); }); - test('It should find firstname Jango', async () => { + test('It should find firstname Din', async () => { const jsonResult = require('./resources/people/json-name-din-en.json'); const response = await request(app).get('/api/ldap?q=Din&hl=en'); expect(response.statusCode).toBe(200); @@ -93,6 +93,13 @@ describe('Test API People ("/api/ldap")', () => { expect(JSON.parse(response.text)).toStrictEqual(jsonResult); }); + test('It should find Bo Katan Kryze', async () => { + const jsonResult = require('./resources/people/json-name-kryze-fr.json'); + const response = await request(app).get('/api/ldap?q=Bo Katan Kryze'); + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.text)).toStrictEqual(jsonResult); + }); + test('It should not find sciper 679999 without ldap server', async () => { const mockLdapService = jest.spyOn(ldapService, 'searchAll'); mockLdapService.mockRejectedValue(new Error('LDAP is Gone')); diff --git a/tests/resources/ldap/directory.json b/tests/resources/ldap/directory.json index c6d99d0..534fb5f 100644 --- a/tests/resources/ldap/directory.json +++ b/tests/resources/ldap/directory.json @@ -130,5 +130,27 @@ ], "ou;lang-en": "Mandalore is a planet located in the Outer Rim territories of the galaxy." } + }, + { + "dn": "cn=Bo-Katan Kryze,ou=kalevala,ou=tv-4,ou=so,o=epfl,c=ch", + "attributes": { + "cn": ["Bo-Katan Kryze", "Kryze"], + "sn": "Kryze", + "givenName": "Bo-Katan", + "displayName": "Bo-Katan Kryze", + "uniqueIdentifier": "670006", + "mail": "bo-katan.kryze@epfl.ch", + "objectClass": "Person", + "roomNumber": "Castle Kryze", + "telephoneNumber": "+41 21 0054323", + "EPFLAccredOrder": "1", + "description": "Princesse", + "description;lang-en": "Princess", + "ou": [ + "Kalevala", + "Kalevala est une planète du système Mandalore située dans les territoires de la bordure extérieure." + ], + "ou;lang-en": "Kalevala is a planet in the Mandalore system located in the Outer Rim territories." + } } ] diff --git a/tests/resources/people/json-name-kryze-fr.json b/tests/resources/people/json-name-kryze-fr.json new file mode 100644 index 0000000..ab0c0ce --- /dev/null +++ b/tests/resources/people/json-name-kryze-fr.json @@ -0,0 +1,21 @@ +[ + { + "sciper": "670006", + "rank": 0, + "name": "Kryze", + "firstname": "Bo-Katan", + "profile": "bo-katan.kryze", + "email": "bo-katan.kryze@epfl.ch", + "accreds": [ + { + "phoneList": ["+41 21 0054323"], + "officeList": ["Castle Kryze"], + "path": "EPFL/SO/TV-4/KALEVALA", + "rank": "1", + "position": "Princesse", + "acronym": "KALEVALA", + "name": "Kalevala est une planète du système Mandalore située dans les territoires de la bordure extérieure." + } + ] + } +]