טוב, נהיה לי בלאגן שלם בראש מהכללים האלו.
יש לי המון (9 והיד עוד נטויה) פונקציות עזר למאגר מידע, ויש לי פונקציה שמקבלת רשימת פרמטרים + פונקציות לבדיקה שמתאימות להן ועוד פרמטר שאומר האם לבדוק שכל הפרמטרים ההכרחיים נמצאים או לא.
אני ממש לא יודע מה צריך לבדוק בפונקציות למאגר מידע ומה לא (נראה לי לא אחראי ליצור ככה פונקציות, מצד שני אם בודקים תקינות בפונקציה עצמה, ישר מזדעקים שזה "סקריפט שברירי") או איך לחלק אותן, אשמח לעזרה.
זה קובץ של הולידציה
// סכימה לאימות פרמטרים של מודעה חדשה
const adValidationSchema = {
phone: {validate: isValidIsraeliPhone, required: true},
type: {validate: checkType, required: true},
profession: {validate: checkProfession, required: true},
min_max_price: {validate: checkNumber, required: false},
ringMode: {validate: checkRingMode, required: false},
area: {validate: checkArea, required: false},
city: {validate: checkCity, required: false},
path: {validate: (path) => typeof path === 'string' && path.trim() !== '', required: false}
};
/**
* פונקציות DB עם פורמט תשובה אחיד: { success, data, message?, error? }
*/
function successResponse(data, message = null) {
return { success: true, data, message, error: null };
}
function errorResponse(error) {
return { success: false, data: null, message: null, error };
}
// פונקציה לאימות פרמטרים של מודעה חדשה והכנת אובייקט נקי להוספה למסד הנתונים
export function validateAdParams(params, checkNecessary = true) {
// הגנה מפני קלט ריק
if (!params || typeof params !== 'object') {
return errorResponse("Invalid parameters input");
}
let cleanParams = {};
// בדיקה שכל הפרמטרים שהועברו קיימים בסכימה ותקינים
for (const [key, value] of Object.entries(params)) {
if (!(key in adValidationSchema) ) {return errorResponse(`Unknown parameter: ${key}`);}
const validateFunc = adValidationSchema[key].validate;
if (!validateFunc(value)) { return errorResponse(`Invalid value for ${key}`); }
cleanParams[key] = value;
}
if (checkNecessary) {
// בדיקה שכל הפרמטרים החיוניים קיימים
for (const [key, field] of Object.entries(adValidationSchema)) {
if (field.required && !(key in params)) {
return errorResponse(`Missing required parameter: ${key}`);
}
}
}
return successResponse(cleanParams);
}
אלו הפונקציות של מסד הנתונים:
import argon from "argon2";
/**
* פונקציות DB עם פורמט תשובה אחיד: { success, data, message?, error? }
*/
function successResponse(data, message = null) {
return { success: true, data, message, error: null };
}
function errorResponse(error) {
return { success: false, data: null, message: null, error };
}
/**
* ולידציה בסיסית עבור שדות מודעות
*/
function validateAdData(tableName, data) {
const allowedTables = ['ads', 'ad_cities'];
if (!allowedTables.includes(tableName)) {
throw new Error("שם טבלה לא חוקי");
}
const records = Array.isArray(data) ? data : [data];
if (records.length === 0) return true;
const validColumns = {
ads: ['phone', 'type', 'profession', 'recording_path', 'min_max_price', 'ring_mode'],
ad_cities: ['ad_id', 'city']
};
const columns = validColumns[tableName];
for (const record of records) {
const invalidKeys = Object.keys(record).filter(k => !columns.includes(k));
if (invalidKeys.length > 0) {
throw new Error(`עמודה לא חוקית בטבלה ${tableName}: ${invalidKeys.join(', ')}`);
}
}
return true;
}
/**
* הוספת רשומה אחת או מרובות לטבלה
*/
export async function insertRecord(trx, tableName, data) {
try {
validateAdData(tableName, data);
const records = Array.isArray(data) ? data : [data];
if (records.length === 0) {
return successResponse([], "אין רשומות להוספה");
}
const result = await trx(tableName).insert(records);
return successResponse(
{ insertedId: result[0], affectedRows: records.length },
"רשומות נוספו בהצלחה"
);
} catch (err) {
console.error("שגיאה בהכנסת נתונים:", err);
return errorResponse(err.message || "שגיאה בהכנסת נתונים");
}
}
/**
* פונקציה אוניברסלית לעדכון מודעה ו/או הערים שלה
*/
export async function updateAdWithCities(knex, adId, adData = {}, cities) {
try {
return await knex.transaction(async (trx) => {
// 1️⃣ עדכון המודעה (אם נשלחו נתונים)
if (adData && Object.keys(adData).length > 0) {
validateAdData('ads', adData);
const updatedRows = await trx('ads')
.where({ id: adId })
.update(adData);
if (updatedRows === 0) {
// חובה לזרוק שגיאה כדי לבטל את הטרנזקציה ולא לשמור בטעות שינויים
throw new Error("AD_NOT_FOUND");
}
}
// 2️⃣ טיפול בערים (רק אם נשלח מערך - גם אם הוא ריק)
if (Array.isArray(cities)) {
// מחיקת כל הערים הקיימות
await trx('ad_cities').where({ ad_id: adId }).del();
const validCities = [...new Set(cities)].filter(c => typeof c === 'string' && c.trim());
// הכנסת הערים החדשות
if (validCities.length > 0) {
const records = validCities.map(city => ({ ad_id: adId, city: city.trim() }));
validateAdData('ad_cities', records);
await trx('ad_cities').insert(records);
}
}
return successResponse({ adId }, "העדכון בוצע בהצלחה");
});
} catch (err) {
console.error("שגיאה בעדכון מודעה:", err);
// תפיסת השגיאה היזומה שלנו
if (err.message === "AD_NOT_FOUND") {
return errorResponse(`מודעה עם ID ${adId} לא נמצאה`);
}
return errorResponse(err.message || "שגיאה בעדכון מודעה או הערים");
}
}
/**
* מחזיר את כל המודעות עם הערים הקשורות
*/
export async function getAllAds(knex) {
try {
const rows = await knex('ads as a')
.leftJoin('ad_cities as c', 'a.id', 'c.ad_id')
.select(
'a.*',
knex.raw('COALESCE(JSON_ARRAYAGG(c.city), JSON_ARRAY()) as cities')
)
.groupBy('a.id');
const formatted = rows.map(row => ({
...row,
cities: (typeof row.cities === 'string' ? JSON.parse(row.cities) : row.cities).filter(Boolean)
}));
return successResponse(formatted);
} catch (err) {
console.error("שגיאה בשליפת מודעות:", err);
return errorResponse("שגיאה בשליפת מודעות");
}
}
/**
* מחזיר מודעה לפי מזהה
*/
export async function getAdById(knex, adId) {
try {
const ad = await knex('ads as a')
.leftJoin('ad_cities as c', 'a.id', 'c.ad_id')
.select(
'a.*',
knex.raw('COALESCE(JSON_ARRAYAGG(c.city), JSON_ARRAY()) as cities')
)
.where('a.id', adId)
.groupBy('a.id')
.first();
if (!ad) return errorResponse(`מודעה עם ID ${adId} לא נמצאה`);
const citiesArray = typeof ad.cities === 'string' ? JSON.parse(ad.cities) : ad.cities;
return successResponse({ ...ad, cities: citiesArray.filter(Boolean) });
} catch (err) {
console.error("שגיאה בשליפת מודעה:", err);
return errorResponse("שגיאה בשליפת מודעה");
}
}
/**
* הגדרת שדות מותרים לסינון והאופרטור שלהם
*/
const FILTER_CONFIG = {
phone: '=',
type: '=',
profession: '=',
ringMode: '=',
city: '='
};
// פונקציה למציאת מודעות עבור משתמש עם אפשרות לסינון וסטטוס קריאה
export async function findAdsForUser(
knex,
{ phone, filters = {}, status = "all"/*, limit = 50, offset = 0 */}
) {
if (!phone) return errorResponse("מספר טלפון חסר");
/* const cleanLimit = Math.min(Math.max(parseInt(limit) || 50, 1), 100);
const cleanOffset = Math.max(parseInt(offset) || 0, 0);*/
const allowedStatus = ["all", "read", "unread"];
const cleanStatus = allowedStatus.includes(status) ? status : "all";
try {
// 1️⃣ הפילטרים מגיעים ישירות מהבקשה
const activeFilters = { ...filters };
// 2️⃣ שאילתת בסיס
let baseQuery = knex("ads as M");
// 3️⃣ יישום פילטרים קבועים מהקוד
Object.entries(FILTER_CONFIG).forEach(([field, operator]) => {
const value = activeFilters[field];
if (value !== undefined && value !== null && value !== "") {
baseQuery.where(`M.${field}`, operator, value);
}
});
// 4️⃣ סטטוס קריאה
const readSubquery = knex("adsReads")
.whereRaw("adsReads.ModhaId = M.Id")
.andWhere("adsReads.Phone", phone);
if (cleanStatus === "read") baseQuery.whereExists(readSubquery);
else if (cleanStatus === "unread") baseQuery.whereNotExists(readSubquery);
// 5️⃣ ספירה
const countQuery = baseQuery.clone().countDistinct({ total: "M.Id" }).first();
// 6️⃣ שליפת נתונים עם is_read
const dataQuery = baseQuery.clone()
.select("M.*")
.select(
knex.raw(
`EXISTS (
SELECT 1
FROM adsReads
WHERE ModhaId = M.Id
AND Phone = ?
) as is_read`,
[phone]
)
)
.orderBy("M.Id", "desc")
/*.limit(cleanLimit)
.offset(cleanOffset)*/;
// 7️⃣ הרצה מקבילית
const [totalResult, rows] = await Promise.all([countQuery, dataQuery]);
const totalCount = parseInt(totalResult?.total || 0);
return successResponse({
ads: rows.map(r => ({ ...r, is_read: !!r.is_read }))/*,
pagination: {
total: totalCount,
limit: cleanLimit,
offset: cleanOffset,
hasMore: cleanOffset + rows.length < totalCount
}*/
});
} catch (err) {
console.error("[findAdsForUser] Error:", err);
return errorResponse("שגיאה בשליפת המודעות");
}
}
/**
* מחיקת מודעה (הערים יימחקו אוטומטית בזכות ON DELETE CASCADE ב-DB)
*
*/
export async function deleteAd(knex, adId) {
try {
// מחיקת המודעה (שים לב: העמודה בטבלת ads נקראת id)
const deletedRows = await knex('ads').where({ id: adId }).del();
// אם 0 שורות נמחקו, סימן שהמודעה לא הייתה קיימת
if (deletedRows === 0) {
return errorResponse(`מודעה עם ID ${adId} לא נמצאה`);
}
// החזרת תשובת הצלחה בפורמט האחיד
return successResponse({ adId }, "המודעה נמחקה בהצלחה");
} catch (err) {
console.error("שגיאה במחיקת מודעה:", err);
return errorResponse("שגיאה פנימית במחיקת מודעה");
}
}
// מסמן מודעה כנקראה עבור משתמש מסוים
export async function markAdAsRead(knex, phone, adId) {
if (!phone || !adId) {
return errorResponse("מספר טלפון או מזהה מודעה חסרים");
}
try {
await knex("adsReads").insert({ ModhaId: adId, Phone: phone }).onconflict(['ModhaId', 'Phone']).ignore();
return successResponse(null, "המודעה סומנה כנקראה");
} catch (err) {
console.error("[markAdAsRead] Error:", err);
return errorResponse("שגיאה בסימון המודעה כנקראה");
}
}
export async function findAdByFilter(knex, filters = {}) {
if ( !filters ||
typeof filters !== 'object' ||
Array.isArray(filters) ||
Object.keys(filters).length === 0) {
return errorResponse("לא סופקו פילטרים חוקיים");
}
try {
let { city, ...activeFilters } = filters;
// 2️⃣ שאילתת בסיס
let query = knex("ads as M");
// 3️⃣ יישום פילטרים קבועים מהקוד
if (activeFilters.phone) {
delete activeFilters.phone;
}
// טיפול בסינון לפי עיר (תמיכה גם במערך של ערים)
if (city) {
query.join("ad_cities as C", "C.adId", "M.id");
if (Array.isArray(city)) {
query.whereIn("C.city", city);
} else {
query.where("C.city", city);
}
}
Object.entries(FILTER_CONFIG).forEach(([field, operator]) => {
const value = activeFilters[field];
if (value !== undefined && value !== null && value !== "") {
// אם הערך הוא מערך, נשתמש ב-IN כדי לאפשר בחירה מרובה
if (Array.isArray(value)) {
query.whereIn(`M.${field}`, value);
} else {
query.where(`M.${field}`, operator, value);
}
}
});
// ביצוע השאילתה - שימוש ב-distinct כדי למנוע כפילויות במקרה של JOIN
const result = await query
.select("M.*")
.distinct();
return successResponse({ ads: result });
} catch (error) {
console.error("[findAdByFilter] Error:", error);
return errorResponse("שגיאה בשליפת המודעה לפי פילטרים");
}
}
export async function rate(knex, phone, phoneTorating, rating) {
if (!phone || !phoneTorating || typeof rating !== 'number' || rating < 1 || rating > 5) {
return errorResponse("מספר טלפון או דירוג לא חוקיים");
}
try {
const hashedPhone = await argon.hash(phone);
await knex("rating").insert({ phone: hashedPhone, phoneTorating, rating }).onconflict(['phone', 'phoneTorating']).merge();
return successResponse(null, "הדירוג נוסף/עודכן בהצלחה");
} catch (err) {
console.error("[rate] Error:", err);
return errorResponse("שגיאה בהוספת הדירוג");
}
}