252 lines
7.6 KiB
JavaScript
252 lines
7.6 KiB
JavaScript
const db = require('../db/models');
|
|
const ValidationError = require('./notifications/errors/validation');
|
|
const RolesDBApi = require('../db/api/roles');
|
|
const GooglePlacesService = require('./googlePlaces');
|
|
|
|
const Sequelize = db.Sequelize;
|
|
const Op = Sequelize.Op;
|
|
|
|
// Cache for the 'Public' role object
|
|
let publicRoleCache = null;
|
|
|
|
async function getPublicRole() {
|
|
if (publicRoleCache) return publicRoleCache;
|
|
publicRoleCache = await RolesDBApi.findBy({ name: 'Public' });
|
|
return publicRoleCache;
|
|
}
|
|
|
|
/**
|
|
* @param {string} permission
|
|
* @param {object} currentUser
|
|
*/
|
|
async function checkPermissions(permission, currentUser) {
|
|
let role = null;
|
|
|
|
if (currentUser) {
|
|
const userPermission = currentUser.custom_permissions?.find(
|
|
(cp) => cp.name === permission,
|
|
);
|
|
if (userPermission) return true;
|
|
role = currentUser.app_role;
|
|
} else {
|
|
role = await getPublicRole();
|
|
}
|
|
|
|
if (!role) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
let permissions = [];
|
|
if (typeof role.getPermissions === 'function') {
|
|
permissions = await role.getPermissions();
|
|
} else {
|
|
permissions = role.permissions || [];
|
|
}
|
|
return !!permissions.find((p) => p.name === permission);
|
|
} catch (e) {
|
|
console.error("Search permission check error:", e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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",
|
|
],
|
|
"categories": [
|
|
"name",
|
|
"slug",
|
|
"icon",
|
|
"description",
|
|
],
|
|
"locations": [
|
|
"label",
|
|
"city",
|
|
"state",
|
|
"zip",
|
|
],
|
|
"businesses": [
|
|
"name",
|
|
"slug",
|
|
"description",
|
|
"phone",
|
|
"email",
|
|
"website",
|
|
"address",
|
|
"city",
|
|
"state",
|
|
"zip",
|
|
],
|
|
};
|
|
const columnsInt = {
|
|
"businesses": [
|
|
"lat",
|
|
"lng",
|
|
"reliability_score",
|
|
"response_time_median_minutes",
|
|
],
|
|
};
|
|
|
|
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 hasPerm = await checkPermissions(`READ_${tableName.toUpperCase()}`, currentUser);
|
|
if (!hasPerm) {
|
|
continue;
|
|
}
|
|
|
|
const include = [];
|
|
if (tableName === 'businesses') {
|
|
include.push({
|
|
model: db.business_photos,
|
|
as: 'business_photos_business',
|
|
include: [{
|
|
model: db.file,
|
|
as: 'photos'
|
|
}]
|
|
});
|
|
}
|
|
|
|
const foundRecords = await db[tableName].findAll({
|
|
where: whereCondition,
|
|
attributes: [...tableColumns[tableName], 'id', ...attributesIntToSearch],
|
|
include
|
|
});
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Special case: if we found categories, find businesses in those categories
|
|
const foundCategories = allFoundRecords.filter(r => r.tableName === 'categories');
|
|
if (foundCategories.length > 0) {
|
|
const categoryIds = foundCategories.map(c => c.id);
|
|
const businessesInCategories = await db.businesses.findAll({
|
|
include: [
|
|
{
|
|
model: db.business_categories,
|
|
as: 'business_categories_business',
|
|
where: { categoryId: { [Op.in]: categoryIds } }
|
|
},
|
|
{
|
|
model: db.business_photos,
|
|
as: 'business_photos_business',
|
|
include: [{
|
|
model: db.file,
|
|
as: 'photos'
|
|
}]
|
|
}
|
|
]
|
|
});
|
|
|
|
for (const biz of businessesInCategories) {
|
|
if (!allFoundRecords.find(r => r.id === biz.id && r.tableName === 'businesses')) {
|
|
allFoundRecords.push({
|
|
...biz.get(),
|
|
matchAttribute: ['category'],
|
|
tableName: 'businesses',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// If few local businesses found, try Google Places
|
|
const localBusinessesCount = allFoundRecords.filter(r => r.tableName === 'businesses').length;
|
|
if (localBusinessesCount < 5) {
|
|
try {
|
|
// If no location in search query, try to append a default location to help Google
|
|
let refinedQuery = searchQuery;
|
|
if (!searchQuery.match(/\d{5}/) && !searchQuery.match(/in\s+[A-Za-z]+/i)) {
|
|
refinedQuery = `${searchQuery} 22193`; // Default to Woodbridge, VA area
|
|
}
|
|
|
|
const googleResults = await GooglePlacesService.searchPlaces(refinedQuery);
|
|
for (const gPlace of googleResults) {
|
|
// Import each place to our DB
|
|
const importedBusiness = await GooglePlacesService.importFromGoogle(gPlace);
|
|
|
|
// Re-fetch with associations to get photos
|
|
const fullBusiness = await db.businesses.findByPk(importedBusiness.id, {
|
|
include: [{
|
|
model: db.business_photos,
|
|
as: 'business_photos_business',
|
|
include: [{
|
|
model: db.file,
|
|
as: 'photos'
|
|
}]
|
|
}]
|
|
});
|
|
|
|
// Add to search results if not already there
|
|
if (!allFoundRecords.find(r => r.id === fullBusiness.id)) {
|
|
allFoundRecords.push({
|
|
...fullBusiness.get(),
|
|
matchAttribute: ['name'],
|
|
tableName: 'businesses',
|
|
isFromGoogle: true,
|
|
});
|
|
}
|
|
}
|
|
} catch (gError) {
|
|
console.error("Google Search fallback error:", gError);
|
|
}
|
|
}
|
|
|
|
return allFoundRecords;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
} |