Extract supporter list SQL filters into a server-only module.

This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-19 04:16:47 -03:00
parent 4e68e647d4
commit 4ee7d12085
5 changed files with 79 additions and 76 deletions

View file

@ -19,24 +19,22 @@ export const PrimaryContactCombobox = ({
name,
current,
search,
}: PrimaryContactComboboxProps) => {
return (
<AsyncSearchCombobox
name={name}
label="Selecionar contato principal"
value={current ? { id: current.id, label: contactLabel(current) } : null}
emptyOptionLabel="Sem contato principal"
dialogDescription="Busque entre as lideranças engajadas deste núcleo."
search={async (query) => {
const result = await search(query)
const limit = result.current ? 99 : 100
return result.options.slice(0, limit).map((contact) => ({
id: contact.id,
label: contactLabel(contact),
}))
}}
pinnedOptions={current ? [{ id: current.id, label: contactLabel(current) }] : []}
pinnedGroupHeading="Contato atual"
/>
)
}
}: PrimaryContactComboboxProps) => (
<AsyncSearchCombobox
name={name}
label="Selecionar contato principal"
value={current ? { id: current.id, label: contactLabel(current) } : null}
emptyOptionLabel="Sem contato principal"
dialogDescription="Busque entre as lideranças engajadas deste núcleo."
search={async (query) => {
const result = await search(query)
const limit = result.current ? 99 : 100
return result.options.slice(0, limit).map((contact) => ({
id: contact.id,
label: contactLabel(contact),
}))
}}
pinnedOptions={current ? [{ id: current.id, label: contactLabel(current) }] : []}
pinnedGroupHeading="Contato atual"
/>
)

View file

@ -1,4 +1,3 @@
import { sql } from '@payloadcms/db-postgres'
import type { Where } from 'payload'
import { isContactSearchQueryReady, normalizeContactSearchQuery } from '@/lib/contactSearchQuery'
@ -11,11 +10,6 @@ export type SupporterSearchTerms = {
phoneDigits: string | null
}
export type AggregateSqlConditions = {
conditions: ReturnType<typeof sql>[]
needsContactJoin: boolean
}
export const buildSupporterSearchTerms = (rawQ: string): SupporterSearchTerms | null => {
if (!isContactSearchQueryReady(rawQ)) return null
@ -41,21 +35,6 @@ const buildPayloadSearchWhere = (terms: SupporterSearchTerms): Where => {
return { or: searchFilters }
}
const buildSqlSearchCondition = (terms: SupporterSearchTerms): ReturnType<typeof sql> => {
const searchTerms: ReturnType<typeof sql>[] = [
sql`"contact"."name" ILIKE ${`%${terms.q}%`}`,
sql`"contact"."city" ILIKE ${`%${terms.q}%`}`,
]
if (terms.normalizedPhone) {
searchTerms.push(sql`"contact"."phone" = ${terms.normalizedPhone}`)
} else if (terms.phoneDigits) {
searchTerms.push(sql`"contact"."phone" ILIKE ${`%${terms.phoneDigits}%`}`)
}
return sql`(${sql.join(searchTerms, sql` OR `)})`
}
export const toPayloadWhere = (state: SupporterListState): Where => {
const filters: Where[] = []
@ -78,31 +57,3 @@ export const toPayloadWhere = (state: SupporterListState): Where => {
return filters.length ? { and: filters } : {}
}
export const toAggregateSqlConditions = (state: SupporterListState): AggregateSqlConditions => {
const conditions: ReturnType<typeof sql>[] = []
let needsContactJoin = false
if (state.voteIntention) {
conditions.push(sql`"supporter"."vote_intention" = ${state.voteIntention}`)
}
if (state.nucleus) {
conditions.push(sql`"supporter"."nucleus_id" = ${state.nucleus}`)
}
if (state.city) {
needsContactJoin = true
conditions.push(sql`"contact"."city" = ${state.city}`)
}
if (state.q) {
const searchTerms = buildSupporterSearchTerms(state.q)
if (searchTerms) {
needsContactJoin = true
conditions.push(buildSqlSearchCondition(searchTerms))
}
}
return { conditions, needsContactJoin }
}

View file

@ -6,7 +6,7 @@ import type { Payload } from 'payload'
import type { CampaignUser } from '@/payload-types'
import { getCoordinatorNucleusIds, isCampaignGeneral } from '@/utilities/campaignAccess'
import { drizzleResultRows } from '@/utilities/drizzleBulk'
import { toAggregateSqlConditions } from '@/utilities/supporterListFilters'
import { toAggregateSqlConditions } from '@/utilities/supporterListSqlFilters'
import type { SupporterListState } from '@/utilities/supporterUi'
import type { SupporterListOverviewViewModel } from '@/utilities/supporterViewModels'

View file

@ -0,0 +1,57 @@
import 'server-only'
import { sql } from '@payloadcms/db-postgres'
import {
buildSupporterSearchTerms,
type SupporterSearchTerms,
} from '@/utilities/supporterListFilters'
import type { SupporterListState } from '@/utilities/supporterUi'
export type AggregateSqlConditions = {
conditions: ReturnType<typeof sql>[]
needsContactJoin: boolean
}
const buildSqlSearchCondition = (terms: SupporterSearchTerms): ReturnType<typeof sql> => {
const searchTerms: ReturnType<typeof sql>[] = [
sql`"contact"."name" ILIKE ${`%${terms.q}%`}`,
sql`"contact"."city" ILIKE ${`%${terms.q}%`}`,
]
if (terms.normalizedPhone) {
searchTerms.push(sql`"contact"."phone" = ${terms.normalizedPhone}`)
} else if (terms.phoneDigits) {
searchTerms.push(sql`"contact"."phone" ILIKE ${`%${terms.phoneDigits}%`}`)
}
return sql`(${sql.join(searchTerms, sql` OR `)})`
}
export const toAggregateSqlConditions = (state: SupporterListState): AggregateSqlConditions => {
const conditions: ReturnType<typeof sql>[] = []
let needsContactJoin = false
if (state.voteIntention) {
conditions.push(sql`"supporter"."vote_intention" = ${state.voteIntention}`)
}
if (state.nucleus) {
conditions.push(sql`"supporter"."nucleus_id" = ${state.nucleus}`)
}
if (state.city) {
needsContactJoin = true
conditions.push(sql`"contact"."city" = ${state.city}`)
}
if (state.q) {
const searchTerms = buildSupporterSearchTerms(state.q)
if (searchTerms) {
needsContactJoin = true
conditions.push(buildSqlSearchCondition(searchTerms))
}
}
return { conditions, needsContactJoin }
}

View file

@ -2,11 +2,8 @@
import { describe, expect, it } from 'vitest'
import {
buildSupporterSearchTerms,
toAggregateSqlConditions,
toPayloadWhere,
} from '@/utilities/supporterListFilters'
import { buildSupporterSearchTerms, toPayloadWhere } from '@/utilities/supporterListFilters'
import { toAggregateSqlConditions } from '@/utilities/supporterListSqlFilters'
describe('supporterListFilters', () => {
it('ignores search terms shorter than the contact search minimum', () => {