/** * الگوریتم مشترک جستجو در کل API: * - عبارت به کلمات شکسته می‌شود و همه کلمات باید (در هر فیلدی) پیدا شوند؛ * پس «علی رضا» هم «علی رضایی» را پیدا می‌کند و بخشی از نام هم کافی است. * - حروف عربی/فارسی (ي/ی، ك/ک، ة/ه، أ/ا …) و ارقام فارسی/عربی/لاتین یکسان در نظر گرفته می‌شوند. * - برای شماره تلفن، پیشوند 0 / 98 / +98 نادیده گرفته می‌شود. */ const MAX_TOKENS = 6; const FA_DIGITS = '۰۱۲۳۴۵۶۷۸۹'; const AR_DIGITS = '٠١٢٣٤٥٦٧٨٩'; function toAsciiDigits(s: string): string { return s.replace(/[۰-۹٠-٩]/g, (ch) => { const fa = FA_DIGITS.indexOf(ch); return String(fa >= 0 ? fa : AR_DIGITS.indexOf(ch)); }); } function toFaDigits(s: string): string { return s.replace(/[0-9]/g, (d) => FA_DIGITS[Number(d)]!); } function toPersianLetters(s: string): string { return s .replace(/[يىئ]/g, 'ی') .replace(/ك/g, 'ک') .replace(/ة/g, 'ه') .replace(/[أإٱ]/g, 'ا') .replace(/ؤ/g, 'و'); } function toArabicLetters(s: string): string { return s.replace(/ی/g, 'ي').replace(/ک/g, 'ك'); } /** متن ورودی جستجو را یکدست می‌کند (ارقام لاتین، حروف فارسی، فاصله‌های یکدست). */ export function normalizeSearchText(raw: string | null | undefined): string { return toPersianLetters(toAsciiDigits(String(raw ?? ''))) .replace(/[\u064B-\u065F\u0670]/g, '') .replace(/[\u200c\u200f\u200e]/g, ' ') .replace(/\s+/g, ' ') .trim(); } export function searchTokens(raw: string | null | undefined): string[] { const text = normalizeSearchText(raw); if (!text) return []; return [...new Set(text.split(' ').filter(Boolean))].slice(0, MAX_TOKENS); } /** شکل‌های مختلف یک کلمه که ممکن است در دیتابیس ذخیره شده باشد. */ export function tokenVariants(token: string): string[] { const out = new Set([token, toArabicLetters(token)]); if (/[0-9]/.test(token)) { out.add(toFaDigits(token)); if (/^\+?[0-9]+$/.test(token)) { const core = token.replace(/^\+/, '').replace(/^(0098|98|0)/, ''); if (core.length >= 3) out.add(core); } } return [...out].filter(Boolean); } /** * شرط Prisma برای جستجو: AND روی کلمات، OR روی شکل‌ها و فیلدها. * `fieldsFor(term)` شرط‌های OR یک کلمه را برای فیلدهای آن مدل برمی‌گرداند. */ export function buildSearchWhere( raw: string | null | undefined, fieldsFor: (term: string) => W[], ): W | undefined { const tokens = searchTokens(raw); if (!tokens.length) return undefined; const parts = tokens.map((token) => ({ OR: tokenVariants(token).flatMap((v) => fieldsFor(v)), })); return (parts.length === 1 ? parts[0] : { AND: parts }) as unknown as W; } /** پارامتر چندتایی کوئری: «a,b,c» → ['a','b','c'] */ export function splitCsv(raw: string | null | undefined, max = 50): string[] { if (!raw) return []; return [ ...new Set( String(raw) .split(',') .map((v) => v.trim()) .filter(Boolean), ), ].slice(0, max); } /** کمک برای فیلدهای متنی ساده: `{ field: { contains, mode: 'insensitive' } }` */ export function ci(term: string) { return { contains: term, mode: 'insensitive' as const }; }