186 lines
5.7 KiB
JavaScript

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 });
}
};