Files
CRM_Montzar/apps/api/src/common/search.ts
T

99 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* الگوریتم مشترک جستجو در کل 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<string>([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<W extends object>(
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 };
}