Implemented product stocks and minor improvements.
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
This commit is contained in:
parent
cf01c3aa38
commit
7c44f36590
@ -23,6 +23,7 @@ import {
|
||||
import { getAllModels } from '../services/misc/model.js';
|
||||
import { redisServer } from './redis.js';
|
||||
import { auditLogModel } from './schemas/management/auditlog.schema.js';
|
||||
import { convertObjectIdStringsInFilter } from './utils.js';
|
||||
|
||||
const logger = log4js.getLogger('Database');
|
||||
logger.level = config.server.logLevel;
|
||||
@ -570,8 +571,12 @@ export const listObjectsByProperties = async ({
|
||||
}
|
||||
}
|
||||
|
||||
if (masterFilter != {}) {
|
||||
pipeline.push({ $match: { ...masterFilter } });
|
||||
logger.debug('Master filter:', masterFilter);
|
||||
|
||||
if (Object.keys(masterFilter).length > 0) {
|
||||
const convertedFilter = convertObjectIdStringsInFilter(masterFilter);
|
||||
logger.debug('Converted filter:', convertedFilter);
|
||||
pipeline.push({ $match: convertedFilter });
|
||||
}
|
||||
|
||||
if (propertiesPresent) {
|
||||
@ -593,10 +598,13 @@ export const listObjectsByProperties = async ({
|
||||
} else {
|
||||
// If no properties specified, just return all objects without grouping
|
||||
// Ensure pipeline is not empty by adding a $match stage if needed
|
||||
if (pipeline.length === 0 && masterFilter == {}) {
|
||||
if (pipeline.length === 0 && Object.keys(masterFilter).length === 0) {
|
||||
console.log('Adding empty match stage');
|
||||
pipeline.push({ $match: {} });
|
||||
}
|
||||
console.log('Running pipeline:', pipeline);
|
||||
const results = await model.aggregate(pipeline);
|
||||
console.log('Results:', results);
|
||||
return results;
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
64
src/database/schemas/inventory/productstock.schema.js
Normal file
64
src/database/schemas/inventory/productstock.schema.js
Normal file
@ -0,0 +1,64 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
const { Schema } = mongoose;
|
||||
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
||||
|
||||
const partStockUsageSchema = new Schema({
|
||||
partStock: { type: Schema.Types.ObjectId, ref: 'partStock', required: false },
|
||||
part: { type: Schema.Types.ObjectId, ref: 'part', required: true },
|
||||
quantity: { type: Number, required: true },
|
||||
});
|
||||
|
||||
// Define the main productStock schema - tracks assembled products consisting of part stocks
|
||||
const productStockSchema = new Schema(
|
||||
{
|
||||
_reference: { type: String, default: () => generateId()() },
|
||||
state: {
|
||||
type: { type: String, required: true },
|
||||
progress: { type: Number, required: false },
|
||||
},
|
||||
product: { type: mongoose.Schema.Types.ObjectId, ref: 'product', required: true },
|
||||
currentQuantity: { type: Number, required: true },
|
||||
partStocks: [partStockUsageSchema],
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const rollupConfigs = [
|
||||
{
|
||||
name: 'totalCurrentQuantity',
|
||||
filter: {},
|
||||
rollups: [{ name: 'totalCurrentQuantity', property: 'currentQuantity', operation: 'sum' }],
|
||||
},
|
||||
];
|
||||
|
||||
productStockSchema.statics.stats = async function () {
|
||||
const results = await aggregateRollups({
|
||||
model: this,
|
||||
rollupConfigs: rollupConfigs,
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
productStockSchema.statics.history = async function (from, to) {
|
||||
const results = await aggregateRollupsHistory({
|
||||
model: this,
|
||||
startDate: from,
|
||||
endDate: to,
|
||||
rollupConfigs: rollupConfigs,
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
// Add virtual id getter
|
||||
productStockSchema.virtual('id').get(function () {
|
||||
return this._id;
|
||||
});
|
||||
|
||||
// Configure JSON serialization to include virtuals
|
||||
productStockSchema.set('toJSON', { virtuals: true });
|
||||
|
||||
// Create and export the model
|
||||
export const productStockModel = mongoose.model('productStock', productStockSchema);
|
||||
@ -12,6 +12,7 @@ import { orderItemModel } from './inventory/orderitem.schema.js';
|
||||
import { stockEventModel } from './inventory/stockevent.schema.js';
|
||||
import { stockAuditModel } from './inventory/stockaudit.schema.js';
|
||||
import { partStockModel } from './inventory/partstock.schema.js';
|
||||
import { productStockModel } from './inventory/productstock.schema.js';
|
||||
import { auditLogModel } from './management/auditlog.schema.js';
|
||||
import { userModel } from './management/user.schema.js';
|
||||
import { appPasswordModel } from './management/apppassword.schema.js';
|
||||
@ -115,12 +116,12 @@ export const models = {
|
||||
label: 'Part Stock',
|
||||
},
|
||||
PDS: {
|
||||
model: null,
|
||||
model: productStockModel,
|
||||
idField: '_id',
|
||||
type: 'productStock',
|
||||
referenceField: '_reference',
|
||||
label: 'Product Stock',
|
||||
}, // No productStockModel found
|
||||
},
|
||||
ADL: {
|
||||
model: auditLogModel,
|
||||
idField: '_id',
|
||||
|
||||
@ -1,7 +1,50 @@
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
export const generateId = () => {
|
||||
// 10 characters
|
||||
return customAlphabet(ALPHABET, 12);
|
||||
};
|
||||
|
||||
/** Check if a value is a string that looks like a MongoDB ObjectId (24 hex chars). */
|
||||
export function isObjectIdString(value) {
|
||||
return typeof value === 'string' && /^[a-f\d]{24}$/i.test(value);
|
||||
}
|
||||
|
||||
/** Convert a value to ObjectId if it's a valid ObjectId string; otherwise return as-is. */
|
||||
export function toObjectIdIfValid(value) {
|
||||
if (isObjectIdString(value)) {
|
||||
return new mongoose.Types.ObjectId(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Recursively convert ObjectId strings to ObjectId in a filter object for MongoDB $match. */
|
||||
export function convertObjectIdStringsInFilter(filter) {
|
||||
if (!filter || typeof filter !== 'object') return filter;
|
||||
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (key.startsWith('$')) {
|
||||
if ((key === '$in' || key === '$nin') && Array.isArray(value)) {
|
||||
result[key] = value.map((v) => (isObjectIdString(v) ? new mongoose.Types.ObjectId(v) : v));
|
||||
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
result[key] = convertObjectIdStringsInFilter(value);
|
||||
} else {
|
||||
result[key] = toObjectIdIfValid(value);
|
||||
}
|
||||
} else if (
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
!Array.isArray(value) &&
|
||||
!(value instanceof mongoose.Types.ObjectId) &&
|
||||
!(value instanceof Date)
|
||||
) {
|
||||
result[key] = convertObjectIdStringsInFilter(value);
|
||||
} else {
|
||||
result[key] = toObjectIdIfValid(value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
vendorRoutes,
|
||||
materialRoutes,
|
||||
partStockRoutes,
|
||||
productStockRoutes,
|
||||
filamentStockRoutes,
|
||||
purchaseOrderRoutes,
|
||||
orderItemRoutes,
|
||||
@ -135,6 +136,7 @@ app.use('/products', productRoutes);
|
||||
app.use('/vendors', vendorRoutes);
|
||||
app.use('/materials', materialRoutes);
|
||||
app.use('/partstocks', partStockRoutes);
|
||||
app.use('/productstocks', productStockRoutes);
|
||||
app.use('/filamentstocks', filamentStockRoutes);
|
||||
app.use('/purchaseorders', purchaseOrderRoutes);
|
||||
app.use('/orderitems', orderItemRoutes);
|
||||
|
||||
@ -14,6 +14,7 @@ import productRoutes from './management/products.js';
|
||||
import vendorRoutes from './management/vendors.js';
|
||||
import materialRoutes from './management/materials.js';
|
||||
import partStockRoutes from './inventory/partstocks.js';
|
||||
import productStockRoutes from './inventory/productstocks.js';
|
||||
import filamentStockRoutes from './inventory/filamentstocks.js';
|
||||
import purchaseOrderRoutes from './inventory/purchaseorders.js';
|
||||
import orderItemRoutes from './inventory/orderitems.js';
|
||||
@ -58,6 +59,7 @@ export {
|
||||
vendorRoutes,
|
||||
materialRoutes,
|
||||
partStockRoutes,
|
||||
productStockRoutes,
|
||||
filamentStockRoutes,
|
||||
purchaseOrderRoutes,
|
||||
orderItemRoutes,
|
||||
|
||||
64
src/routes/inventory/productstocks.js
Normal file
64
src/routes/inventory/productstocks.js
Normal file
@ -0,0 +1,64 @@
|
||||
import express from 'express';
|
||||
import { isAuthenticated } from '../../keycloak.js';
|
||||
import { getFilter, convertPropertiesString } from '../../utils.js';
|
||||
|
||||
const router = express.Router();
|
||||
import {
|
||||
listProductStocksRouteHandler,
|
||||
getProductStockRouteHandler,
|
||||
editProductStockRouteHandler,
|
||||
editMultipleProductStocksRouteHandler,
|
||||
newProductStockRouteHandler,
|
||||
deleteProductStockRouteHandler,
|
||||
listProductStocksByPropertiesRouteHandler,
|
||||
getProductStockStatsRouteHandler,
|
||||
getProductStockHistoryRouteHandler,
|
||||
} from '../../services/inventory/productstocks.js';
|
||||
|
||||
router.get('/', isAuthenticated, (req, res) => {
|
||||
const { page, limit, property, search, sort, order } = req.query;
|
||||
const allowedFilters = ['product', 'state', 'currentQuantity', 'product._id'];
|
||||
const filter = getFilter(req.query, allowedFilters);
|
||||
listProductStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||
});
|
||||
|
||||
router.get('/properties', isAuthenticated, (req, res) => {
|
||||
let properties = convertPropertiesString(req.query.properties);
|
||||
const allowedFilters = ['product', 'state.type'];
|
||||
const filter = getFilter(req.query, allowedFilters, false);
|
||||
var masterFilter = {};
|
||||
if (req.query.masterFilter) {
|
||||
masterFilter = JSON.parse(req.query.masterFilter);
|
||||
}
|
||||
listProductStocksByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||
});
|
||||
|
||||
router.post('/', isAuthenticated, (req, res) => {
|
||||
newProductStockRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.get('/stats', isAuthenticated, (req, res) => {
|
||||
getProductStockStatsRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.get('/history', isAuthenticated, (req, res) => {
|
||||
getProductStockHistoryRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.get('/:id', isAuthenticated, (req, res) => {
|
||||
getProductStockRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.put('/', isAuthenticated, async (req, res) => {
|
||||
editMultipleProductStocksRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
||||
editProductStockRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.delete('/:id', isAuthenticated, async (req, res) => {
|
||||
deleteProductStockRouteHandler(req, res);
|
||||
});
|
||||
|
||||
export default router;
|
||||
221
src/services/inventory/productstocks.js
Normal file
221
src/services/inventory/productstocks.js
Normal file
@ -0,0 +1,221 @@
|
||||
import config from '../../config.js';
|
||||
import { productStockModel } from '../../database/schemas/inventory/productstock.schema.js';
|
||||
import log4js from 'log4js';
|
||||
import mongoose from 'mongoose';
|
||||
import {
|
||||
deleteObject,
|
||||
listObjects,
|
||||
getObject,
|
||||
editObject,
|
||||
editObjects,
|
||||
newObject,
|
||||
listObjectsByProperties,
|
||||
getModelStats,
|
||||
getModelHistory,
|
||||
} from '../../database/database.js';
|
||||
import { productModel } from '../../database/schemas/management/product.schema.js';
|
||||
const logger = log4js.getLogger('Product Stocks');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
export const listProductStocksRouteHandler = async (
|
||||
req,
|
||||
res,
|
||||
page = 1,
|
||||
limit = 25,
|
||||
property = '',
|
||||
filter = {},
|
||||
search = '',
|
||||
sort = '',
|
||||
order = 'ascend'
|
||||
) => {
|
||||
const result = await listObjects({
|
||||
model: productStockModel,
|
||||
page,
|
||||
limit,
|
||||
property,
|
||||
filter,
|
||||
search,
|
||||
sort,
|
||||
order,
|
||||
populate: [{ path: 'product' }, { path: 'partStocks.partStock' }],
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
logger.error('Error listing product stocks.');
|
||||
res.status(result.code).send(result);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`List of product stocks (Page ${page}, Limit ${limit}). Count: ${result.length}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const listProductStocksByPropertiesRouteHandler = async (
|
||||
req,
|
||||
res,
|
||||
properties = '',
|
||||
filter = {},
|
||||
masterFilter = {}
|
||||
) => {
|
||||
const result = await listObjectsByProperties({
|
||||
model: productStockModel,
|
||||
properties,
|
||||
filter,
|
||||
populate: ['product', 'partStocks.partStock'],
|
||||
masterFilter,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
logger.error('Error listing product stocks.');
|
||||
res.status(result.code).send(result);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`List of product stocks. Count: ${result.length}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const getProductStockRouteHandler = async (req, res) => {
|
||||
const id = req.params.id;
|
||||
const result = await getObject({
|
||||
model: productStockModel,
|
||||
id,
|
||||
populate: [{ path: 'partStocks.part' }, { path: 'partStocks.partStock' }, { path: 'product' }],
|
||||
});
|
||||
if (result?.error) {
|
||||
logger.warn(`Product Stock not found with supplied id.`);
|
||||
return res.status(result.code).send(result);
|
||||
}
|
||||
logger.debug(`Retrieved product stock with ID: ${id}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const editProductStockRouteHandler = async (req, res) => {
|
||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||
|
||||
logger.trace(`Product Stock with ID: ${id}`);
|
||||
|
||||
const updateData = {
|
||||
partStocks: req.body?.partStocks?.map((partStock) => ({
|
||||
quantity: partStock.quantity,
|
||||
partStock: partStock.partStock,
|
||||
})),
|
||||
};
|
||||
|
||||
const result = await editObject({
|
||||
model: productStockModel,
|
||||
id,
|
||||
updateData,
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
logger.error('Error editing product stock:', result.error);
|
||||
res.status(result).send(result);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Edited product stock with ID: ${id}`);
|
||||
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const editMultipleProductStocksRouteHandler = async (req, res) => {
|
||||
const updates = req.body.map((update) => ({
|
||||
_id: update._id,
|
||||
}));
|
||||
|
||||
if (!Array.isArray(updates)) {
|
||||
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
|
||||
}
|
||||
|
||||
const result = await editObjects({
|
||||
model: productStockModel,
|
||||
updates,
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
logger.error('Error editing product stocks:', result.error);
|
||||
res.status(result.code || 500).send(result);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Edited ${updates.length} product stocks`);
|
||||
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const newProductStockRouteHandler = async (req, res) => {
|
||||
const productId = new mongoose.Types.ObjectId(req.body.product?._id);
|
||||
const product = await getObject({
|
||||
model: productModel,
|
||||
id: productId,
|
||||
});
|
||||
const newData = {
|
||||
updatedAt: new Date(),
|
||||
currentQuantity: req.body.currentQuantity,
|
||||
product: req.body.product,
|
||||
state: req.body.state,
|
||||
partStocks: product.parts.map((part) => ({
|
||||
part: part.part,
|
||||
quantity: part.quantity,
|
||||
partStock: undefined,
|
||||
})),
|
||||
};
|
||||
const result = await newObject({
|
||||
model: productStockModel,
|
||||
newData,
|
||||
user: req.user,
|
||||
});
|
||||
if (result.error) {
|
||||
logger.error('No product stock created:', result.error);
|
||||
return res.status(result.code).send(result);
|
||||
}
|
||||
|
||||
logger.debug(`New product stock with ID: ${result._id}`);
|
||||
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const deleteProductStockRouteHandler = async (req, res) => {
|
||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||
|
||||
logger.trace(`Product Stock with ID: ${id}`);
|
||||
|
||||
const result = await deleteObject({
|
||||
model: productStockModel,
|
||||
id,
|
||||
user: req.user,
|
||||
});
|
||||
if (result.error) {
|
||||
logger.error('No product stock deleted:', result.error);
|
||||
return res.status(result.code).send(result);
|
||||
}
|
||||
|
||||
logger.debug(`Deleted product stock with ID: ${result._id}`);
|
||||
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const getProductStockStatsRouteHandler = async (req, res) => {
|
||||
const result = await getModelStats({ model: productStockModel });
|
||||
if (result?.error) {
|
||||
logger.error('Error fetching product stock stats:', result.error);
|
||||
return res.status(result.code).send(result);
|
||||
}
|
||||
logger.trace('Product stock stats:', result);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const getProductStockHistoryRouteHandler = async (req, res) => {
|
||||
const from = req.query.from;
|
||||
const to = req.query.to;
|
||||
const result = await getModelHistory({ model: productStockModel, from, to });
|
||||
if (result?.error) {
|
||||
logger.error('Error fetching product stock history:', result.error);
|
||||
return res.status(result.code).send(result);
|
||||
}
|
||||
logger.trace('Product stock history:', result);
|
||||
res.send(result);
|
||||
};
|
||||
@ -1,161 +0,0 @@
|
||||
import config from '../../config.js';
|
||||
import { jobModel } from '../../database/schemas/production/job.schema.js';
|
||||
import { subJobModel } from '../../database/schemas/production/subjob.schema.js';
|
||||
import log4js from 'log4js';
|
||||
import { printerModel } from '../../database/schemas/production/printer.schema.js';
|
||||
import { filamentModel } from '../../database/schemas/management/filament.schema.js';
|
||||
import { gcodeFileModel } from '../../database/schemas/production/gcodefile.schema.js';
|
||||
import { partModel } from '../../database/schemas/management/part.schema.js';
|
||||
import { productModel } from '../../database/schemas/management/product.schema.js';
|
||||
import { vendorModel } from '../../database/schemas/management/vendor.schema.js';
|
||||
import { filamentStockModel } from '../../database/schemas/inventory/filamentstock.schema.js';
|
||||
import { stockEventModel } from '../../database/schemas/inventory/stockevent.schema.js';
|
||||
import { stockAuditModel } from '../../database/schemas/inventory/stockaudit.schema.js';
|
||||
import { partStockModel } from '../../database/schemas/inventory/partstock.schema.js';
|
||||
import { auditLogModel } from '../../database/schemas/management/auditlog.schema.js';
|
||||
import { userModel } from '../../database/schemas/management/user.schema.js';
|
||||
import { noteTypeModel } from '../../database/schemas/management/notetype.schema.js';
|
||||
import { noteModel } from '../../database/schemas/misc/note.schema.js';
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const logger = log4js.getLogger('Jobs');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
// Map prefixes to models and id fields
|
||||
const PREFIX_MODEL_MAP = {
|
||||
PRN: { model: printerModel, idField: '_id', type: 'printer' },
|
||||
FIL: { model: filamentModel, idField: '_id', type: 'filament' },
|
||||
SPL: { model: null, idField: '_id', type: 'spool' }, // No spool model found
|
||||
GCF: { model: gcodeFileModel, idField: '_id', type: 'gcodefile' },
|
||||
JOB: { model: jobModel, idField: '_id', type: 'job' },
|
||||
PRT: { model: partModel, idField: '_id', type: 'part' },
|
||||
PRD: { model: productModel, idField: '_id', type: 'product' },
|
||||
VEN: { model: vendorModel, idField: '_id', type: 'vendor' },
|
||||
SJB: { model: subJobModel, idField: '_id', type: 'subjob' },
|
||||
FLS: { model: filamentStockModel, idField: '_id', type: 'filamentstock' },
|
||||
SEV: { model: stockEventModel, idField: '_id', type: 'stockevent' },
|
||||
SAU: { model: stockAuditModel, idField: '_id', type: 'stockaudit' },
|
||||
PTS: { model: partStockModel, idField: '_id', type: 'partstock' },
|
||||
PDS: { model: null, idField: '_id', type: 'productstock' }, // No productStockModel found
|
||||
ADL: { model: auditLogModel, idField: '_id', type: 'auditlog' },
|
||||
USR: { model: userModel, idField: '_id', type: 'user' },
|
||||
NTY: { model: noteTypeModel, idField: '_id', type: 'notetype' },
|
||||
NTE: { model: noteModel, idField: '_id', type: 'note' },
|
||||
};
|
||||
|
||||
// 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) => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
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);
|
||||
const delimiter = query.substring(3, 4);
|
||||
const suffix = query.substring(4);
|
||||
|
||||
if (delimiter == ':') {
|
||||
const prefixEntry = PREFIX_MODEL_MAP[prefix];
|
||||
if (!prefixEntry || !prefixEntry.model) {
|
||||
res.status(400).send({ error: 'Invalid or unsupported prefix' });
|
||||
return;
|
||||
}
|
||||
const { model, idField } = prefixEntry;
|
||||
|
||||
// Validate ObjectId if the idField is '_id'
|
||||
if (idField === '_id' && !mongoose.Types.ObjectId.isValid(suffix)) {
|
||||
res.status(404).send({ error: `${prefix} not found` });
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the object by the correct field
|
||||
const queryObj = {};
|
||||
queryObj[idField] = suffix.toLowerCase();
|
||||
let doc = await model.findOne(queryObj).lean();
|
||||
if (!doc) {
|
||||
res.status(404).send({ error: `${prefix} not found` });
|
||||
return;
|
||||
}
|
||||
// Build the response with only the required fields
|
||||
const response = trimSpotlightObject(doc);
|
||||
res.status(200).send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(queryParams).length > 0) {
|
||||
const prefixEntry = PREFIX_MODEL_MAP[prefix];
|
||||
if (!prefixEntry || !prefixEntry.model) {
|
||||
res.status(400).send({ error: 'Invalid or unsupported prefix' });
|
||||
return;
|
||||
}
|
||||
const { model } = 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));
|
||||
|
||||
res.status(200).send(response);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error in spotlight lookup:', error);
|
||||
res.status(500).send({ error: error });
|
||||
}
|
||||
};
|
||||
@ -64,6 +64,7 @@ function getModelFilterFields(objectType) {
|
||||
subJob: ['job'],
|
||||
filamentStock: ['filament'],
|
||||
partStock: ['part'],
|
||||
productStock: ['product'],
|
||||
purchaseOrder: ['vendor'],
|
||||
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'],
|
||||
shipment: ['order._id', 'orderType', 'courierService._id'],
|
||||
|
||||
@ -70,6 +70,7 @@ function getModelFilterFields(objectType) {
|
||||
subJob: ['job'],
|
||||
filamentStock: ['filament'],
|
||||
partStock: ['part'],
|
||||
productStock: ['product'],
|
||||
purchaseOrder: ['vendor'],
|
||||
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'],
|
||||
shipment: ['order._id', 'orderType', 'courierService._id'],
|
||||
|
||||
@ -344,6 +344,7 @@ function getModelFilterFields(objectType) {
|
||||
subJob: ['job'],
|
||||
filamentStock: ['filament'],
|
||||
partStock: ['part'],
|
||||
productStock: ['product'],
|
||||
purchaseOrder: ['vendor'],
|
||||
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'],
|
||||
shipment: ['order._id', 'orderType', 'courierService._id'],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user