From 358d3c95b8a8b86113108d6acf886cced416e154 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Tue, 21 Apr 2026 16:56:37 +0530 Subject: [PATCH 1/2] PM-4886 Allow multiple countries in member search --- .circleci/config.yml | 1 + src/reports/member/dto/member-search.dto.ts | 12 ++++++++---- .../member/member-search.controller.spec.ts | 2 +- src/reports/member/member-search.service.spec.ts | 6 +++--- src/reports/member/member-search.service.ts | 16 ++++++++++++---- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 10c4841..6a40302 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -69,6 +69,7 @@ workflows: - PM-4490 - PM-4491-fix - PM-3497_talent-search + - PM-4886 # Production builds are exectuted only on tagged commits to the # master branch. diff --git a/src/reports/member/dto/member-search.dto.ts b/src/reports/member/dto/member-search.dto.ts index e0f36fa..1e730ae 100644 --- a/src/reports/member/dto/member-search.dto.ts +++ b/src/reports/member/dto/member-search.dto.ts @@ -8,6 +8,7 @@ import { IsOptional, IsString, IsUUID, + ArrayNotEmpty, Max, Min, ValidateNested, @@ -77,12 +78,15 @@ export class MemberSearchBodyDto { @ApiPropertyOptional({ description: - "Filter by country name or code as stored in the member location (case-insensitive).", - example: "Australia", + "Filter by multiple country names or country codes (case-insensitive).", + type: [String], + example: ["US", "Australia"], }) @IsOptional() - @IsString() - country?: string; + @IsArray() + @ArrayNotEmpty() + @IsString({ each: true }) + countries?: string[]; @ApiPropertyOptional({ description: diff --git a/src/reports/member/member-search.controller.spec.ts b/src/reports/member/member-search.controller.spec.ts index 1a79ef4..0c1b3f0 100644 --- a/src/reports/member/member-search.controller.spec.ts +++ b/src/reports/member/member-search.controller.spec.ts @@ -33,7 +33,7 @@ describe("MemberSearchController", () => { it("delegates search requests to the service and returns response", async () => { const body = { - country: "Australia", + countries: ["Australia"], sortBy: "handle" as const, sortOrder: "asc" as const, page: 2, diff --git a/src/reports/member/member-search.service.spec.ts b/src/reports/member/member-search.service.spec.ts index faf35c6..0d43800 100644 --- a/src/reports/member/member-search.service.spec.ts +++ b/src/reports/member/member-search.service.spec.ts @@ -92,7 +92,7 @@ describe("MemberSearchService", () => { .mockResolvedValueOnce([{ total: 0 }]); await service.search({ - country: "us", + countries: ["us"], page: 2, limit: 5, sortBy: "handle", @@ -106,8 +106,8 @@ describe("MemberSearchService", () => { expect(dataSql).toContain( 'ORDER BY m.handle ASC, "matchIndex" DESC NULLS LAST', ); - expect(dataParams).toEqual(["us", 5, 5]); - expect(countParams).toEqual(["us"]); + expect(dataParams).toEqual([["us"], 5, 5]); + expect(countParams).toEqual([["us"]]); }); it("deduplicates skills and keeps last wins value when building skill query", async () => { diff --git a/src/reports/member/member-search.service.ts b/src/reports/member/member-search.service.ts index 2470f8a..72f45fc 100644 --- a/src/reports/member/member-search.service.ts +++ b/src/reports/member/member-search.service.ts @@ -31,7 +31,7 @@ export class MemberSearchService { openToWork, recentlyActive, verifiedProfile, - country, + countries, sortBy = "matchIndex", sortOrder = "desc", page = 1, @@ -201,10 +201,18 @@ member_address AS ( ); } - if (country) { - const pCountry = p(country); + const normalizedCountries = Array.isArray(countries) + ? countries.map((value) => String(value).trim()).filter(Boolean) + : []; + + if (normalizedCountries.length > 0) { + const pCountries = p(normalizedCountries); where.push( - `(LOWER(m."homeCountryCode") = LOWER(${pCountry}) OR LOWER(m."competitionCountryCode") = LOWER(${pCountry}) OR LOWER(m.country) = LOWER(${pCountry}))`, + `( + LOWER(m."homeCountryCode") = ANY(SELECT LOWER(c) FROM unnest(${pCountries}::text[]) AS c) + OR LOWER(m."competitionCountryCode") = ANY(SELECT LOWER(c) FROM unnest(${pCountries}::text[]) AS c) + OR LOWER(m.country) = ANY(SELECT LOWER(c) FROM unnest(${pCountries}::text[]) AS c) + )`, ); } From 08076111b694a93cdb6a4619906b34570b497695 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Wed, 22 Apr 2026 00:46:46 +0530 Subject: [PATCH 2/2] PM-4886 Fix PR feedback --- src/reports/member/dto/member-search.dto.ts | 2 -- .../member/member-search.service.spec.ts | 17 +++++++++++++++++ src/reports/member/member-search.service.ts | 14 ++++++++++---- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/reports/member/dto/member-search.dto.ts b/src/reports/member/dto/member-search.dto.ts index 1e730ae..d84e299 100644 --- a/src/reports/member/dto/member-search.dto.ts +++ b/src/reports/member/dto/member-search.dto.ts @@ -8,7 +8,6 @@ import { IsOptional, IsString, IsUUID, - ArrayNotEmpty, Max, Min, ValidateNested, @@ -84,7 +83,6 @@ export class MemberSearchBodyDto { }) @IsOptional() @IsArray() - @ArrayNotEmpty() @IsString({ each: true }) countries?: string[]; diff --git a/src/reports/member/member-search.service.spec.ts b/src/reports/member/member-search.service.spec.ts index 0d43800..f33239c 100644 --- a/src/reports/member/member-search.service.spec.ts +++ b/src/reports/member/member-search.service.spec.ts @@ -106,10 +106,27 @@ describe("MemberSearchService", () => { expect(dataSql).toContain( 'ORDER BY m.handle ASC, "matchIndex" DESC NULLS LAST', ); + expect(dataSql).toContain( + 'LOWER(m."homeCountryCode") = ANY($1::text[])', + ); expect(dataParams).toEqual([["us"], 5, 5]); expect(countParams).toEqual([["us"]]); }); + it("treats empty countries as no country filter", async () => { + mockDbService.query + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([{ total: 0 }]); + + await service.search({ countries: [] }); + + const dataSql = mockDbService.query.mock.calls[0][0] as string; + const countParams = mockDbService.query.mock.calls[1][1] as unknown[]; + + expect(dataSql).not.toContain('LOWER(m."homeCountryCode") = ANY('); + expect(countParams).toEqual([]); + }); + it("deduplicates skills and keeps last wins value when building skill query", async () => { const skillA = "550e8400-e29b-41d4-a716-446655440000"; const skillB = "550e8400-e29b-41d4-a716-446655440001"; diff --git a/src/reports/member/member-search.service.ts b/src/reports/member/member-search.service.ts index 72f45fc..b37807b 100644 --- a/src/reports/member/member-search.service.ts +++ b/src/reports/member/member-search.service.ts @@ -202,16 +202,22 @@ member_address AS ( } const normalizedCountries = Array.isArray(countries) - ? countries.map((value) => String(value).trim()).filter(Boolean) + ? [ + ...new Set( + countries + .map((value) => String(value).trim().toLowerCase()) + .filter(Boolean), + ), + ] : []; if (normalizedCountries.length > 0) { const pCountries = p(normalizedCountries); where.push( `( - LOWER(m."homeCountryCode") = ANY(SELECT LOWER(c) FROM unnest(${pCountries}::text[]) AS c) - OR LOWER(m."competitionCountryCode") = ANY(SELECT LOWER(c) FROM unnest(${pCountries}::text[]) AS c) - OR LOWER(m.country) = ANY(SELECT LOWER(c) FROM unnest(${pCountries}::text[]) AS c) + LOWER(m."homeCountryCode") = ANY(${pCountries}::text[]) + OR LOWER(m."competitionCountryCode") = ANY(${pCountries}::text[]) + OR LOWER(m.country) = ANY(${pCountries}::text[]) )`, ); }