const db = require('../db/models'); const ValidationError = require('./notifications/errors/validation'); const Sequelize = db.Sequelize; const Op = Sequelize.Op; /** * @param {string} permission * @param {object} currentUser */ async function checkPermissions(permission, currentUser) { if (!currentUser) { throw new ValidationError('auth.unauthorized'); } const userPermission = currentUser.custom_permissions.find( (cp) => cp.name === permission, ); if (userPermission) { return true; } try { if (!currentUser.app_role) { throw new ValidationError('auth.forbidden'); } const permissions = await currentUser.app_role.getPermissions(); return !!permissions.find((p) => p.name === permission); } catch (e) { throw e; } } module.exports = class SearchService { static async search(searchQuery, currentUser ) { try { if (!searchQuery) { throw new ValidationError('iam.errors.searchQueryRequired'); } const tableColumns = { "users": [ "firstName", "lastName", "phoneNumber", "email", ], "courses": [ "title", "slug", "description", "from_language", "to_language", ], "units": [ "title", "description", ], "lessons": [ "title", "intro_text", ], "exercises": [ "prompt", "hint", ], "exercise_choices": [ "choice_text", "choice_explanation", ], "word_bank_items": [ "token_text", ], "pairs": [ "left_text", "right_text", ], "exercise_attempts": [ "user_answer_text", ], "achievements": [ "title", "description", ], "leaderboard_entries": [ "leaderboard_key", ], "notifications": [ "title", "message", ], "ui_themes": [ "name", "primary_color", "secondary_color", "accent_color", "background_color", "font_family", ], }; const columnsInt = { "units": [ "order_index", ], "lessons": [ "order_index", "xp_reward", "time_limit_seconds", ], "exercises": [ "order_index", "xp_value", ], "exercise_choices": [ "order_index", ], "word_bank_items": [ "order_index", ], "pairs": [ "order_index", ], "user_course_enrollments": [ "current_xp", "current_unit_index", "current_lesson_index", ], "lesson_attempts": [ "xp_earned", "hearts_used", "accuracy_percent", "time_spent_seconds", ], "exercise_attempts": [ "time_spent_ms", "xp_earned", ], "user_lesson_progress": [ "best_xp", "best_accuracy_percent", ], "achievements": [ "threshold_value", ], "leaderboard_entries": [ "xp_total", "rank_position", ], "streak_records": [ "current_streak_days", "best_streak_days", ], }; let allFoundRecords = []; for (const tableName in tableColumns) { if (tableColumns.hasOwnProperty(tableName)) { const attributesToSearch = tableColumns[tableName]; const attributesIntToSearch = columnsInt[tableName] || []; const whereCondition = { [Op.or]: [ ...attributesToSearch.map(attribute => ({ [attribute]: { [Op.iLike] : `%${searchQuery}%`, }, })), ...attributesIntToSearch.map(attribute => ( Sequelize.where( Sequelize.cast(Sequelize.col(`${tableName}.${attribute}`), 'varchar'), { [Op.iLike]: `%${searchQuery}%` } ) )), ], }; const hasPermission = await checkPermissions(`READ_${tableName.toUpperCase()}`, currentUser); if (!hasPermission) { continue; } const foundRecords = await db[tableName].findAll({ where: whereCondition, attributes: [...tableColumns[tableName], 'id', ...attributesIntToSearch], }); const modifiedRecords = foundRecords.map((record) => { const matchAttribute = []; for (const attribute of attributesToSearch) { if (record[attribute]?.toLowerCase()?.includes(searchQuery.toLowerCase())) { matchAttribute.push(attribute); } } for (const attribute of attributesIntToSearch) { const castedValue = String(record[attribute]); if (castedValue && castedValue.toLowerCase().includes(searchQuery.toLowerCase())) { matchAttribute.push(attribute); } } return { ...record.get(), matchAttribute, tableName, }; }); allFoundRecords = allFoundRecords.concat(modifiedRecords); } } return allFoundRecords; } catch (error) { throw error; } } }