diff --git a/src/lib/services/search.service.ts b/src/lib/services/search.service.ts index c20c43d3..da344c7b 100644 --- a/src/lib/services/search.service.ts +++ b/src/lib/services/search.service.ts @@ -41,6 +41,7 @@ */ import { sql } from 'drizzle-orm'; +import { match } from 'ts-pattern'; import { db } from '@/lib/db'; import { redis } from '@/lib/redis'; @@ -1215,16 +1216,12 @@ async function searchNotes(portId: string, query: string, limit: number): Promis } function labelForSource(source: 'client' | 'interest' | 'yacht' | 'company'): string { - switch (source) { - case 'client': - return 'Client'; - case 'interest': - return 'Interest'; - case 'yacht': - return 'Yacht'; - case 'company': - return 'Company'; - } + return match(source) + .with('client', () => 'Client') + .with('interest', () => 'Interest') + .with('yacht', () => 'Yacht') + .with('company', () => 'Company') + .exhaustive(); } // ─── Cross-port (super admin) ──────────────────────────────────────────────── @@ -2005,8 +2002,12 @@ async function runSingleBucket( opts: SearchOptions, ): Promise { const empty = emptyResults(); - switch (opts.type) { - case 'clients': + // Defensive: callers always pass a defined type here (the dispatch above + // narrows it), but the SearchOptions type leaves it optional so we + // explicitly handle undefined for the type-checker. + if (!opts.type) return empty; + return match(opts.type) + .with('clients', async () => { if (!can(opts, 'clients.view')) return empty; empty.clients = applyAffinity( await searchClients(portId, query, limit), @@ -2014,7 +2015,8 @@ async function runSingleBucket( ); empty.totals.clients = empty.clients.length; return empty; - case 'residentialClients': + }) + .with('residentialClients', async () => { if (!can(opts, 'residential_clients.view')) return empty; empty.residentialClients = applyAffinity( await searchResidentialClients(portId, query, limit), @@ -2022,7 +2024,8 @@ async function runSingleBucket( ); empty.totals.residentialClients = empty.residentialClients.length; return empty; - case 'yachts': + }) + .with('yachts', async () => { if (!can(opts, 'yachts.view')) return empty; empty.yachts = applyAffinity( await searchYachts(portId, query, limit), @@ -2030,7 +2033,8 @@ async function runSingleBucket( ); empty.totals.yachts = empty.yachts.length; return empty; - case 'companies': + }) + .with('companies', async () => { if (!can(opts, 'companies.view')) return empty; empty.companies = applyAffinity( await searchCompanies(portId, query, limit), @@ -2038,7 +2042,8 @@ async function runSingleBucket( ); empty.totals.companies = empty.companies.length; return empty; - case 'interests': + }) + .with('interests', async () => { if (!can(opts, 'interests.view')) return empty; empty.interests = applyAffinity( await searchInterests(portId, query, limit), @@ -2046,7 +2051,8 @@ async function runSingleBucket( ); empty.totals.interests = empty.interests.length; return empty; - case 'residentialInterests': + }) + .with('residentialInterests', async () => { if (!can(opts, 'residential_clients.view')) return empty; empty.residentialInterests = applyAffinity( await searchResidentialInterests(portId, query, limit), @@ -2054,7 +2060,8 @@ async function runSingleBucket( ); empty.totals.residentialInterests = empty.residentialInterests.length; return empty; - case 'berths': + }) + .with('berths', async () => { if (!can(opts, 'berths.view')) return empty; empty.berths = applyAffinity( await searchBerths(portId, query, limit), @@ -2062,7 +2069,8 @@ async function runSingleBucket( ); empty.totals.berths = empty.berths.length; return empty; - case 'invoices': + }) + .with('invoices', async () => { if (!can(opts, 'invoices.view')) return empty; empty.invoices = applyAffinity( await searchInvoices(portId, query, limit), @@ -2070,7 +2078,8 @@ async function runSingleBucket( ); empty.totals.invoices = empty.invoices.length; return empty; - case 'expenses': + }) + .with('expenses', async () => { if (!can(opts, 'expenses.view')) return empty; empty.expenses = applyAffinity( await searchExpenses(portId, query, limit), @@ -2078,7 +2087,8 @@ async function runSingleBucket( ); empty.totals.expenses = empty.expenses.length; return empty; - case 'documents': + }) + .with('documents', async () => { if (!can(opts, 'documents.view')) return empty; empty.documents = applyAffinity( await searchDocuments(portId, query, limit), @@ -2086,28 +2096,33 @@ async function runSingleBucket( ); empty.totals.documents = empty.documents.length; return empty; - case 'files': + }) + .with('files', async () => { if (!can(opts, 'files.view') && !can(opts, 'documents.view')) return empty; empty.files = applyAffinity(await searchFiles(portId, query, limit), opts.recentlyTouchedIds); empty.totals.files = empty.files.length; return empty; - case 'reminders': + }) + .with('reminders', async () => { empty.reminders = applyAffinity( await searchReminders(portId, query, limit), opts.recentlyTouchedIds, ); empty.totals.reminders = empty.reminders.length; return empty; - case 'brochures': + }) + .with('brochures', async () => { if (!can(opts, 'admin.manage_settings')) return empty; empty.brochures = await searchBrochures(portId, query, limit); empty.totals.brochures = empty.brochures.length; return empty; - case 'tags': + }) + .with('tags', async () => { empty.tags = await searchTags(portId, query, limit); empty.totals.tags = empty.tags.length; return empty; - case 'navigation': { + }) + .with('navigation', async () => { const { searchNavCatalog } = await import('@/lib/services/search-nav-catalog'); empty.navigation = searchNavCatalog(query, { isSuperAdmin: opts.isSuperAdmin, @@ -2116,10 +2131,16 @@ async function runSingleBucket( }).map((e) => ({ id: e.href, href: e.href, label: e.label, category: e.category })); empty.totals.navigation = empty.navigation.length; return empty; - } - default: + }) + .with('notes', async () => { + // Previously this case silently fell through to the default (which + // returned empty). The exhaustive-match conversion surfaced the + // missing dispatch: searchNotes() already exists, so wire it up. + empty.notes = applyAffinity(await searchNotes(portId, query, limit), opts.recentlyTouchedIds); + empty.totals.notes = empty.notes.length; return empty; - } + }) + .exhaustive(); } function emptyResults(): SearchResults {