186 lines
5.7 KiB
JavaScript
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 });
|
|
}
|
|
};
|