Added product category support, including new routes, database schemas, and service updates. Enhanced existing product functionalities to incorporate product categories in filtering and population.
This commit is contained in:
parent
92e07c97d7
commit
af0934b163
@ -12,6 +12,7 @@
|
||||
"body-parser": "^2.2.0",
|
||||
"canonical-json": "^0.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"diff": "^9.0.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"exceljs": "^4.4.0",
|
||||
"exifr": "^7.1.3",
|
||||
|
||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@ -32,6 +32,9 @@ importers:
|
||||
cors:
|
||||
specifier: ^2.8.5
|
||||
version: 2.8.6
|
||||
diff:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
dotenv:
|
||||
specifier: ^17.2.3
|
||||
version: 17.2.3
|
||||
@ -2265,6 +2268,10 @@ packages:
|
||||
dezalgo@1.0.4:
|
||||
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
|
||||
|
||||
diff@9.0.0:
|
||||
resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
|
||||
doctrine@2.1.0:
|
||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -7723,6 +7730,8 @@ snapshots:
|
||||
asap: 2.0.6
|
||||
wrappy: 1.0.2
|
||||
|
||||
diff@9.0.0: {}
|
||||
|
||||
doctrine@2.1.0:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
||||
@ -1,7 +1,26 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
const { Schema } = mongoose;
|
||||
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
||||
import { stockEventModel } from './stockevent.schema.js';
|
||||
|
||||
const getStockEventTotal = async (stock, parentType) => {
|
||||
const stockId = stock?._id;
|
||||
if (!stockId) return null;
|
||||
|
||||
const parentId =
|
||||
stockId instanceof mongoose.Types.ObjectId ? stockId : new mongoose.Types.ObjectId(stockId);
|
||||
|
||||
const [result] = await stockEventModel.aggregate([
|
||||
{ $match: { parent: parentId, parentType } },
|
||||
{ $group: { _id: null, total: { $sum: '$value' }, count: { $sum: 1 } } },
|
||||
]);
|
||||
|
||||
return {
|
||||
total: result?.total ?? 0,
|
||||
count: result?.count ?? 0,
|
||||
};
|
||||
};
|
||||
|
||||
// Define the main filamentStock schema
|
||||
const filamentStockSchema = new Schema(
|
||||
@ -32,7 +51,11 @@ const filamentStockSchema = new Schema(
|
||||
|
||||
filamentStockSchema.pre('validate', async function () {
|
||||
if (!this.filament && this.filamentSku) {
|
||||
const sku = await mongoose.model('filamentSku').findById(this.filamentSku).select('filament').lean();
|
||||
const sku = await mongoose
|
||||
.model('filamentSku')
|
||||
.findById(this.filamentSku)
|
||||
.select('filament')
|
||||
.lean();
|
||||
if (sku?.filament) this.filament = sku.filament;
|
||||
}
|
||||
});
|
||||
@ -66,6 +89,33 @@ filamentStockSchema.statics.history = async function (from, to) {
|
||||
return results;
|
||||
};
|
||||
|
||||
filamentStockSchema.statics.recalculate = async function (filamentStock, user) {
|
||||
const events = await getStockEventTotal(filamentStock, this.modelName);
|
||||
if (!events?.count) return;
|
||||
|
||||
const net = events.total;
|
||||
const startingNet = filamentStock.startingWeight?.net ?? 0;
|
||||
const startingGross = filamentStock.startingWeight?.gross ?? 0;
|
||||
const gross = startingNet > 0 ? (startingGross * net) / startingNet : net;
|
||||
|
||||
console.log('Recalculating filament stock');
|
||||
console.log('events', events);
|
||||
console.log('filamentStock', filamentStock);
|
||||
|
||||
await editObject({
|
||||
model: this,
|
||||
id: filamentStock._id,
|
||||
updateData: {
|
||||
currentWeight: {
|
||||
net,
|
||||
gross,
|
||||
},
|
||||
},
|
||||
user,
|
||||
recalculate: false,
|
||||
});
|
||||
};
|
||||
|
||||
// Add virtual id getter
|
||||
filamentStockSchema.virtual('id').get(function () {
|
||||
return this._id;
|
||||
|
||||
@ -1,7 +1,26 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
const { Schema } = mongoose;
|
||||
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
||||
import { stockEventModel } from './stockevent.schema.js';
|
||||
|
||||
const getStockEventTotal = async (stock, parentType) => {
|
||||
const stockId = stock?._id;
|
||||
if (!stockId) return null;
|
||||
|
||||
const parentId =
|
||||
stockId instanceof mongoose.Types.ObjectId ? stockId : new mongoose.Types.ObjectId(stockId);
|
||||
|
||||
const [result] = await stockEventModel.aggregate([
|
||||
{ $match: { parent: parentId, parentType } },
|
||||
{ $group: { _id: null, total: { $sum: '$value' }, count: { $sum: 1 } } },
|
||||
]);
|
||||
|
||||
return {
|
||||
total: result?.total ?? 0,
|
||||
count: result?.count ?? 0,
|
||||
};
|
||||
};
|
||||
|
||||
// Define the main partStock schema
|
||||
const partStockSchema = new Schema(
|
||||
@ -53,6 +72,21 @@ partStockSchema.statics.history = async function (from, to) {
|
||||
return results;
|
||||
};
|
||||
|
||||
partStockSchema.statics.recalculate = async function (partStock, user) {
|
||||
const events = await getStockEventTotal(partStock, this.modelName);
|
||||
if (!events?.count) return;
|
||||
|
||||
await editObject({
|
||||
model: this,
|
||||
id: partStock._id,
|
||||
updateData: {
|
||||
currentQuantity: events.total,
|
||||
},
|
||||
user,
|
||||
recalculate: false,
|
||||
});
|
||||
};
|
||||
|
||||
// Add virtual id getter
|
||||
partStockSchema.virtual('id').get(function () {
|
||||
return this._id;
|
||||
|
||||
@ -1,7 +1,26 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
const { Schema } = mongoose;
|
||||
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
||||
import { stockEventModel } from './stockevent.schema.js';
|
||||
|
||||
const getStockEventTotal = async (stock, parentType) => {
|
||||
const stockId = stock?._id;
|
||||
if (!stockId) return null;
|
||||
|
||||
const parentId =
|
||||
stockId instanceof mongoose.Types.ObjectId ? stockId : new mongoose.Types.ObjectId(stockId);
|
||||
|
||||
const [result] = await stockEventModel.aggregate([
|
||||
{ $match: { parent: parentId, parentType } },
|
||||
{ $group: { _id: null, total: { $sum: '$value' }, count: { $sum: 1 } } },
|
||||
]);
|
||||
|
||||
return {
|
||||
total: result?.total ?? 0,
|
||||
count: result?.count ?? 0,
|
||||
};
|
||||
};
|
||||
|
||||
const partStockUsageSchema = new Schema({
|
||||
partStock: { type: Schema.Types.ObjectId, ref: 'partStock', required: false },
|
||||
@ -68,6 +87,21 @@ productStockSchema.statics.history = async function (from, to) {
|
||||
return results;
|
||||
};
|
||||
|
||||
productStockSchema.statics.recalculate = async function (productStock, user) {
|
||||
const events = await getStockEventTotal(productStock, this.modelName);
|
||||
if (!events?.count) return;
|
||||
|
||||
await editObject({
|
||||
model: this,
|
||||
id: productStock._id,
|
||||
updateData: {
|
||||
currentQuantity: events.total,
|
||||
},
|
||||
user,
|
||||
recalculate: false,
|
||||
});
|
||||
};
|
||||
|
||||
// Add virtual id getter
|
||||
productStockSchema.virtual('id').get(function () {
|
||||
return this._id;
|
||||
|
||||
@ -11,7 +11,7 @@ const stockTransferLineSchema = new Schema(
|
||||
},
|
||||
fromStock: {
|
||||
type: Schema.Types.ObjectId,
|
||||
refPath: 'fromStockType',
|
||||
refPath: 'lines.fromStockType',
|
||||
required: true,
|
||||
},
|
||||
quantity: { type: Number, required: true },
|
||||
@ -27,7 +27,7 @@ const stockTransferLineSchema = new Schema(
|
||||
},
|
||||
toStock: {
|
||||
type: Schema.Types.ObjectId,
|
||||
refPath: 'toStockType',
|
||||
refPath: 'lines.toStockType',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
@ -37,6 +37,7 @@ const stockTransferLineSchema = new Schema(
|
||||
const stockTransferSchema = new Schema(
|
||||
{
|
||||
_reference: { type: String, default: () => generateId()() },
|
||||
name: { type: String, required: true },
|
||||
state: {
|
||||
type: { type: String, required: true, default: 'draft' },
|
||||
progress: { type: Number, required: false },
|
||||
|
||||
@ -7,6 +7,7 @@ const productSchema = new Schema(
|
||||
{
|
||||
_reference: { type: String, default: () => generateId()() },
|
||||
name: { type: String, required: true },
|
||||
productCategory: { type: Schema.Types.ObjectId, ref: 'productCategory', required: true },
|
||||
tags: [{ type: String }],
|
||||
version: { type: String },
|
||||
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
|
||||
|
||||
18
src/database/schemas/management/productcategory.schema.js
Normal file
18
src/database/schemas/management/productcategory.schema.js
Normal file
@ -0,0 +1,18 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
|
||||
const productCategorySchema = new mongoose.Schema(
|
||||
{
|
||||
_reference: { type: String, default: () => generateId()() },
|
||||
name: { required: true, type: String },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
productCategorySchema.virtual('id').get(function () {
|
||||
return this._id;
|
||||
});
|
||||
|
||||
productCategorySchema.set('toJSON', { virtuals: true });
|
||||
|
||||
export const productCategoryModel = mongoose.model('productCategory', productCategorySchema);
|
||||
@ -7,6 +7,7 @@ import { gcodeFileModel } from './production/gcodefile.schema.js';
|
||||
import { partModel } from './management/part.schema.js';
|
||||
import { partSkuModel } from './management/partsku.schema.js';
|
||||
import { productModel } from './management/product.schema.js';
|
||||
import { productCategoryModel } from './management/productcategory.schema.js';
|
||||
import { productSkuModel } from './management/productsku.schema.js';
|
||||
import { vendorModel } from './management/vendor.schema.js';
|
||||
import { materialModel } from './management/material.schema.js';
|
||||
@ -43,6 +44,7 @@ import { salesOrderModel } from './sales/salesorder.schema.js';
|
||||
import { marketplaceModel } from './sales/marketplace.schema.js';
|
||||
import { listingModel } from './sales/listing.schema.js';
|
||||
import { listingVarientModel } from './sales/listingvarient.schema.js';
|
||||
import { paymentModel } from './finance/payment.schema.js';
|
||||
|
||||
// Map prefixes to models and id fields
|
||||
export const models = {
|
||||
@ -96,6 +98,13 @@ export const models = {
|
||||
referenceField: '_reference',
|
||||
label: 'Product',
|
||||
},
|
||||
PCG: {
|
||||
model: productCategoryModel,
|
||||
idField: '_id',
|
||||
type: 'productCategory',
|
||||
referenceField: '_reference',
|
||||
label: 'Product Category',
|
||||
},
|
||||
SKU: {
|
||||
model: productSkuModel,
|
||||
idField: '_id',
|
||||
@ -355,4 +364,11 @@ export const models = {
|
||||
label: 'Listing Varient',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
PAY: {
|
||||
model: paymentModel,
|
||||
idField: '_id',
|
||||
type: 'payment',
|
||||
label: 'Payment',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
};
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
partRoutes,
|
||||
partSkuRoutes,
|
||||
productRoutes,
|
||||
productCategoryRoutes,
|
||||
productSkuRoutes,
|
||||
vendorRoutes,
|
||||
materialRoutes,
|
||||
@ -143,6 +144,7 @@ app.use('/filamentskus', filamentSkuRoutes);
|
||||
app.use('/parts', partRoutes);
|
||||
app.use('/partskus', partSkuRoutes);
|
||||
app.use('/products', productRoutes);
|
||||
app.use('/productcategories', productCategoryRoutes);
|
||||
app.use('/productskus', productSkuRoutes);
|
||||
app.use('/vendors', vendorRoutes);
|
||||
app.use('/materials', materialRoutes);
|
||||
|
||||
@ -13,6 +13,7 @@ import spotlightRoutes from './misc/spotlight.js';
|
||||
import partRoutes from './management/parts.js';
|
||||
import partSkuRoutes from './management/partskus.js';
|
||||
import productRoutes from './management/products.js';
|
||||
import productCategoryRoutes from './management/productcategories.js';
|
||||
import productSkuRoutes from './management/productskus.js';
|
||||
import vendorRoutes from './management/vendors.js';
|
||||
import materialRoutes from './management/materials.js';
|
||||
@ -66,6 +67,7 @@ export {
|
||||
partRoutes,
|
||||
partSkuRoutes,
|
||||
productRoutes,
|
||||
productCategoryRoutes,
|
||||
productSkuRoutes,
|
||||
vendorRoutes,
|
||||
materialRoutes,
|
||||
|
||||
@ -35,7 +35,13 @@ router.get('/', isAuthenticated, (req, res) => {
|
||||
|
||||
router.get('/properties', isAuthenticated, (req, res) => {
|
||||
let properties = convertPropertiesString(req.query.properties);
|
||||
const allowedFilters = ['filament', 'filament._id', 'filamentSku', 'state.type'];
|
||||
const allowedFilters = [
|
||||
'filament',
|
||||
'filament._id',
|
||||
'filamentSku',
|
||||
'state.type',
|
||||
'filamentSku._id',
|
||||
];
|
||||
const filter = getFilter(req.query, allowedFilters, false);
|
||||
var masterFilter = {};
|
||||
if (req.query.masterFilter) {
|
||||
|
||||
@ -21,14 +21,36 @@ import {
|
||||
// list of purchase orders
|
||||
router.get('/', isAuthenticated, (req, res) => {
|
||||
const { page, limit, property, search, sort, order } = req.query;
|
||||
const allowedFilters = ['vendor', 'state', 'value', 'vendor._id'];
|
||||
const allowedFilters = [
|
||||
'vendor',
|
||||
'state',
|
||||
'value',
|
||||
'vendor._id',
|
||||
'totalAmount',
|
||||
'totalAmountWithTax',
|
||||
'totalTaxAmount',
|
||||
'shippingAmount',
|
||||
'shippingAmountWithTax',
|
||||
'grandTotalAmount',
|
||||
];
|
||||
const filter = getFilter(req.query, allowedFilters);
|
||||
listPurchaseOrdersRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||
});
|
||||
|
||||
router.get('/properties', isAuthenticated, (req, res) => {
|
||||
let properties = convertPropertiesString(req.query.properties);
|
||||
const allowedFilters = ['vendor', 'state.type', 'value', 'vendor._id'];
|
||||
const allowedFilters = [
|
||||
'vendor',
|
||||
'state.type',
|
||||
'value',
|
||||
'vendor._id',
|
||||
'totalAmount',
|
||||
'totalAmountWithTax',
|
||||
'totalTaxAmount',
|
||||
'shippingAmount',
|
||||
'shippingAmountWithTax',
|
||||
'grandTotalAmount',
|
||||
];
|
||||
const filter = getFilter(req.query, allowedFilters, false);
|
||||
var masterFilter = {};
|
||||
if (req.query.masterFilter) {
|
||||
|
||||
55
src/routes/management/productcategories.js
Normal file
55
src/routes/management/productcategories.js
Normal file
@ -0,0 +1,55 @@
|
||||
import express from 'express';
|
||||
import { isAuthenticated } from '../../keycloak.js';
|
||||
import { convertPropertiesString, getFilter } from '../../utils.js';
|
||||
import {
|
||||
deleteProductCategoryRouteHandler,
|
||||
editProductCategoryRouteHandler,
|
||||
getProductCategoryHistoryRouteHandler,
|
||||
getProductCategoryRouteHandler,
|
||||
getProductCategoryStatsRouteHandler,
|
||||
listProductCategoriesByPropertiesRouteHandler,
|
||||
listProductCategoriesRouteHandler,
|
||||
newProductCategoryRouteHandler,
|
||||
} from '../../services/management/productcategories.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', isAuthenticated, (req, res) => {
|
||||
const { page, limit, property, search, sort, order } = req.query;
|
||||
const allowedFilters = ['_id', 'name'];
|
||||
const filter = getFilter(req.query, allowedFilters);
|
||||
listProductCategoriesRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||
});
|
||||
|
||||
router.get('/properties', isAuthenticated, (req, res) => {
|
||||
const properties = convertPropertiesString(req.query.properties);
|
||||
const allowedFilters = ['name'];
|
||||
const filter = getFilter(req.query, allowedFilters, false);
|
||||
listProductCategoriesByPropertiesRouteHandler(req, res, properties, filter);
|
||||
});
|
||||
|
||||
router.post('/', isAuthenticated, (req, res) => {
|
||||
newProductCategoryRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.get('/stats', isAuthenticated, (req, res) => {
|
||||
getProductCategoryStatsRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.get('/history', isAuthenticated, (req, res) => {
|
||||
getProductCategoryHistoryRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.get('/:id', isAuthenticated, (req, res) => {
|
||||
getProductCategoryRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
||||
editProductCategoryRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.delete('/:id', isAuthenticated, async (req, res) => {
|
||||
deleteProductCategoryRouteHandler(req, res);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@ -17,7 +17,7 @@ import {
|
||||
// list of products
|
||||
router.get('/', isAuthenticated, (req, res) => {
|
||||
const { page, limit, property, search, sort, order } = req.query;
|
||||
const allowedFilters = ['_id', 'name', 'globalPrice'];
|
||||
const allowedFilters = ['_id', 'name', 'globalPrice', 'productCategory', 'productCategory._id'];
|
||||
const filter = getFilter(req.query, allowedFilters);
|
||||
listProductsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||
});
|
||||
|
||||
@ -16,7 +16,7 @@ import { convertPropertiesString, getFilter } from '../../utils.js';
|
||||
// list of jobs
|
||||
router.get('/', isAuthenticated, (req, res) => {
|
||||
const { page, limit, property, search, sort, order } = req.query;
|
||||
const allowedFilters = ['state'];
|
||||
const allowedFilters = ['state', 'gcodeFile._id', 'quantity'];
|
||||
const filter = getFilter(req.query, allowedFilters);
|
||||
listJobsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||
});
|
||||
|
||||
@ -81,11 +81,7 @@ export const listPaymentsByPropertiesRouteHandler = async (
|
||||
|
||||
export const getPaymentRouteHandler = async (req, res) => {
|
||||
const id = req.params.id;
|
||||
const populateFields = [
|
||||
{ path: 'vendor' },
|
||||
{ path: 'client' },
|
||||
{ path: 'invoice' },
|
||||
];
|
||||
const populateFields = [{ path: 'vendor' }, { path: 'client' }, { path: 'invoice' }];
|
||||
const result = await getObject({
|
||||
model: paymentModel,
|
||||
id,
|
||||
@ -181,8 +177,11 @@ export const newPaymentRouteHandler = async (req, res) => {
|
||||
// Get invoice to populate vendor/client
|
||||
const invoice = await getObject({
|
||||
model: invoiceModel,
|
||||
id: req.body.invoice,
|
||||
populate: [{ path: 'vendor' }, { path: 'client' }],
|
||||
id: req.body.invoice?._id || req.body.invoice,
|
||||
populate: [
|
||||
{ path: 'vendor', strictPopulate: false },
|
||||
{ path: 'client', strictPopulate: false },
|
||||
],
|
||||
});
|
||||
|
||||
if (invoice.error) {
|
||||
@ -370,4 +369,3 @@ export const cancelPaymentRouteHandler = async (req, res) => {
|
||||
logger.debug(`Cancelled payment with ID: ${id}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
|
||||
@ -64,7 +64,7 @@ export const listFilamentStocksByPropertiesRouteHandler = async (
|
||||
model: filamentStockModel,
|
||||
properties,
|
||||
filter,
|
||||
populate: ['filament', { path: 'filamentSku', populate: 'filament' }, 'stockLocation'],
|
||||
populate: ['filament', 'filamentSku', 'stockLocation'],
|
||||
masterFilter,
|
||||
});
|
||||
|
||||
|
||||
@ -30,31 +30,78 @@ const normalizeLineInput = (l) => ({
|
||||
toStockLocation: l.toStockLocation?._id ?? l.toStockLocation,
|
||||
});
|
||||
|
||||
async function createStockEventsForLine({ transferId, fromId, fromType, toId, toType, qty, unit }) {
|
||||
const stockModelByType = {
|
||||
filamentStock: filamentStockModel,
|
||||
partStock: partStockModel,
|
||||
productStock: productStockModel,
|
||||
};
|
||||
|
||||
async function createStockEvent(newData, user) {
|
||||
const result = await newObject({
|
||||
model: stockEventModel,
|
||||
newData,
|
||||
user,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function recalculateStock(stockType, stock, user) {
|
||||
const model = stockModelByType[stockType];
|
||||
if (!model?.recalculate) return;
|
||||
|
||||
await model.recalculate(stock, user);
|
||||
}
|
||||
|
||||
async function createStockEventsForLine({ transferId, fromId, fromType, toId, toType, qty, unit, user }) {
|
||||
const ts = new Date();
|
||||
await stockEventModel.insertMany([
|
||||
{
|
||||
value: -Math.abs(qty),
|
||||
unit,
|
||||
parent: fromId,
|
||||
parentType: fromType,
|
||||
owner: transferId,
|
||||
ownerType: 'stockTransfer',
|
||||
timestamp: ts,
|
||||
},
|
||||
{
|
||||
value: Math.abs(qty),
|
||||
unit,
|
||||
parent: toId,
|
||||
parentType: toType,
|
||||
owner: transferId,
|
||||
ownerType: 'stockTransfer',
|
||||
timestamp: ts,
|
||||
},
|
||||
await Promise.all([
|
||||
createStockEvent(
|
||||
{
|
||||
value: -Math.abs(qty),
|
||||
unit,
|
||||
parent: fromId,
|
||||
parentType: fromType,
|
||||
owner: transferId,
|
||||
ownerType: 'stockTransfer',
|
||||
timestamp: ts,
|
||||
},
|
||||
user
|
||||
),
|
||||
createStockEvent(
|
||||
{
|
||||
value: Math.abs(qty),
|
||||
unit,
|
||||
parent: toId,
|
||||
parentType: toType,
|
||||
owner: transferId,
|
||||
ownerType: 'stockTransfer',
|
||||
timestamp: ts,
|
||||
},
|
||||
user
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
async function executePostedLine(transferId, line) {
|
||||
async function createStock(model, newData, user) {
|
||||
const result = await newObject({
|
||||
model,
|
||||
newData,
|
||||
user,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function executePostedLine(transferId, line, user) {
|
||||
const toLocId = line.toStockLocation;
|
||||
const loc = await stockLocationModel.findById(toLocId).lean();
|
||||
if (!loc) {
|
||||
@ -73,25 +120,23 @@ async function executePostedLine(transferId, line) {
|
||||
throw new Error('Filament transfer quantity exceeds available net weight');
|
||||
}
|
||||
const tareBefore = Math.max(0, (src.currentWeight?.gross ?? 0) - (src.currentWeight?.net ?? 0));
|
||||
const newNet = netAvail - line.quantity;
|
||||
const ratio = netAvail > 0 ? newNet / netAvail : 0;
|
||||
const newGross = (src.currentWeight?.gross ?? 0) * ratio;
|
||||
await filamentStockModel.findByIdAndUpdate(src._id, {
|
||||
$set: { 'currentWeight.net': newNet, 'currentWeight.gross': newGross },
|
||||
});
|
||||
|
||||
const destWeight = {
|
||||
net: line.quantity,
|
||||
gross: line.quantity + tareBefore,
|
||||
};
|
||||
const dest = await filamentStockModel.create({
|
||||
state: src.state,
|
||||
startingWeight: destWeight,
|
||||
currentWeight: destWeight,
|
||||
filament: src.filament,
|
||||
filamentSku: src.filamentSku,
|
||||
stockLocation: toLocId,
|
||||
});
|
||||
const dest = await createStock(
|
||||
filamentStockModel,
|
||||
{
|
||||
state: src.state,
|
||||
startingWeight: destWeight,
|
||||
currentWeight: destWeight,
|
||||
filament: src.filament,
|
||||
filamentSku: src.filamentSku,
|
||||
stockLocation: toLocId,
|
||||
},
|
||||
user
|
||||
);
|
||||
|
||||
await createStockEventsForLine({
|
||||
transferId,
|
||||
@ -101,31 +146,37 @@ async function executePostedLine(transferId, line) {
|
||||
toType: 'filamentStock',
|
||||
qty: line.quantity,
|
||||
unit: 'g',
|
||||
user,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
recalculateStock('filamentStock', src, user),
|
||||
recalculateStock('filamentStock', dest, user),
|
||||
]);
|
||||
|
||||
return { toStockType: 'filamentStock', toStock: dest._id };
|
||||
}
|
||||
|
||||
if (line.fromStockType === 'partStock') {
|
||||
const src = await partStockModel.findById(line.fromStock);
|
||||
console.log(src);
|
||||
if (!src) throw new Error('From part stock not found');
|
||||
const currentQuantity = src.state.type === 'new' ? src.startingQuantity : src.currentQuantity;
|
||||
const currentQuantity = src.currentQuantity;
|
||||
if (line.quantity > currentQuantity) {
|
||||
throw new Error('Part transfer quantity exceeds current quantity');
|
||||
}
|
||||
await partStockModel.findByIdAndUpdate(src._id, {
|
||||
$inc: { currentQuantity: -line.quantity },
|
||||
});
|
||||
|
||||
const dest = await partStockModel.create({
|
||||
partSku: src.partSku,
|
||||
currentQuantity: line.quantity,
|
||||
state: { type: 'new' },
|
||||
sourceType: 'stockTransfer',
|
||||
source: transferId,
|
||||
stockLocation: toLocId,
|
||||
});
|
||||
const dest = await createStock(
|
||||
partStockModel,
|
||||
{
|
||||
partSku: src.partSku,
|
||||
currentQuantity: line.quantity,
|
||||
state: { type: 'new' },
|
||||
sourceType: 'stockTransfer',
|
||||
source: transferId,
|
||||
stockLocation: toLocId,
|
||||
},
|
||||
user
|
||||
);
|
||||
|
||||
await createStockEventsForLine({
|
||||
transferId,
|
||||
@ -135,8 +186,14 @@ async function executePostedLine(transferId, line) {
|
||||
toType: 'partStock',
|
||||
qty: line.quantity,
|
||||
unit: 'each',
|
||||
user,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
recalculateStock('partStock', src, user),
|
||||
recalculateStock('partStock', dest, user),
|
||||
]);
|
||||
|
||||
return { toStockType: 'partStock', toStock: dest._id };
|
||||
}
|
||||
|
||||
@ -146,18 +203,19 @@ async function executePostedLine(transferId, line) {
|
||||
if (line.quantity > src.currentQuantity) {
|
||||
throw new Error('Product transfer quantity exceeds current quantity');
|
||||
}
|
||||
await productStockModel.findByIdAndUpdate(src._id, {
|
||||
$inc: { currentQuantity: -line.quantity },
|
||||
});
|
||||
|
||||
const dest = await productStockModel.create({
|
||||
productSku: src.productSku,
|
||||
currentQuantity: line.quantity,
|
||||
state: { type: 'posted' },
|
||||
postedAt: new Date(),
|
||||
partStocks: [],
|
||||
stockLocation: toLocId,
|
||||
});
|
||||
const dest = await createStock(
|
||||
productStockModel,
|
||||
{
|
||||
productSku: src.productSku,
|
||||
currentQuantity: line.quantity,
|
||||
state: { type: 'posted' },
|
||||
postedAt: new Date(),
|
||||
partStocks: [],
|
||||
stockLocation: toLocId,
|
||||
},
|
||||
user
|
||||
);
|
||||
|
||||
await createStockEventsForLine({
|
||||
transferId,
|
||||
@ -167,8 +225,14 @@ async function executePostedLine(transferId, line) {
|
||||
toType: 'productStock',
|
||||
qty: line.quantity,
|
||||
unit: 'each',
|
||||
user,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
recalculateStock('productStock', src, user),
|
||||
recalculateStock('productStock', dest, user),
|
||||
]);
|
||||
|
||||
return { toStockType: 'productStock', toStock: dest._id };
|
||||
}
|
||||
|
||||
@ -280,6 +344,9 @@ export const editStockTransferRouteHandler = async (req, res) => {
|
||||
const updateData = {
|
||||
lines: (req.body.lines || []).map((l) => normalizeLineInput(l)),
|
||||
};
|
||||
if (req.body.name !== undefined) {
|
||||
updateData.name = req.body.name;
|
||||
}
|
||||
|
||||
const result = await editObject({
|
||||
model: stockTransferModel,
|
||||
@ -325,6 +392,7 @@ export const editMultipleStockTransfersRouteHandler = async (req, res) => {
|
||||
|
||||
export const newStockTransferRouteHandler = async (req, res) => {
|
||||
const newData = {
|
||||
name: req.body.name,
|
||||
state: req.body.state ?? { type: 'draft' },
|
||||
lines: (req.body.lines || []).map((l) => normalizeLineInput(l)),
|
||||
};
|
||||
@ -404,7 +472,7 @@ export const postStockTransferRouteHandler = async (req, res) => {
|
||||
try {
|
||||
for (const line of doc.lines) {
|
||||
const plain = line.toObject();
|
||||
const { toStockType, toStock } = await executePostedLine(doc._id, plain);
|
||||
const { toStockType, toStock } = await executePostedLine(doc._id, plain, req.user);
|
||||
updatedLines.push({
|
||||
...plain,
|
||||
toStockType,
|
||||
@ -412,24 +480,34 @@ export const postStockTransferRouteHandler = async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const posted = await stockTransferModel
|
||||
.findByIdAndUpdate(
|
||||
id,
|
||||
{
|
||||
$set: {
|
||||
state: { type: 'posted' },
|
||||
postedAt: new Date(),
|
||||
lines: updatedLines,
|
||||
},
|
||||
},
|
||||
{ new: true }
|
||||
)
|
||||
.populate([
|
||||
const postedResult = await editObject({
|
||||
model: stockTransferModel,
|
||||
id,
|
||||
updateData: {
|
||||
state: { type: 'posted' },
|
||||
postedAt: new Date(),
|
||||
lines: updatedLines,
|
||||
},
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
if (postedResult?.error) {
|
||||
throw new Error(postedResult.error);
|
||||
}
|
||||
|
||||
const posted = await getObject({
|
||||
model: stockTransferModel,
|
||||
id,
|
||||
populate: [
|
||||
{ path: 'lines.fromStock' },
|
||||
{ path: 'lines.toStockLocation' },
|
||||
{ path: 'lines.toStock' },
|
||||
])
|
||||
.lean();
|
||||
],
|
||||
});
|
||||
|
||||
if (posted?.error) {
|
||||
throw new Error(posted.error);
|
||||
}
|
||||
|
||||
logger.debug(`Posted stock transfer with ID: ${id}`);
|
||||
res.send(posted);
|
||||
|
||||
@ -42,7 +42,10 @@ export const listAuditLogsRouteHandler = async (
|
||||
.sort({ [sort]: sortOrder })
|
||||
.skip(skip)
|
||||
.limit(Number(limit))
|
||||
.populate('owner', 'name _id');
|
||||
.populate([
|
||||
{ path: 'owner', select: 'name _id color' },
|
||||
{ path: 'parent', select: '_id name' },
|
||||
]);
|
||||
|
||||
const auditLogs = await query;
|
||||
logger.trace(
|
||||
@ -51,7 +54,7 @@ export const listAuditLogsRouteHandler = async (
|
||||
);
|
||||
|
||||
const expandedIdAuditLogs = auditLogs.map((auditLog) => {
|
||||
const expendedAuditLog = { ...auditLog._doc, parent: { _id: auditLog.parent } };
|
||||
const expendedAuditLog = { ...auditLog._doc };
|
||||
return expendedAuditLog;
|
||||
});
|
||||
res.send(expandedIdAuditLogs);
|
||||
|
||||
177
src/services/management/productcategories.js
Normal file
177
src/services/management/productcategories.js
Normal file
@ -0,0 +1,177 @@
|
||||
import config from '../../config.js';
|
||||
import { productCategoryModel } from '../../database/schemas/management/productcategory.schema.js';
|
||||
import log4js from 'log4js';
|
||||
import mongoose from 'mongoose';
|
||||
import {
|
||||
deleteObject,
|
||||
editObject,
|
||||
getModelHistory,
|
||||
getModelStats,
|
||||
getObject,
|
||||
listObjects,
|
||||
listObjectsByProperties,
|
||||
newObject,
|
||||
} from '../../database/database.js';
|
||||
|
||||
const logger = log4js.getLogger('ProductCategories');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
export const listProductCategoriesRouteHandler = async (
|
||||
req,
|
||||
res,
|
||||
page = 1,
|
||||
limit = 25,
|
||||
property = '',
|
||||
filter = {},
|
||||
search = '',
|
||||
sort = '',
|
||||
order = 'ascend'
|
||||
) => {
|
||||
const result = await listObjects({
|
||||
model: productCategoryModel,
|
||||
page,
|
||||
limit,
|
||||
property,
|
||||
filter,
|
||||
search,
|
||||
sort,
|
||||
order,
|
||||
populate: [],
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
logger.error('Error listing product categories.');
|
||||
res.status(result.code).send(result);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`List of product categories (Page ${page}, Limit ${limit}). Count: ${result.length}.`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const listProductCategoriesByPropertiesRouteHandler = async (
|
||||
req,
|
||||
res,
|
||||
properties = [],
|
||||
filter = {}
|
||||
) => {
|
||||
const result = await listObjectsByProperties({
|
||||
model: productCategoryModel,
|
||||
properties,
|
||||
filter,
|
||||
populate: [],
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
logger.error('Error listing product categories.');
|
||||
res.status(result.code).send(result);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`List of product categories. Count: ${result.length}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const getProductCategoryRouteHandler = async (req, res) => {
|
||||
const id = req.params.id;
|
||||
const result = await getObject({
|
||||
model: productCategoryModel,
|
||||
id,
|
||||
populate: [],
|
||||
});
|
||||
if (result?.error) {
|
||||
logger.warn(`Product category not found with supplied id.`);
|
||||
return res.status(result.code).send(result);
|
||||
}
|
||||
logger.debug(`Retrieved product category with ID: ${id}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const editProductCategoryRouteHandler = async (req, res) => {
|
||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||
|
||||
logger.trace(`Product category with ID: ${id}`);
|
||||
|
||||
const updateData = {
|
||||
updatedAt: new Date(),
|
||||
name: req.body.name,
|
||||
};
|
||||
|
||||
const result = await editObject({
|
||||
model: productCategoryModel,
|
||||
id,
|
||||
updateData,
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
logger.error('Error editing product category:', result.error);
|
||||
res.status(result.code).send(result);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Edited product category with ID: ${id}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const newProductCategoryRouteHandler = async (req, res) => {
|
||||
const newData = {
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: req.body.name,
|
||||
};
|
||||
|
||||
const result = await newObject({
|
||||
model: productCategoryModel,
|
||||
newData,
|
||||
user: req.user,
|
||||
});
|
||||
if (result.error) {
|
||||
logger.error('No product category created:', result.error);
|
||||
return res.status(result.code).send(result);
|
||||
}
|
||||
|
||||
logger.debug(`New product category with ID: ${result._id}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const deleteProductCategoryRouteHandler = async (req, res) => {
|
||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||
|
||||
logger.trace(`Product category with ID: ${id}`);
|
||||
|
||||
const result = await deleteObject({
|
||||
model: productCategoryModel,
|
||||
id,
|
||||
user: req.user,
|
||||
});
|
||||
if (result.error) {
|
||||
logger.error('No product category deleted:', result.error);
|
||||
return res.status(result.code).send(result);
|
||||
}
|
||||
|
||||
logger.debug(`Deleted product category with ID: ${result._id}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const getProductCategoryStatsRouteHandler = async (req, res) => {
|
||||
const result = await getModelStats({ model: productCategoryModel });
|
||||
if (result?.error) {
|
||||
logger.error('Error fetching product category stats:', result.error);
|
||||
return res.status(result.code).send(result);
|
||||
}
|
||||
logger.trace('Product category stats:', result);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const getProductCategoryHistoryRouteHandler = async (req, res) => {
|
||||
const from = req.query.from;
|
||||
const to = req.query.to;
|
||||
const result = await getModelHistory({ model: productCategoryModel, from, to });
|
||||
if (result?.error) {
|
||||
logger.error('Error fetching product category history:', result.error);
|
||||
return res.status(result.code).send(result);
|
||||
}
|
||||
logger.trace('Product category history:', result);
|
||||
res.send(result);
|
||||
};
|
||||
@ -35,7 +35,7 @@ export const listProductsRouteHandler = async (
|
||||
search,
|
||||
sort,
|
||||
order,
|
||||
populate: ['vendor', 'costTaxRate', 'priceTaxRate'],
|
||||
populate: ['productCategory', 'vendor', 'costTaxRate', 'priceTaxRate'],
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
@ -58,7 +58,7 @@ export const listProductsByPropertiesRouteHandler = async (
|
||||
model: productModel,
|
||||
properties,
|
||||
filter,
|
||||
populate: ['vendor'],
|
||||
populate: ['productCategory', 'vendor'],
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
@ -76,7 +76,7 @@ export const getProductRouteHandler = async (req, res) => {
|
||||
const result = await getObject({
|
||||
model: productModel,
|
||||
id,
|
||||
populate: ['vendor', 'costTaxRate', 'priceTaxRate'],
|
||||
populate: ['productCategory', 'vendor', 'costTaxRate', 'priceTaxRate'],
|
||||
});
|
||||
if (result?.error) {
|
||||
logger.warn(`Product not found with supplied id.`);
|
||||
@ -95,6 +95,7 @@ export const editProductRouteHandler = async (req, res) => {
|
||||
const updateData = {
|
||||
updatedAt: new Date(),
|
||||
name: req.body?.name,
|
||||
productCategory: req.body?.productCategory,
|
||||
tags: req.body?.tags,
|
||||
version: req.body?.version,
|
||||
vendor: req.body.vendor,
|
||||
@ -130,6 +131,7 @@ export const newProductRouteHandler = async (req, res) => {
|
||||
const newData = {
|
||||
updatedAt: new Date(),
|
||||
name: req.body?.name,
|
||||
productCategory: req.body?.productCategory,
|
||||
tags: req.body?.tags,
|
||||
version: req.body?.version,
|
||||
vendor: req.body.vendor,
|
||||
|
||||
@ -107,6 +107,7 @@ export const editUserRouteHandler = async (req, res) => {
|
||||
id,
|
||||
updateData,
|
||||
user: req.user,
|
||||
populate: ['profileImage'],
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
|
||||
@ -18,14 +18,24 @@ export const EXPORT_FILTER_BY_TYPE = {
|
||||
material: ['name', 'tags'],
|
||||
partStock: ['partSku'],
|
||||
partSku: ['part', 'vendor', 'priceTaxRate', 'costTaxRate'],
|
||||
productCategory: ['name'],
|
||||
product: ['productCategory', 'productCategory._id', 'vendor', 'priceTaxRate', 'costTaxRate'],
|
||||
productStock: ['productSku'],
|
||||
productSku: ['product', 'vendor', 'priceTaxRate', 'costTaxRate'],
|
||||
purchaseOrder: ['vendor'],
|
||||
purchaseOrder: [
|
||||
'vendor',
|
||||
'totalAmount',
|
||||
'totalAmountWithTax',
|
||||
'totalTaxAmount',
|
||||
'shippingAmount',
|
||||
'shippingAmountWithTax',
|
||||
'grandTotalAmount',
|
||||
],
|
||||
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'sku._id', 'shipment._id'],
|
||||
shipment: ['order._id', 'orderType', 'courierService._id'],
|
||||
stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
|
||||
stockLocation: ['name', 'address'],
|
||||
stockTransfer: ['state.type', 'postedAt'],
|
||||
stockTransfer: ['name', 'state.type', 'postedAt'],
|
||||
stockAudit: ['filamentStock._id', 'partStock._id'],
|
||||
documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'],
|
||||
documentTemplate: ['parent._id', 'documentSize._id'],
|
||||
|
||||
@ -40,6 +40,7 @@ const buildSearchFilter = (params) => {
|
||||
const trimSpotlightObject = (object, objectType) => {
|
||||
return {
|
||||
_id: object._id,
|
||||
_reference: object._reference || undefined,
|
||||
name: object.name || undefined,
|
||||
state: object.state && object?.state.type ? { type: object.state.type } : undefined,
|
||||
tags: object.tags || undefined,
|
||||
@ -48,6 +49,7 @@ const trimSpotlightObject = (object, objectType) => {
|
||||
updatedAt: object.updatedAt || undefined,
|
||||
objectType: objectType || undefined,
|
||||
online: object.online || undefined,
|
||||
amount: object.amount || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
504
src/utils.js
504
src/utils.js
@ -14,21 +14,309 @@ import { createEmailRenderAuthCode } from './services/misc/emailRenderAuth.js';
|
||||
import { Worker } from 'worker_threads';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { diffJson } from 'diff';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const logger = log4js.getLogger('Utils');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Filter expression parsing (Microsoft Dynamics NAV / Business Central style)
|
||||
//
|
||||
// Supported syntax (all compiled down to MongoDB query operators):
|
||||
// = equal to 377 -> { field: 377 }
|
||||
// <> not equal to <>0 -> { field: { $ne: 0 } }
|
||||
// > greater than >1200 -> { field: { $gt: 1200 } }
|
||||
// >= greater than or equal >=1200 -> { field: { $gte: 1200 } }
|
||||
// < less than <1200 -> { field: { $lt: 1200 } }
|
||||
// <= less than or equal <=1200 -> { field: { $lte: 1200 } }
|
||||
// .. interval 1100..2100 -> { field: { $gte: 1100, $lte: 2100 } }
|
||||
// open-ended intervals ..2500 / 23..
|
||||
// | either / or 1200|1300 -> { field: { $in: [1200, 1300] } }
|
||||
// & and <2000&>1000 -> { field: { $gt: 1000, $lt: 2000 } }
|
||||
// ( ) grouping / precedence 30|(>=10&<=20)
|
||||
// * any number of characters Co* / *Co / *Co*
|
||||
// ? a single character Hans?n
|
||||
// @ ignore case @location (text matching is case-insensitive)
|
||||
//
|
||||
// Date-typed fields (detected by name, e.g. *At / *Date / *Time) interpret bare
|
||||
// values as calendar days/datetimes, e.g. "22" => the whole of day 22 of the
|
||||
// current month/year, "22..23" => start of 22 through end of 23.
|
||||
//
|
||||
// Note: BC's space-delimited date shorthand is locale dependent; here numeric
|
||||
// date operands are read day-first ("D M Y H Min S") with missing components
|
||||
// filled from the current date and the interval boundary.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildWildcardRegexPattern(input) {
|
||||
// Escape all regex special chars except * (which we treat as a wildcard)
|
||||
const escaped = input.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
||||
// Convert * to "match anything"
|
||||
const withWildcards = escaped.replace(/\*/g, '.*');
|
||||
// Anchor so that, without *, this is an exact match
|
||||
// Escape all regex special chars except * and ? (which we treat as wildcards)
|
||||
const escaped = String(input).replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
||||
// * matches any run of characters, ? matches exactly one character
|
||||
const withWildcards = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
|
||||
// Anchor so that, without wildcards, this is an exact match
|
||||
return `^${withWildcards}$`;
|
||||
}
|
||||
|
||||
function looksLikeDateField(property) {
|
||||
const last = String(property).split('.').pop();
|
||||
return /[a-z](?:At|Date|Time)$/.test(last) || /^(?:date|time|datetime)$/i.test(last);
|
||||
}
|
||||
|
||||
function isObjectIdString(value) {
|
||||
return /^[a-f\d]{24}$/i.test(value);
|
||||
}
|
||||
|
||||
function isNumeric(value) {
|
||||
return value.trim() !== '' && !isNaN(value);
|
||||
}
|
||||
|
||||
function startOfDay(date) {
|
||||
const d = new Date(date);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
function endOfDay(date) {
|
||||
const d = new Date(date);
|
||||
d.setHours(23, 59, 59, 999);
|
||||
return d;
|
||||
}
|
||||
|
||||
function normalizeYear(year) {
|
||||
if (year >= 100) return year;
|
||||
return year < 70 ? 2000 + year : 1900 + year;
|
||||
}
|
||||
|
||||
// Parses a single date operand to a Date, filling missing parts from the
|
||||
// current date and the supplied interval boundary ('start' | 'end').
|
||||
function parseDateOperand(value, boundary = 'start') {
|
||||
const text = String(value).trim();
|
||||
if (!text) return null;
|
||||
const end = boundary === 'end';
|
||||
|
||||
// Values with explicit separators (ISO and similar) are parsed directly.
|
||||
if (/[-/T]/.test(text) || /\d:\d/.test(text)) {
|
||||
const parsed = new Date(text);
|
||||
if (isNaN(parsed.getTime())) return null;
|
||||
if (!/[T:]/.test(text)) {
|
||||
return end ? endOfDay(parsed) : startOfDay(parsed);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// Numeric, day-first form: "D [M] [Y] [H] [Min] [S]".
|
||||
const parts = text.split(/\s+/);
|
||||
if (!parts.every((part) => /^\d+$/.test(part))) return null;
|
||||
const nums = parts.map(Number);
|
||||
const now = new Date();
|
||||
const day = nums[0];
|
||||
const month = nums.length >= 2 ? nums[1] : now.getMonth() + 1;
|
||||
const year = nums.length >= 3 ? normalizeYear(nums[2]) : now.getFullYear();
|
||||
const hour = nums.length >= 4 ? nums[3] : end ? 23 : 0;
|
||||
const minute = nums.length >= 5 ? nums[4] : end ? 59 : 0;
|
||||
const second = nums.length >= 6 ? nums[5] : end ? 59 : 0;
|
||||
const ms = end ? 999 : 0;
|
||||
const date = new Date(year, month - 1, day, hour, minute, second, ms);
|
||||
return isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
// Coerces a literal to its strongest matching scalar type.
|
||||
function coerceScalar(value) {
|
||||
const lower = value.toLowerCase();
|
||||
if (lower === 'true') return true;
|
||||
if (lower === 'false') return false;
|
||||
if (isObjectIdString(value)) return new mongoose.Types.ObjectId(value);
|
||||
if (isNumeric(value)) return Number(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Coerces an interval/comparison boundary, preferring a date for date fields.
|
||||
function coerceBoundary(value, isDateField, boundary) {
|
||||
if (isDateField) {
|
||||
const date = parseDateOperand(value, boundary);
|
||||
if (date) return date;
|
||||
}
|
||||
return coerceScalar(value);
|
||||
}
|
||||
|
||||
// Splits on a separator while respecting parentheses depth.
|
||||
function splitTopLevel(str, separator) {
|
||||
const parts = [];
|
||||
let depth = 0;
|
||||
let current = '';
|
||||
for (const ch of str) {
|
||||
if (ch === '(') depth++;
|
||||
else if (ch === ')') depth = Math.max(0, depth - 1);
|
||||
|
||||
if (ch === separator && depth === 0) {
|
||||
parts.push(current);
|
||||
current = '';
|
||||
} else {
|
||||
current += ch;
|
||||
}
|
||||
}
|
||||
parts.push(current);
|
||||
return parts;
|
||||
}
|
||||
|
||||
// True when a single outer pair of parentheses wraps the whole token.
|
||||
function isWrappedInParens(str) {
|
||||
if (!str.startsWith('(') || !str.endsWith(')')) return false;
|
||||
let depth = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (str[i] === '(') depth++;
|
||||
else if (str[i] === ')') {
|
||||
depth--;
|
||||
if (depth === 0 && i < str.length - 1) return false;
|
||||
}
|
||||
}
|
||||
return depth === 0;
|
||||
}
|
||||
|
||||
// Parses a filter expression into an AST of or / and / leaf nodes.
|
||||
function parseExpression(str) {
|
||||
const orParts = splitTopLevel(str, '|');
|
||||
if (orParts.length > 1) {
|
||||
return { type: 'or', items: orParts.map(parseExpression) };
|
||||
}
|
||||
const andParts = splitTopLevel(str, '&');
|
||||
if (andParts.length > 1) {
|
||||
return { type: 'and', items: andParts.map(parseExpression) };
|
||||
}
|
||||
const trimmed = str.trim();
|
||||
if (isWrappedInParens(trimmed)) {
|
||||
return parseExpression(trimmed.slice(1, -1));
|
||||
}
|
||||
return { type: 'leaf', token: trimmed };
|
||||
}
|
||||
|
||||
// Strips a leading @ (ignore-case marker); text matching is already case-insensitive.
|
||||
function stripIgnoreCase(str) {
|
||||
return str.startsWith('@') ? str.slice(1) : str;
|
||||
}
|
||||
|
||||
// Builds an equality condition (value, date range, or wildcard regex).
|
||||
function buildEquality(value, isDateField) {
|
||||
if (isDateField) {
|
||||
const start = parseDateOperand(value, 'start');
|
||||
const end = parseDateOperand(value, 'end');
|
||||
if (start && end) return { op: { $gte: start, $lte: end } };
|
||||
}
|
||||
const lower = value.toLowerCase();
|
||||
if (lower === 'true') return { value: true };
|
||||
if (lower === 'false') return { value: false };
|
||||
if (isObjectIdString(value)) return { value: new mongoose.Types.ObjectId(value) };
|
||||
if (isNumeric(value)) return { value: Number(value) };
|
||||
return { op: { $regex: buildWildcardRegexPattern(value), $options: 'i' } };
|
||||
}
|
||||
|
||||
// Builds a comparison condition for a single operator.
|
||||
function buildComparison(name, value, isDateField) {
|
||||
if (name === 'eq') return buildEquality(value, isDateField);
|
||||
|
||||
if (name === 'ne') {
|
||||
if (isDateField) {
|
||||
const start = parseDateOperand(value, 'start');
|
||||
const end = parseDateOperand(value, 'end');
|
||||
if (start && end) return { op: { $not: { $gte: start, $lte: end } } };
|
||||
}
|
||||
if (/[*?]/.test(value)) {
|
||||
return { op: { $not: { $regex: buildWildcardRegexPattern(value), $options: 'i' } } };
|
||||
}
|
||||
return { op: { $ne: coerceScalar(value) } };
|
||||
}
|
||||
|
||||
// For dates, < and >= align to the start of the day, > and <= to the end.
|
||||
const boundary = name === 'gt' || name === 'lte' ? 'end' : 'start';
|
||||
return { op: { [`$${name}`]: coerceBoundary(value, isDateField, boundary) } };
|
||||
}
|
||||
|
||||
// Parses a leaf token (range, comparison, or plain value) into a condition.
|
||||
function parseLeafCondition(rawToken, isDateField) {
|
||||
const token = rawToken.trim();
|
||||
if (token === '') {
|
||||
return { op: { $regex: '^$', $options: 'i' } };
|
||||
}
|
||||
|
||||
// Interval: a..b, ..b, a..
|
||||
const rangeIdx = token.indexOf('..');
|
||||
if (rangeIdx !== -1) {
|
||||
const lo = stripIgnoreCase(token.slice(0, rangeIdx).trim());
|
||||
const hi = stripIgnoreCase(token.slice(rangeIdx + 2).trim());
|
||||
const op = {};
|
||||
if (lo !== '') op.$gte = coerceBoundary(lo, isDateField, 'start');
|
||||
if (hi !== '') op.$lte = coerceBoundary(hi, isDateField, 'end');
|
||||
// A bare ".." places no constraint on the field.
|
||||
if (Object.keys(op).length === 0) return { query: {} };
|
||||
return { op };
|
||||
}
|
||||
|
||||
// Comparison operators, longest symbols first.
|
||||
const operators = [
|
||||
['<>', 'ne'],
|
||||
['>=', 'gte'],
|
||||
['<=', 'lte'],
|
||||
['>', 'gt'],
|
||||
['<', 'lt'],
|
||||
['=', 'eq'],
|
||||
];
|
||||
for (const [symbol, name] of operators) {
|
||||
if (token.startsWith(symbol)) {
|
||||
return buildComparison(name, stripIgnoreCase(token.slice(symbol.length).trim()), isDateField);
|
||||
}
|
||||
}
|
||||
|
||||
return buildEquality(stripIgnoreCase(token), isDateField);
|
||||
}
|
||||
|
||||
// Converts a condition descriptor into a MongoDB query object for `property`.
|
||||
function conditionToQuery(cond, property) {
|
||||
if (cond.query) return cond.query;
|
||||
if (cond.op) return { [property]: cond.op };
|
||||
return { [property]: cond.value };
|
||||
}
|
||||
|
||||
function isOnlyRegex(op) {
|
||||
return Object.keys(op).every((key) => key === '$regex' || key === '$options');
|
||||
}
|
||||
|
||||
// Combines AND-ed conditions, merging operator objects on the same field where possible.
|
||||
function combineAnd(children, property) {
|
||||
if (children.some((child) => child.query)) {
|
||||
return { query: { $and: children.map((child) => conditionToQuery(child, property)) } };
|
||||
}
|
||||
const merged = {};
|
||||
for (const child of children) {
|
||||
if (child.value !== undefined) {
|
||||
merged.$eq = child.value;
|
||||
} else if (child.op) {
|
||||
Object.assign(merged, child.op);
|
||||
}
|
||||
}
|
||||
return { op: merged };
|
||||
}
|
||||
|
||||
// Combines OR-ed conditions, collapsing to $in or a single regex when possible.
|
||||
function combineOr(children, property) {
|
||||
if (children.every((child) => child.value !== undefined)) {
|
||||
return { op: { $in: children.map((child) => child.value) } };
|
||||
}
|
||||
if (children.every((child) => child.op && isOnlyRegex(child.op))) {
|
||||
const pattern = children.map((child) => child.op.$regex).join('|');
|
||||
return { op: { $regex: pattern, $options: 'i' } };
|
||||
}
|
||||
return { query: { $or: children.map((child) => conditionToQuery(child, property)) } };
|
||||
}
|
||||
|
||||
function buildCondition(node, property, isDateField) {
|
||||
if (node.type === 'leaf') {
|
||||
return parseLeafCondition(node.token, isDateField);
|
||||
}
|
||||
const children = node.items.map((item) => buildCondition(item, property, isDateField));
|
||||
return node.type === 'and' ? combineAnd(children, property) : combineOr(children, property);
|
||||
}
|
||||
|
||||
function parseFilter(property, value) {
|
||||
// Normalize state filter to state.type for schemas with state: { type }
|
||||
if (property === 'state') {
|
||||
@ -38,40 +326,22 @@ function parseFilter(property, value) {
|
||||
if (value?._id !== undefined && value?._id !== null) {
|
||||
return { [property]: { _id: new mongoose.Types.ObjectId(value._id) } };
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
var trimmed = value.trim();
|
||||
if (trimmed.charAt(3) == ':') {
|
||||
trimmed = value.split(':')[1];
|
||||
}
|
||||
|
||||
// Handle booleans
|
||||
if (trimmed.toLowerCase() === 'true') return { [property]: true };
|
||||
if (trimmed.toLowerCase() === 'false') return { [property]: false };
|
||||
|
||||
// Handle ObjectId (24-char hex)
|
||||
|
||||
if (/^[a-f\d]{24}$/i.test(trimmed) && trimmed.length >= 24) {
|
||||
return { [property]: new mongoose.Types.ObjectId(trimmed) };
|
||||
}
|
||||
|
||||
// Handle numbers
|
||||
if (!isNaN(trimmed)) {
|
||||
return { [property]: parseFloat(trimmed) };
|
||||
}
|
||||
|
||||
// Default to case-insensitive regex for non-numeric strings.
|
||||
// Supports * as a wildcard (e.g. "filament*" matches "filament stock").
|
||||
const pattern = buildWildcardRegexPattern(trimmed);
|
||||
return {
|
||||
[property]: {
|
||||
$regex: pattern,
|
||||
$options: 'i',
|
||||
},
|
||||
};
|
||||
// Non-string values (actual booleans, numbers, objects, etc.) pass through.
|
||||
if (typeof value !== 'string') {
|
||||
return { [property]: value };
|
||||
}
|
||||
|
||||
// Handle actual booleans, numbers, objects, etc.
|
||||
return { [property]: value };
|
||||
let trimmed = value.trim();
|
||||
if (trimmed.charAt(3) === ':') {
|
||||
const afterColon = value.split(':')[1];
|
||||
trimmed = afterColon != null ? afterColon.trim() : '';
|
||||
}
|
||||
|
||||
const isDateField = looksLikeDateField(property);
|
||||
const tree = parseExpression(trimmed);
|
||||
const condition = buildCondition(tree, property, isDateField);
|
||||
return conditionToQuery(condition, property);
|
||||
}
|
||||
|
||||
function convertToCamelCase(obj) {
|
||||
@ -303,6 +573,30 @@ function extractGCodeConfigBlock(fileContent, useCamelCase = true) {
|
||||
return useCamelCase ? convertToCamelCase(configObject) : configObject;
|
||||
}
|
||||
|
||||
const AUDIT_IGNORED_KEYS = ['createdAt', 'updatedAt', '_id'];
|
||||
|
||||
function isDiffableObject(value) {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) && value !== null;
|
||||
}
|
||||
|
||||
function isNumericValue(value) {
|
||||
return (
|
||||
typeof value === 'number' ||
|
||||
(value !== null && value !== undefined && !isNaN(Number(value)) && value !== '')
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeDiffValue(value) {
|
||||
return isNumericValue(value) ? Number(value) : value;
|
||||
}
|
||||
|
||||
function valuesDiffer(oldVal, newVal) {
|
||||
return diffJson(
|
||||
{ value: normalizeDiffValue(oldVal) },
|
||||
{ value: normalizeDiffValue(newVal) }
|
||||
).some((part) => part.added || part.removed);
|
||||
}
|
||||
|
||||
function getChangedValues(oldObj, newObj, old = false) {
|
||||
const changes = {};
|
||||
|
||||
@ -311,24 +605,15 @@ function getChangedValues(oldObj, newObj, old = false) {
|
||||
// Check all keys in the new object
|
||||
for (const key in combinedObj) {
|
||||
// Skip if the key is _id or timestamps
|
||||
if (key === 'createdAt' || key === 'updatedAt' || key === '_id') continue;
|
||||
if (AUDIT_IGNORED_KEYS.includes(key)) continue;
|
||||
|
||||
const oldVal = oldObj ? oldObj[key] : undefined;
|
||||
const newVal = newObj ? newObj[key] : undefined;
|
||||
|
||||
// If both values are objects (but not arrays or null), recurse
|
||||
if (
|
||||
oldVal &&
|
||||
newVal &&
|
||||
typeof oldVal === 'object' &&
|
||||
typeof newVal === 'object' &&
|
||||
!Array.isArray(oldVal) &&
|
||||
!Array.isArray(newVal) &&
|
||||
oldVal !== null &&
|
||||
newVal !== null
|
||||
) {
|
||||
if (isDiffableObject(oldVal) && isDiffableObject(newVal)) {
|
||||
if (oldVal?._id || newVal?._id) {
|
||||
if (JSON.stringify(oldVal?._id) !== JSON.stringify(newVal?._id)) {
|
||||
if (valuesDiffer(oldVal?._id, newVal?._id)) {
|
||||
changes[key] = old ? oldVal : newVal;
|
||||
}
|
||||
} else {
|
||||
@ -338,24 +623,7 @@ function getChangedValues(oldObj, newObj, old = false) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check if both values are numbers (or can be converted to numbers)
|
||||
const oldIsNumber =
|
||||
typeof oldVal === 'number' ||
|
||||
(oldVal !== null && oldVal !== undefined && !isNaN(Number(oldVal)) && oldVal !== '');
|
||||
const newIsNumber =
|
||||
typeof newVal === 'number' ||
|
||||
(newVal !== null && newVal !== undefined && !isNaN(Number(newVal)) && newVal !== '');
|
||||
|
||||
let valuesDiffer;
|
||||
if (oldIsNumber && newIsNumber) {
|
||||
// Compare numbers directly (this normalizes 7.50 to 7.5)
|
||||
valuesDiffer = Number(oldVal) !== Number(newVal);
|
||||
} else {
|
||||
// Use JSON.stringify for non-number comparisons
|
||||
valuesDiffer = JSON.stringify(oldVal) !== JSON.stringify(newVal);
|
||||
}
|
||||
|
||||
if (valuesDiffer) {
|
||||
if (valuesDiffer(oldVal, newVal)) {
|
||||
// If the old value is different from the new value, include it
|
||||
changes[key] = old ? oldVal : newVal;
|
||||
}
|
||||
@ -426,6 +694,8 @@ async function editAuditLog(oldValue, newValue, parentId, parentType, user) {
|
||||
operation: 'edit',
|
||||
});
|
||||
|
||||
console.log('auditLog', oldValue);
|
||||
|
||||
await auditLog.save();
|
||||
|
||||
await distributeNew(auditLog._id, 'auditLog');
|
||||
@ -544,29 +814,52 @@ async function distributeDelete(value, type) {
|
||||
await natsServer.publish(`${type}s.delete`, value);
|
||||
}
|
||||
|
||||
function getReferenceId(value) {
|
||||
if (value instanceof mongoose.Types.ObjectId) {
|
||||
return value;
|
||||
}
|
||||
if (value && typeof value === 'object' && value._id) {
|
||||
return getReferenceId(value._id);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function getReferenceIdString(value) {
|
||||
const id = getReferenceId(value);
|
||||
return id == null ? null : id.toString();
|
||||
}
|
||||
|
||||
async function distributeChildUpdate(oldValue, newValue, id, model) {
|
||||
const oldPopulatedObjects = populateObjects(oldValue, model) || [];
|
||||
const oldPopulatedObjectIds = oldPopulatedObjects.map((populate) => populate._id.toString());
|
||||
const oldPopulatedObjectIds = oldPopulatedObjects.map((populate) =>
|
||||
getReferenceIdString(populate._id)
|
||||
);
|
||||
const newPopulatedObjects = populateObjects(newValue, model) || [];
|
||||
const newPopulatedObjectIds = newPopulatedObjects.map((populate) => populate._id.toString());
|
||||
const newPopulatedObjectIds = newPopulatedObjects.map((populate) =>
|
||||
getReferenceIdString(populate._id)
|
||||
);
|
||||
|
||||
for (const populated of oldPopulatedObjects) {
|
||||
if (!newPopulatedObjectIds.includes(populated._id.toString())) {
|
||||
const populatedId = getReferenceIdString(populated._id);
|
||||
if (!populatedId) continue;
|
||||
if (!newPopulatedObjectIds.includes(populatedId)) {
|
||||
logger.debug(
|
||||
`Distributing child update for ${populated.ref}s.${populated._id}.events.childUpdate`
|
||||
`Distributing child update for ${populated.ref}s.${populatedId}.events.childUpdate`
|
||||
);
|
||||
await natsServer.publish(`${populated.ref}s.${populated._id}.events.childUpdate`, {
|
||||
await natsServer.publish(`${populated.ref}s.${populatedId}.events.childUpdate`, {
|
||||
type: 'childUpdate',
|
||||
data: { parentId: id, parentType: model.modelName },
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const populated of newPopulatedObjects) {
|
||||
if (!oldPopulatedObjectIds.includes(populated._id.toString())) {
|
||||
const populatedId = getReferenceIdString(populated._id);
|
||||
if (!populatedId) continue;
|
||||
if (!oldPopulatedObjectIds.includes(populatedId)) {
|
||||
logger.debug(
|
||||
`Distributing child update for ${populated.ref}s.${populated._id}.events.childUpdate`
|
||||
`Distributing child update for ${populated.ref}s.${populatedId}.events.childUpdate`
|
||||
);
|
||||
await natsServer.publish(`${populated.ref}s.${populated._id}.events.childUpdate`, {
|
||||
await natsServer.publish(`${populated.ref}s.${populatedId}.events.childUpdate`, {
|
||||
type: 'childUpdate',
|
||||
data: { parentId: id, parentType: model.modelName },
|
||||
});
|
||||
@ -577,10 +870,12 @@ async function distributeChildUpdate(oldValue, newValue, id, model) {
|
||||
async function distributeChildDelete(value, id, model) {
|
||||
const populatedObjects = populateObjects(value, model) || [];
|
||||
for (const populated of populatedObjects) {
|
||||
const populatedId = getReferenceIdString(populated._id);
|
||||
if (!populatedId) continue;
|
||||
logger.debug(
|
||||
`Distributing child delete for ${populated.ref}s.${populated._id}.events.childDelete`
|
||||
`Distributing child delete for ${populated.ref}s.${populatedId}.events.childDelete`
|
||||
);
|
||||
await natsServer.publish(`${populated.ref}s.${populated._id}.events.childDelete`, {
|
||||
await natsServer.publish(`${populated.ref}s.${populatedId}.events.childDelete`, {
|
||||
type: 'childDelete',
|
||||
data: { parentId: id, parentType: model.modelName },
|
||||
});
|
||||
@ -590,8 +885,10 @@ async function distributeChildDelete(value, id, model) {
|
||||
async function distributeChildNew(value, id, model) {
|
||||
const populatedObjects = populateObjects(value, model) || [];
|
||||
for (const populated of populatedObjects) {
|
||||
logger.debug(`Distributing child new for ${populated.ref}s.${populated._id}.events.childNew`);
|
||||
await natsServer.publish(`${populated.ref}s.${populated._id}.events.childNew`, {
|
||||
const populatedId = getReferenceIdString(populated._id);
|
||||
if (!populatedId) continue;
|
||||
logger.debug(`Distributing child new for ${populated.ref}s.${populatedId}.events.childNew`);
|
||||
await natsServer.publish(`${populated.ref}s.${populatedId}.events.childNew`, {
|
||||
type: 'childNew',
|
||||
data: { parentId: id, parentType: model.modelName },
|
||||
});
|
||||
@ -760,16 +1057,36 @@ function expandObjectIds(input) {
|
||||
return expand(input);
|
||||
}
|
||||
|
||||
// Returns a filter object based on allowed filters and req.query
|
||||
function getFilter(query, allowedFilters, parse = true) {
|
||||
let filter = {};
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (allowedFilters.includes(key)) {
|
||||
const parsedFilter = parse ? parseFilter(key, value) : { [key]: value };
|
||||
filter = { ...filter, ...parsedFilter };
|
||||
// Merges per-field filter clauses, falling back to $and when clauses use
|
||||
// logical operators or target the same field (so nothing is silently lost).
|
||||
function mergeFilterClauses(clauses) {
|
||||
const valid = clauses.filter((clause) => clause && Object.keys(clause).length > 0);
|
||||
if (valid.length === 0) return {};
|
||||
if (valid.length === 1) return valid[0];
|
||||
|
||||
const seen = new Set();
|
||||
let needsAnd = false;
|
||||
for (const clause of valid) {
|
||||
for (const key of Object.keys(clause)) {
|
||||
if (key === '$or' || key === '$and' || seen.has(key)) {
|
||||
needsAnd = true;
|
||||
}
|
||||
seen.add(key);
|
||||
}
|
||||
}
|
||||
return filter;
|
||||
|
||||
return needsAnd ? { $and: valid } : Object.assign({}, ...valid);
|
||||
}
|
||||
|
||||
// Returns a filter object based on allowed filters and req.query
|
||||
function getFilter(query, allowedFilters, parse = true) {
|
||||
const clauses = [];
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (allowedFilters.includes(key)) {
|
||||
clauses.push(parse ? parseFilter(key, value) : { [key]: value });
|
||||
}
|
||||
}
|
||||
return mergeFilterClauses(clauses);
|
||||
}
|
||||
|
||||
// Converts a properties argument (string or array) to an array of strings
|
||||
@ -869,14 +1186,15 @@ function buildDeepPopulateSpec(object, model, populated = new Set()) {
|
||||
const refModel = model.db.model(refName);
|
||||
const childPopulate = buildDeepPopulateSpec(object, refModel, populated);
|
||||
|
||||
const id = object[pathname]?._id || object[pathname];
|
||||
const values = Array.isArray(object[pathname]) ? object[pathname] : [object[pathname]];
|
||||
const ids = values.map(getReferenceId).filter(Boolean);
|
||||
|
||||
if (id == null || !id) return;
|
||||
|
||||
if (childPopulate.length > 0) {
|
||||
populateSpec.push({ path: pathname, populate: childPopulate, ref: refName, _id: id });
|
||||
} else {
|
||||
populateSpec.push({ path: pathname, ref: refName, _id: id });
|
||||
for (const id of ids) {
|
||||
if (childPopulate.length > 0) {
|
||||
populateSpec.push({ path: pathname, populate: childPopulate, ref: refName, _id: id });
|
||||
} else {
|
||||
populateSpec.push({ path: pathname, ref: refName, _id: id });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user