import config from '../../config.js'; import log4js from 'log4js'; import mongoose from 'mongoose'; import { getAllModels, getModelByPrefix } from './model.js'; const logger = log4js.getLogger('Spotlight'); logger.level = config.server.logLevel; // Helper function to build search filter from query parameters const buildSearchFilter = (params) => { const filter = {}; for (const [key, value] of Object.entries(params)) { // Skip pagination and limit parameters as they're not search filters if (key === 'limit' || key === 'page') continue; // Handle different field types if (key === 'name') { filter.name = { $regex: value, $options: 'i' }; // Case-insensitive search } else if (key === 'id' || key === '_id') { if (mongoose.Types.ObjectId.isValid(value)) { filter._id = value; } } else if (key === 'tags') { filter.tags = { $in: [new RegExp(value, 'i')] }; } else if (key === 'state') { filter['state.type'] = value; } else if (key.includes('.')) { // Handle nested fields like 'state.type', 'address.city', etc. filter[key] = { $regex: value, $options: 'i' }; } else { // For all other fields, do a case-insensitive search filter[key] = { $regex: value, $options: 'i' }; } } return filter; }; const trimSpotlightObject = (object, objectType) => { return { _id: object._id, name: object.name || undefined, state: object.state && object?.state.type ? { type: object.state.type } : undefined, tags: object.tags || undefined, email: object.email || undefined, color: object.color || undefined, updatedAt: object.updatedAt || undefined, objectType: objectType || undefined, online: object.online || undefined, }; }; export const getSpotlightRouteHandler = async (req, res) => { try { const query = req.params.query; const queryParams = req.query; if (query.length < 3) { res.status(200).send([]); return; } const prefix = query.substring(0, 3).toUpperCase(); const delimiter = query.substring(3, 4); const suffix = query.substring(4); logger.trace(`Spotlight query: ${query}`); if (delimiter == ':') { const prefixEntry = getModelByPrefix(prefix); if (!prefixEntry || !prefixEntry.model) { res.status(400).send({ error: 'Invalid or unsupported prefix' }); return; } const { model, idField, type, referenceField } = prefixEntry; const suffixLength = suffix.length; // Validate ObjectId if the idField is '_id' if ( idField === '_id' && referenceField === '_reference' && !mongoose.Types.ObjectId.isValid(suffix) && suffixLength != 12 ) { res.status(200).send([]); return; } // Find the object by the correct field const queryObj = {}; if (suffixLength == 12) { queryObj[referenceField] = suffix.toUpperCase(); } else { queryObj[idField] = suffix.toLowerCase(); } let doc = await model.findOne(queryObj).lean(); if (!doc) { res.status(200).send([]); return; } // Build the response with only the required fields const response = trimSpotlightObject(doc, type); res.status(200).send(response); return; } console.log(queryParams); if (Object.keys(queryParams).length > 0) { const prefixEntry = getModelByPrefix(prefix); console.log(prefixEntry); if (!prefixEntry || !prefixEntry.model) { res.status(400).send({ error: 'Invalid or unsupported prefix' }); return; } const { model, type } = prefixEntry; // Use req.query for search parameters if (Object.keys(queryParams).length === 0) { res.status(400).send({ error: 'No search parameters provided' }); return; } // Build search filter const searchFilter = buildSearchFilter(queryParams); // Perform search with limit const limit = parseInt(req.query.limit) || 10; const docs = await model.find(searchFilter).limit(limit).sort({ updatedAt: -1 }).lean(); // Format response const response = docs.map((doc) => trimSpotlightObject(doc, type)); res.status(200).send(response); return; } // If no query params and no prefix, search all models if (Object.keys(queryParams).length === 0 && (!prefix || !getModelByPrefix(prefix))) { // Search all models for the query string in the 'name' field const searchTerm = query; if (!searchTerm || searchTerm.length < 3) { res.status(200).send([]); return; } // Only use models that are not null const allModelEntries = getAllModels().filter((entry) => entry.model); // Run all searches in parallel const searchPromises = allModelEntries.map(async (entry) => { try { const docs = await entry.model .find({ name: { $regex: searchTerm, $options: 'i' } }) .limit(5) .sort({ updatedAt: -1 }) .lean(); return docs.map((doc) => trimSpotlightObject(doc, entry.type)); } catch (e) { return []; } }); let results = await Promise.all(searchPromises); // Flatten and deduplicate by _id let flatResults = results.flat(); const seen = new Set(); const deduped = []; for (const obj of flatResults) { if (!seen.has(String(obj._id))) { seen.add(String(obj._id)); deduped.push(obj); } if (deduped.length >= 10) break; } res.status(200).send(deduped); return; } return res.status(200).send([]); } catch (error) { logger.error('Error in spotlight lookup:', error); res.status(500).send({ error: error }); } };