Compare commits
No commits in common. "7a2de8258694570e06ebedebc5e25484787e1309" and "57f057e3aa48cac8fb8be6d5e31f8485a43c6264" have entirely different histories.
7a2de82586
...
57f057e3aa
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
@ -26,7 +26,7 @@ pipeline {
|
|||||||
stage('Install Dependencies') {
|
stage('Install Dependencies') {
|
||||||
steps {
|
steps {
|
||||||
nodejs(nodeJSInstallationName: 'Node23') {
|
nodejs(nodeJSInstallationName: 'Node23') {
|
||||||
sh 'pnpm install --frozen-lockfile --production=false --ignore-scripts=false'
|
sh 'pnpm install --frozen-lockfile --production=false'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,6 @@
|
|||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"canonical-json": "^0.2.0",
|
"canonical-json": "^0.2.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"diff": "^9.0.0",
|
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
|
|||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@ -32,9 +32,6 @@ importers:
|
|||||||
cors:
|
cors:
|
||||||
specifier: ^2.8.5
|
specifier: ^2.8.5
|
||||||
version: 2.8.6
|
version: 2.8.6
|
||||||
diff:
|
|
||||||
specifier: ^9.0.0
|
|
||||||
version: 9.0.0
|
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^17.2.3
|
specifier: ^17.2.3
|
||||||
version: 17.2.3
|
version: 17.2.3
|
||||||
@ -2268,10 +2265,6 @@ packages:
|
|||||||
dezalgo@1.0.4:
|
dezalgo@1.0.4:
|
||||||
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
|
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:
|
doctrine@2.1.0:
|
||||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -7730,8 +7723,6 @@ snapshots:
|
|||||||
asap: 2.0.6
|
asap: 2.0.6
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
|
|
||||||
diff@9.0.0: {}
|
|
||||||
|
|
||||||
doctrine@2.1.0:
|
doctrine@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
esutils: 2.0.3
|
esutils: 2.0.3
|
||||||
|
|||||||
@ -1,26 +1,7 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
import { aggregateRollups, aggregateRollupsHistory } 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
|
// Define the main filamentStock schema
|
||||||
const filamentStockSchema = new Schema(
|
const filamentStockSchema = new Schema(
|
||||||
@ -38,7 +19,6 @@ const filamentStockSchema = new Schema(
|
|||||||
net: { type: Number, required: true },
|
net: { type: Number, required: true },
|
||||||
gross: { type: Number, required: true },
|
gross: { type: Number, required: true },
|
||||||
},
|
},
|
||||||
filament: { type: mongoose.Schema.Types.ObjectId, ref: 'filament', required: true },
|
|
||||||
filamentSku: { type: mongoose.Schema.Types.ObjectId, ref: 'filamentSku', required: true },
|
filamentSku: { type: mongoose.Schema.Types.ObjectId, ref: 'filamentSku', required: true },
|
||||||
stockLocation: {
|
stockLocation: {
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
@ -49,17 +29,6 @@ const filamentStockSchema = new Schema(
|
|||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
filamentStockSchema.pre('validate', async function () {
|
|
||||||
if (!this.filament && this.filamentSku) {
|
|
||||||
const sku = await mongoose
|
|
||||||
.model('filamentSku')
|
|
||||||
.findById(this.filamentSku)
|
|
||||||
.select('filament')
|
|
||||||
.lean();
|
|
||||||
if (sku?.filament) this.filament = sku.filament;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const rollupConfigs = [
|
const rollupConfigs = [
|
||||||
{
|
{
|
||||||
name: 'totalCurrentWeight',
|
name: 'totalCurrentWeight',
|
||||||
@ -89,33 +58,6 @@ filamentStockSchema.statics.history = async function (from, to) {
|
|||||||
return results;
|
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
|
// Add virtual id getter
|
||||||
filamentStockSchema.virtual('id').get(function () {
|
filamentStockSchema.virtual('id').get(function () {
|
||||||
return this._id;
|
return this._id;
|
||||||
|
|||||||
@ -1,26 +1,7 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
import { aggregateRollups, aggregateRollupsHistory } 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
|
// Define the main partStock schema
|
||||||
const partStockSchema = new Schema(
|
const partStockSchema = new Schema(
|
||||||
@ -72,21 +53,6 @@ partStockSchema.statics.history = async function (from, to) {
|
|||||||
return results;
|
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
|
// Add virtual id getter
|
||||||
partStockSchema.virtual('id').get(function () {
|
partStockSchema.virtual('id').get(function () {
|
||||||
return this._id;
|
return this._id;
|
||||||
|
|||||||
@ -1,26 +1,7 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
import { aggregateRollups, aggregateRollupsHistory } 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({
|
const partStockUsageSchema = new Schema({
|
||||||
partStock: { type: Schema.Types.ObjectId, ref: 'partStock', required: false },
|
partStock: { type: Schema.Types.ObjectId, ref: 'partStock', required: false },
|
||||||
@ -87,21 +68,6 @@ productStockSchema.statics.history = async function (from, to) {
|
|||||||
return results;
|
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
|
// Add virtual id getter
|
||||||
productStockSchema.virtual('id').get(function () {
|
productStockSchema.virtual('id').get(function () {
|
||||||
return this._id;
|
return this._id;
|
||||||
|
|||||||
@ -11,7 +11,7 @@ const stockTransferLineSchema = new Schema(
|
|||||||
},
|
},
|
||||||
fromStock: {
|
fromStock: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
refPath: 'lines.fromStockType',
|
refPath: 'fromStockType',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
quantity: { type: Number, required: true },
|
quantity: { type: Number, required: true },
|
||||||
@ -27,7 +27,7 @@ const stockTransferLineSchema = new Schema(
|
|||||||
},
|
},
|
||||||
toStock: {
|
toStock: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
refPath: 'lines.toStockType',
|
refPath: 'toStockType',
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -37,7 +37,6 @@ const stockTransferLineSchema = new Schema(
|
|||||||
const stockTransferSchema = new Schema(
|
const stockTransferSchema = new Schema(
|
||||||
{
|
{
|
||||||
_reference: { type: String, default: () => generateId()() },
|
_reference: { type: String, default: () => generateId()() },
|
||||||
name: { type: String, required: true },
|
|
||||||
state: {
|
state: {
|
||||||
type: { type: String, required: true, default: 'draft' },
|
type: { type: String, required: true, default: 'draft' },
|
||||||
progress: { type: Number, required: false },
|
progress: { type: Number, required: false },
|
||||||
|
|||||||
@ -7,7 +7,6 @@ const productSchema = new Schema(
|
|||||||
{
|
{
|
||||||
_reference: { type: String, default: () => generateId()() },
|
_reference: { type: String, default: () => generateId()() },
|
||||||
name: { type: String, required: true },
|
name: { type: String, required: true },
|
||||||
productCategory: { type: Schema.Types.ObjectId, ref: 'productCategory', required: true },
|
|
||||||
tags: [{ type: String }],
|
tags: [{ type: String }],
|
||||||
version: { type: String },
|
version: { type: String },
|
||||||
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
|
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
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,7 +7,6 @@ import { gcodeFileModel } from './production/gcodefile.schema.js';
|
|||||||
import { partModel } from './management/part.schema.js';
|
import { partModel } from './management/part.schema.js';
|
||||||
import { partSkuModel } from './management/partsku.schema.js';
|
import { partSkuModel } from './management/partsku.schema.js';
|
||||||
import { productModel } from './management/product.schema.js';
|
import { productModel } from './management/product.schema.js';
|
||||||
import { productCategoryModel } from './management/productcategory.schema.js';
|
|
||||||
import { productSkuModel } from './management/productsku.schema.js';
|
import { productSkuModel } from './management/productsku.schema.js';
|
||||||
import { vendorModel } from './management/vendor.schema.js';
|
import { vendorModel } from './management/vendor.schema.js';
|
||||||
import { materialModel } from './management/material.schema.js';
|
import { materialModel } from './management/material.schema.js';
|
||||||
@ -44,7 +43,6 @@ import { salesOrderModel } from './sales/salesorder.schema.js';
|
|||||||
import { marketplaceModel } from './sales/marketplace.schema.js';
|
import { marketplaceModel } from './sales/marketplace.schema.js';
|
||||||
import { listingModel } from './sales/listing.schema.js';
|
import { listingModel } from './sales/listing.schema.js';
|
||||||
import { listingVarientModel } from './sales/listingvarient.schema.js';
|
import { listingVarientModel } from './sales/listingvarient.schema.js';
|
||||||
import { paymentModel } from './finance/payment.schema.js';
|
|
||||||
|
|
||||||
// Map prefixes to models and id fields
|
// Map prefixes to models and id fields
|
||||||
export const models = {
|
export const models = {
|
||||||
@ -98,13 +96,6 @@ export const models = {
|
|||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
label: 'Product',
|
label: 'Product',
|
||||||
},
|
},
|
||||||
PCG: {
|
|
||||||
model: productCategoryModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'productCategory',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Product Category',
|
|
||||||
},
|
|
||||||
SKU: {
|
SKU: {
|
||||||
model: productSkuModel,
|
model: productSkuModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
@ -364,11 +355,4 @@ export const models = {
|
|||||||
label: 'Listing Varient',
|
label: 'Listing Varient',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
PAY: {
|
|
||||||
model: paymentModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'payment',
|
|
||||||
label: 'Payment',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,7 +14,6 @@ const gcodeFileSchema = new mongoose.Schema(
|
|||||||
name: { required: true, type: String },
|
name: { required: true, type: String },
|
||||||
gcodeFileName: { required: false, type: String },
|
gcodeFileName: { required: false, type: String },
|
||||||
size: { type: Number, required: false },
|
size: { type: Number, required: false },
|
||||||
filament: { type: Schema.Types.ObjectId, ref: 'filament', required: true },
|
|
||||||
filamentSku: { type: Schema.Types.ObjectId, ref: 'filamentSku', required: true },
|
filamentSku: { type: Schema.Types.ObjectId, ref: 'filamentSku', required: true },
|
||||||
parts: [partSchema],
|
parts: [partSchema],
|
||||||
file: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false },
|
file: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false },
|
||||||
@ -23,13 +22,6 @@ const gcodeFileSchema = new mongoose.Schema(
|
|||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
gcodeFileSchema.pre('validate', async function () {
|
|
||||||
if (!this.filament && this.filamentSku) {
|
|
||||||
const sku = await mongoose.model('filamentSku').findById(this.filamentSku).select('filament').lean();
|
|
||||||
if (sku?.filament) this.filament = sku.filament;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
gcodeFileSchema.index({ name: 'text', brand: 'text' });
|
gcodeFileSchema.index({ name: 'text', brand: 'text' });
|
||||||
|
|
||||||
gcodeFileSchema.virtual('id').get(function () {
|
gcodeFileSchema.virtual('id').get(function () {
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import {
|
|||||||
partRoutes,
|
partRoutes,
|
||||||
partSkuRoutes,
|
partSkuRoutes,
|
||||||
productRoutes,
|
productRoutes,
|
||||||
productCategoryRoutes,
|
|
||||||
productSkuRoutes,
|
productSkuRoutes,
|
||||||
vendorRoutes,
|
vendorRoutes,
|
||||||
materialRoutes,
|
materialRoutes,
|
||||||
@ -144,7 +143,6 @@ app.use('/filamentskus', filamentSkuRoutes);
|
|||||||
app.use('/parts', partRoutes);
|
app.use('/parts', partRoutes);
|
||||||
app.use('/partskus', partSkuRoutes);
|
app.use('/partskus', partSkuRoutes);
|
||||||
app.use('/products', productRoutes);
|
app.use('/products', productRoutes);
|
||||||
app.use('/productcategories', productCategoryRoutes);
|
|
||||||
app.use('/productskus', productSkuRoutes);
|
app.use('/productskus', productSkuRoutes);
|
||||||
app.use('/vendors', vendorRoutes);
|
app.use('/vendors', vendorRoutes);
|
||||||
app.use('/materials', materialRoutes);
|
app.use('/materials', materialRoutes);
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import spotlightRoutes from './misc/spotlight.js';
|
|||||||
import partRoutes from './management/parts.js';
|
import partRoutes from './management/parts.js';
|
||||||
import partSkuRoutes from './management/partskus.js';
|
import partSkuRoutes from './management/partskus.js';
|
||||||
import productRoutes from './management/products.js';
|
import productRoutes from './management/products.js';
|
||||||
import productCategoryRoutes from './management/productcategories.js';
|
|
||||||
import productSkuRoutes from './management/productskus.js';
|
import productSkuRoutes from './management/productskus.js';
|
||||||
import vendorRoutes from './management/vendors.js';
|
import vendorRoutes from './management/vendors.js';
|
||||||
import materialRoutes from './management/materials.js';
|
import materialRoutes from './management/materials.js';
|
||||||
@ -67,7 +66,6 @@ export {
|
|||||||
partRoutes,
|
partRoutes,
|
||||||
partSkuRoutes,
|
partSkuRoutes,
|
||||||
productRoutes,
|
productRoutes,
|
||||||
productCategoryRoutes,
|
|
||||||
productSkuRoutes,
|
productSkuRoutes,
|
||||||
vendorRoutes,
|
vendorRoutes,
|
||||||
materialRoutes,
|
materialRoutes,
|
||||||
|
|||||||
@ -19,8 +19,6 @@ import {
|
|||||||
router.get('/', isAuthenticated, (req, res) => {
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
const allowedFilters = [
|
const allowedFilters = [
|
||||||
'filament',
|
|
||||||
'filament._id',
|
|
||||||
'filamentSku',
|
'filamentSku',
|
||||||
'state',
|
'state',
|
||||||
'startingWeight',
|
'startingWeight',
|
||||||
@ -35,13 +33,7 @@ router.get('/', isAuthenticated, (req, res) => {
|
|||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
router.get('/properties', isAuthenticated, (req, res) => {
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
let properties = convertPropertiesString(req.query.properties);
|
||||||
const allowedFilters = [
|
const allowedFilters = ['filamentSku', 'state.type'];
|
||||||
'filament',
|
|
||||||
'filament._id',
|
|
||||||
'filamentSku',
|
|
||||||
'state.type',
|
|
||||||
'filamentSku._id',
|
|
||||||
];
|
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
const filter = getFilter(req.query, allowedFilters, false);
|
||||||
var masterFilter = {};
|
var masterFilter = {};
|
||||||
if (req.query.masterFilter) {
|
if (req.query.masterFilter) {
|
||||||
|
|||||||
@ -21,36 +21,14 @@ import {
|
|||||||
// list of purchase orders
|
// list of purchase orders
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
const allowedFilters = [
|
const allowedFilters = ['vendor', 'state', 'value', 'vendor._id'];
|
||||||
'vendor',
|
|
||||||
'state',
|
|
||||||
'value',
|
|
||||||
'vendor._id',
|
|
||||||
'totalAmount',
|
|
||||||
'totalAmountWithTax',
|
|
||||||
'totalTaxAmount',
|
|
||||||
'shippingAmount',
|
|
||||||
'shippingAmountWithTax',
|
|
||||||
'grandTotalAmount',
|
|
||||||
];
|
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
listPurchaseOrdersRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
listPurchaseOrdersRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
router.get('/properties', isAuthenticated, (req, res) => {
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
let properties = convertPropertiesString(req.query.properties);
|
||||||
const allowedFilters = [
|
const allowedFilters = ['vendor', 'state.type', 'value', 'vendor._id'];
|
||||||
'vendor',
|
|
||||||
'state.type',
|
|
||||||
'value',
|
|
||||||
'vendor._id',
|
|
||||||
'totalAmount',
|
|
||||||
'totalAmountWithTax',
|
|
||||||
'totalTaxAmount',
|
|
||||||
'shippingAmount',
|
|
||||||
'shippingAmountWithTax',
|
|
||||||
'grandTotalAmount',
|
|
||||||
];
|
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
const filter = getFilter(req.query, allowedFilters, false);
|
||||||
var masterFilter = {};
|
var masterFilter = {};
|
||||||
if (req.query.masterFilter) {
|
if (req.query.masterFilter) {
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
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
|
// list of products
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
const allowedFilters = ['_id', 'name', 'globalPrice', 'productCategory', 'productCategory._id'];
|
const allowedFilters = ['_id', 'name', 'globalPrice'];
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
listProductsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
listProductsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -16,14 +16,14 @@ import { convertPropertiesString, getFilter } from '../../utils.js';
|
|||||||
// list of gcodeFiles
|
// list of gcodeFiles
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
const allowedFilters = ['_id', 'name', 'filament', 'filament._id', 'filamentSku', 'filamentSku._id'];
|
const allowedFilters = ['_id', 'name', 'filament._id'];
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
listGCodeFilesRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
listGCodeFilesRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
router.get('/properties', isAuthenticated, (req, res) => {
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
let properties = convertPropertiesString(req.query.properties);
|
||||||
const allowedFilters = ['filament', 'filament._id', 'filamentSku', 'filamentSku._id'];
|
const allowedFilters = ['filament'];
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
const filter = getFilter(req.query, allowedFilters, false);
|
||||||
listGCodeFilesByPropertiesRouteHandler(req, res, properties, filter);
|
listGCodeFilesByPropertiesRouteHandler(req, res, properties, filter);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import { convertPropertiesString, getFilter } from '../../utils.js';
|
|||||||
// list of jobs
|
// list of jobs
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
const allowedFilters = ['state', 'gcodeFile._id', 'quantity'];
|
const allowedFilters = ['state'];
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
listJobsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
listJobsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -81,7 +81,11 @@ export const listPaymentsByPropertiesRouteHandler = async (
|
|||||||
|
|
||||||
export const getPaymentRouteHandler = async (req, res) => {
|
export const getPaymentRouteHandler = async (req, res) => {
|
||||||
const id = req.params.id;
|
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({
|
const result = await getObject({
|
||||||
model: paymentModel,
|
model: paymentModel,
|
||||||
id,
|
id,
|
||||||
@ -177,11 +181,8 @@ export const newPaymentRouteHandler = async (req, res) => {
|
|||||||
// Get invoice to populate vendor/client
|
// Get invoice to populate vendor/client
|
||||||
const invoice = await getObject({
|
const invoice = await getObject({
|
||||||
model: invoiceModel,
|
model: invoiceModel,
|
||||||
id: req.body.invoice?._id || req.body.invoice,
|
id: req.body.invoice,
|
||||||
populate: [
|
populate: [{ path: 'vendor' }, { path: 'client' }],
|
||||||
{ path: 'vendor', strictPopulate: false },
|
|
||||||
{ path: 'client', strictPopulate: false },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (invoice.error) {
|
if (invoice.error) {
|
||||||
@ -369,3 +370,4 @@ export const cancelPaymentRouteHandler = async (req, res) => {
|
|||||||
logger.debug(`Cancelled payment with ID: ${id}`);
|
logger.debug(`Cancelled payment with ID: ${id}`);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -36,11 +36,7 @@ export const listFilamentStocksRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: [
|
populate: [{ path: 'filamentSku' }, { path: 'stockLocation' }],
|
||||||
{ path: 'filament' },
|
|
||||||
{ path: 'filamentSku', populate: 'filament' },
|
|
||||||
{ path: 'stockLocation' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -64,7 +60,7 @@ export const listFilamentStocksByPropertiesRouteHandler = async (
|
|||||||
model: filamentStockModel,
|
model: filamentStockModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['filament', 'filamentSku', 'stockLocation'],
|
populate: ['filamentSku', 'stockLocation'],
|
||||||
masterFilter,
|
masterFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -83,11 +79,7 @@ export const getFilamentStockRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: filamentStockModel,
|
model: filamentStockModel,
|
||||||
id,
|
id,
|
||||||
populate: [
|
populate: [{ path: 'filamentSku' }, { path: 'stockLocation' }],
|
||||||
{ path: 'filament' },
|
|
||||||
{ path: 'filamentSku', populate: 'filament' },
|
|
||||||
{ path: 'stockLocation' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`Filament Stock not found with supplied id.`);
|
logger.warn(`Filament Stock not found with supplied id.`);
|
||||||
@ -155,7 +147,6 @@ export const newFilamentStockRouteHandler = async (req, res) => {
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
startingWeight: req.body.startingWeight,
|
startingWeight: req.body.startingWeight,
|
||||||
currentWeight: req.body.currentWeight,
|
currentWeight: req.body.currentWeight,
|
||||||
filament: req.body.filament,
|
|
||||||
filamentSku: req.body.filamentSku,
|
filamentSku: req.body.filamentSku,
|
||||||
state: req.body.state,
|
state: req.body.state,
|
||||||
stockLocation: req.body.stockLocation,
|
stockLocation: req.body.stockLocation,
|
||||||
|
|||||||
@ -30,37 +30,9 @@ const normalizeLineInput = (l) => ({
|
|||||||
toStockLocation: l.toStockLocation?._id ?? l.toStockLocation,
|
toStockLocation: l.toStockLocation?._id ?? l.toStockLocation,
|
||||||
});
|
});
|
||||||
|
|
||||||
const stockModelByType = {
|
async function createStockEventsForLine({ transferId, fromId, fromType, toId, toType, qty, unit }) {
|
||||||
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();
|
const ts = new Date();
|
||||||
await Promise.all([
|
await stockEventModel.insertMany([
|
||||||
createStockEvent(
|
|
||||||
{
|
{
|
||||||
value: -Math.abs(qty),
|
value: -Math.abs(qty),
|
||||||
unit,
|
unit,
|
||||||
@ -70,9 +42,6 @@ async function createStockEventsForLine({ transferId, fromId, fromType, toId, to
|
|||||||
ownerType: 'stockTransfer',
|
ownerType: 'stockTransfer',
|
||||||
timestamp: ts,
|
timestamp: ts,
|
||||||
},
|
},
|
||||||
user
|
|
||||||
),
|
|
||||||
createStockEvent(
|
|
||||||
{
|
{
|
||||||
value: Math.abs(qty),
|
value: Math.abs(qty),
|
||||||
unit,
|
unit,
|
||||||
@ -82,26 +51,10 @@ async function createStockEventsForLine({ transferId, fromId, fromType, toId, to
|
|||||||
ownerType: 'stockTransfer',
|
ownerType: 'stockTransfer',
|
||||||
timestamp: ts,
|
timestamp: ts,
|
||||||
},
|
},
|
||||||
user
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createStock(model, newData, user) {
|
async function executePostedLine(transferId, line) {
|
||||||
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 toLocId = line.toStockLocation;
|
||||||
const loc = await stockLocationModel.findById(toLocId).lean();
|
const loc = await stockLocationModel.findById(toLocId).lean();
|
||||||
if (!loc) {
|
if (!loc) {
|
||||||
@ -120,23 +73,24 @@ async function executePostedLine(transferId, line, user) {
|
|||||||
throw new Error('Filament transfer quantity exceeds available net weight');
|
throw new Error('Filament transfer quantity exceeds available net weight');
|
||||||
}
|
}
|
||||||
const tareBefore = Math.max(0, (src.currentWeight?.gross ?? 0) - (src.currentWeight?.net ?? 0));
|
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 = {
|
const destWeight = {
|
||||||
net: line.quantity,
|
net: line.quantity,
|
||||||
gross: line.quantity + tareBefore,
|
gross: line.quantity + tareBefore,
|
||||||
};
|
};
|
||||||
const dest = await createStock(
|
const dest = await filamentStockModel.create({
|
||||||
filamentStockModel,
|
|
||||||
{
|
|
||||||
state: src.state,
|
state: src.state,
|
||||||
startingWeight: destWeight,
|
startingWeight: destWeight,
|
||||||
currentWeight: destWeight,
|
currentWeight: destWeight,
|
||||||
filament: src.filament,
|
|
||||||
filamentSku: src.filamentSku,
|
filamentSku: src.filamentSku,
|
||||||
stockLocation: toLocId,
|
stockLocation: toLocId,
|
||||||
},
|
});
|
||||||
user
|
|
||||||
);
|
|
||||||
|
|
||||||
await createStockEventsForLine({
|
await createStockEventsForLine({
|
||||||
transferId,
|
transferId,
|
||||||
@ -146,37 +100,31 @@ async function executePostedLine(transferId, line, user) {
|
|||||||
toType: 'filamentStock',
|
toType: 'filamentStock',
|
||||||
qty: line.quantity,
|
qty: line.quantity,
|
||||||
unit: 'g',
|
unit: 'g',
|
||||||
user,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
recalculateStock('filamentStock', src, user),
|
|
||||||
recalculateStock('filamentStock', dest, user),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { toStockType: 'filamentStock', toStock: dest._id };
|
return { toStockType: 'filamentStock', toStock: dest._id };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.fromStockType === 'partStock') {
|
if (line.fromStockType === 'partStock') {
|
||||||
const src = await partStockModel.findById(line.fromStock);
|
const src = await partStockModel.findById(line.fromStock);
|
||||||
|
console.log(src);
|
||||||
if (!src) throw new Error('From part stock not found');
|
if (!src) throw new Error('From part stock not found');
|
||||||
const currentQuantity = src.currentQuantity;
|
const currentQuantity = src.state.type === 'new' ? src.startingQuantity : src.currentQuantity;
|
||||||
if (line.quantity > currentQuantity) {
|
if (line.quantity > currentQuantity) {
|
||||||
throw new Error('Part transfer quantity exceeds current quantity');
|
throw new Error('Part transfer quantity exceeds current quantity');
|
||||||
}
|
}
|
||||||
|
await partStockModel.findByIdAndUpdate(src._id, {
|
||||||
|
$inc: { currentQuantity: -line.quantity },
|
||||||
|
});
|
||||||
|
|
||||||
const dest = await createStock(
|
const dest = await partStockModel.create({
|
||||||
partStockModel,
|
|
||||||
{
|
|
||||||
partSku: src.partSku,
|
partSku: src.partSku,
|
||||||
currentQuantity: line.quantity,
|
currentQuantity: line.quantity,
|
||||||
state: { type: 'new' },
|
state: { type: 'new' },
|
||||||
sourceType: 'stockTransfer',
|
sourceType: 'stockTransfer',
|
||||||
source: transferId,
|
source: transferId,
|
||||||
stockLocation: toLocId,
|
stockLocation: toLocId,
|
||||||
},
|
});
|
||||||
user
|
|
||||||
);
|
|
||||||
|
|
||||||
await createStockEventsForLine({
|
await createStockEventsForLine({
|
||||||
transferId,
|
transferId,
|
||||||
@ -186,14 +134,8 @@ async function executePostedLine(transferId, line, user) {
|
|||||||
toType: 'partStock',
|
toType: 'partStock',
|
||||||
qty: line.quantity,
|
qty: line.quantity,
|
||||||
unit: 'each',
|
unit: 'each',
|
||||||
user,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
recalculateStock('partStock', src, user),
|
|
||||||
recalculateStock('partStock', dest, user),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { toStockType: 'partStock', toStock: dest._id };
|
return { toStockType: 'partStock', toStock: dest._id };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,19 +145,18 @@ async function executePostedLine(transferId, line, user) {
|
|||||||
if (line.quantity > src.currentQuantity) {
|
if (line.quantity > src.currentQuantity) {
|
||||||
throw new Error('Product transfer quantity exceeds current quantity');
|
throw new Error('Product transfer quantity exceeds current quantity');
|
||||||
}
|
}
|
||||||
|
await productStockModel.findByIdAndUpdate(src._id, {
|
||||||
|
$inc: { currentQuantity: -line.quantity },
|
||||||
|
});
|
||||||
|
|
||||||
const dest = await createStock(
|
const dest = await productStockModel.create({
|
||||||
productStockModel,
|
|
||||||
{
|
|
||||||
productSku: src.productSku,
|
productSku: src.productSku,
|
||||||
currentQuantity: line.quantity,
|
currentQuantity: line.quantity,
|
||||||
state: { type: 'posted' },
|
state: { type: 'posted' },
|
||||||
postedAt: new Date(),
|
postedAt: new Date(),
|
||||||
partStocks: [],
|
partStocks: [],
|
||||||
stockLocation: toLocId,
|
stockLocation: toLocId,
|
||||||
},
|
});
|
||||||
user
|
|
||||||
);
|
|
||||||
|
|
||||||
await createStockEventsForLine({
|
await createStockEventsForLine({
|
||||||
transferId,
|
transferId,
|
||||||
@ -225,14 +166,8 @@ async function executePostedLine(transferId, line, user) {
|
|||||||
toType: 'productStock',
|
toType: 'productStock',
|
||||||
qty: line.quantity,
|
qty: line.quantity,
|
||||||
unit: 'each',
|
unit: 'each',
|
||||||
user,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
recalculateStock('productStock', src, user),
|
|
||||||
recalculateStock('productStock', dest, user),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { toStockType: 'productStock', toStock: dest._id };
|
return { toStockType: 'productStock', toStock: dest._id };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,9 +279,6 @@ export const editStockTransferRouteHandler = async (req, res) => {
|
|||||||
const updateData = {
|
const updateData = {
|
||||||
lines: (req.body.lines || []).map((l) => normalizeLineInput(l)),
|
lines: (req.body.lines || []).map((l) => normalizeLineInput(l)),
|
||||||
};
|
};
|
||||||
if (req.body.name !== undefined) {
|
|
||||||
updateData.name = req.body.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await editObject({
|
const result = await editObject({
|
||||||
model: stockTransferModel,
|
model: stockTransferModel,
|
||||||
@ -392,7 +324,6 @@ export const editMultipleStockTransfersRouteHandler = async (req, res) => {
|
|||||||
|
|
||||||
export const newStockTransferRouteHandler = async (req, res) => {
|
export const newStockTransferRouteHandler = async (req, res) => {
|
||||||
const newData = {
|
const newData = {
|
||||||
name: req.body.name,
|
|
||||||
state: req.body.state ?? { type: 'draft' },
|
state: req.body.state ?? { type: 'draft' },
|
||||||
lines: (req.body.lines || []).map((l) => normalizeLineInput(l)),
|
lines: (req.body.lines || []).map((l) => normalizeLineInput(l)),
|
||||||
};
|
};
|
||||||
@ -472,7 +403,7 @@ export const postStockTransferRouteHandler = async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
for (const line of doc.lines) {
|
for (const line of doc.lines) {
|
||||||
const plain = line.toObject();
|
const plain = line.toObject();
|
||||||
const { toStockType, toStock } = await executePostedLine(doc._id, plain, req.user);
|
const { toStockType, toStock } = await executePostedLine(doc._id, plain);
|
||||||
updatedLines.push({
|
updatedLines.push({
|
||||||
...plain,
|
...plain,
|
||||||
toStockType,
|
toStockType,
|
||||||
@ -480,34 +411,24 @@ export const postStockTransferRouteHandler = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const postedResult = await editObject({
|
const posted = await stockTransferModel
|
||||||
model: stockTransferModel,
|
.findByIdAndUpdate(
|
||||||
id,
|
id,
|
||||||
updateData: {
|
{
|
||||||
|
$set: {
|
||||||
state: { type: 'posted' },
|
state: { type: 'posted' },
|
||||||
postedAt: new Date(),
|
postedAt: new Date(),
|
||||||
lines: updatedLines,
|
lines: updatedLines,
|
||||||
},
|
},
|
||||||
user: req.user,
|
},
|
||||||
});
|
{ new: true }
|
||||||
|
)
|
||||||
if (postedResult?.error) {
|
.populate([
|
||||||
throw new Error(postedResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const posted = await getObject({
|
|
||||||
model: stockTransferModel,
|
|
||||||
id,
|
|
||||||
populate: [
|
|
||||||
{ path: 'lines.fromStock' },
|
{ path: 'lines.fromStock' },
|
||||||
{ path: 'lines.toStockLocation' },
|
{ path: 'lines.toStockLocation' },
|
||||||
{ path: 'lines.toStock' },
|
{ path: 'lines.toStock' },
|
||||||
],
|
])
|
||||||
});
|
.lean();
|
||||||
|
|
||||||
if (posted?.error) {
|
|
||||||
throw new Error(posted.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Posted stock transfer with ID: ${id}`);
|
logger.debug(`Posted stock transfer with ID: ${id}`);
|
||||||
res.send(posted);
|
res.send(posted);
|
||||||
|
|||||||
@ -42,10 +42,7 @@ export const listAuditLogsRouteHandler = async (
|
|||||||
.sort({ [sort]: sortOrder })
|
.sort({ [sort]: sortOrder })
|
||||||
.skip(skip)
|
.skip(skip)
|
||||||
.limit(Number(limit))
|
.limit(Number(limit))
|
||||||
.populate([
|
.populate('owner', 'name _id');
|
||||||
{ path: 'owner', select: 'name _id color' },
|
|
||||||
{ path: 'parent', select: '_id name' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const auditLogs = await query;
|
const auditLogs = await query;
|
||||||
logger.trace(
|
logger.trace(
|
||||||
@ -54,7 +51,7 @@ export const listAuditLogsRouteHandler = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const expandedIdAuditLogs = auditLogs.map((auditLog) => {
|
const expandedIdAuditLogs = auditLogs.map((auditLog) => {
|
||||||
const expendedAuditLog = { ...auditLog._doc };
|
const expendedAuditLog = { ...auditLog._doc, parent: { _id: auditLog.parent } };
|
||||||
return expendedAuditLog;
|
return expendedAuditLog;
|
||||||
});
|
});
|
||||||
res.send(expandedIdAuditLogs);
|
res.send(expandedIdAuditLogs);
|
||||||
|
|||||||
@ -1,177 +0,0 @@
|
|||||||
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,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: ['productCategory', 'vendor', 'costTaxRate', 'priceTaxRate'],
|
populate: ['vendor', 'costTaxRate', 'priceTaxRate'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -58,7 +58,7 @@ export const listProductsByPropertiesRouteHandler = async (
|
|||||||
model: productModel,
|
model: productModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['productCategory', 'vendor'],
|
populate: ['vendor'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -76,7 +76,7 @@ export const getProductRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: productModel,
|
model: productModel,
|
||||||
id,
|
id,
|
||||||
populate: ['productCategory', 'vendor', 'costTaxRate', 'priceTaxRate'],
|
populate: ['vendor', 'costTaxRate', 'priceTaxRate'],
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`Product not found with supplied id.`);
|
logger.warn(`Product not found with supplied id.`);
|
||||||
@ -95,7 +95,6 @@ export const editProductRouteHandler = async (req, res) => {
|
|||||||
const updateData = {
|
const updateData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
name: req.body?.name,
|
name: req.body?.name,
|
||||||
productCategory: req.body?.productCategory,
|
|
||||||
tags: req.body?.tags,
|
tags: req.body?.tags,
|
||||||
version: req.body?.version,
|
version: req.body?.version,
|
||||||
vendor: req.body.vendor,
|
vendor: req.body.vendor,
|
||||||
@ -131,7 +130,6 @@ export const newProductRouteHandler = async (req, res) => {
|
|||||||
const newData = {
|
const newData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
name: req.body?.name,
|
name: req.body?.name,
|
||||||
productCategory: req.body?.productCategory,
|
|
||||||
tags: req.body?.tags,
|
tags: req.body?.tags,
|
||||||
version: req.body?.version,
|
version: req.body?.version,
|
||||||
vendor: req.body.vendor,
|
vendor: req.body.vendor,
|
||||||
|
|||||||
@ -107,7 +107,6 @@ export const editUserRouteHandler = async (req, res) => {
|
|||||||
id,
|
id,
|
||||||
updateData,
|
updateData,
|
||||||
user: req.user,
|
user: req.user,
|
||||||
populate: ['profileImage'],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
|
|||||||
@ -11,31 +11,20 @@ export const EXPORT_FILTER_BY_TYPE = {
|
|||||||
printer: ['host'],
|
printer: ['host'],
|
||||||
job: ['printer', 'gcodeFile'],
|
job: ['printer', 'gcodeFile'],
|
||||||
subJob: ['job'],
|
subJob: ['job'],
|
||||||
filamentStock: ['filament', 'filament._id', 'filamentSku'],
|
filamentStock: ['filamentSku'],
|
||||||
gcodeFile: ['filament', 'filament._id', 'filamentSku'],
|
|
||||||
filament: ['material', 'material._id', 'name', 'diameter', 'cost'],
|
filament: ['material', 'material._id', 'name', 'diameter', 'cost'],
|
||||||
filamentSku: ['filament', 'vendor', 'costTaxRate'],
|
filamentSku: ['filament', 'vendor', 'costTaxRate'],
|
||||||
material: ['name', 'tags'],
|
material: ['name', 'tags'],
|
||||||
partStock: ['partSku'],
|
partStock: ['partSku'],
|
||||||
partSku: ['part', 'vendor', 'priceTaxRate', 'costTaxRate'],
|
partSku: ['part', 'vendor', 'priceTaxRate', 'costTaxRate'],
|
||||||
productCategory: ['name'],
|
|
||||||
product: ['productCategory', 'productCategory._id', 'vendor', 'priceTaxRate', 'costTaxRate'],
|
|
||||||
productStock: ['productSku'],
|
productStock: ['productSku'],
|
||||||
productSku: ['product', 'vendor', 'priceTaxRate', 'costTaxRate'],
|
productSku: ['product', 'vendor', 'priceTaxRate', 'costTaxRate'],
|
||||||
purchaseOrder: [
|
purchaseOrder: ['vendor'],
|
||||||
'vendor',
|
|
||||||
'totalAmount',
|
|
||||||
'totalAmountWithTax',
|
|
||||||
'totalTaxAmount',
|
|
||||||
'shippingAmount',
|
|
||||||
'shippingAmountWithTax',
|
|
||||||
'grandTotalAmount',
|
|
||||||
],
|
|
||||||
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'sku._id', 'shipment._id'],
|
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'sku._id', 'shipment._id'],
|
||||||
shipment: ['order._id', 'orderType', 'courierService._id'],
|
shipment: ['order._id', 'orderType', 'courierService._id'],
|
||||||
stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
|
stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
|
||||||
stockLocation: ['name', 'address'],
|
stockLocation: ['name', 'address'],
|
||||||
stockTransfer: ['name', 'state.type', 'postedAt'],
|
stockTransfer: ['state.type', 'postedAt'],
|
||||||
stockAudit: ['filamentStock._id', 'partStock._id'],
|
stockAudit: ['filamentStock._id', 'partStock._id'],
|
||||||
documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'],
|
documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'],
|
||||||
documentTemplate: ['parent._id', 'documentSize._id'],
|
documentTemplate: ['parent._id', 'documentSize._id'],
|
||||||
|
|||||||
@ -40,7 +40,6 @@ const buildSearchFilter = (params) => {
|
|||||||
const trimSpotlightObject = (object, objectType) => {
|
const trimSpotlightObject = (object, objectType) => {
|
||||||
return {
|
return {
|
||||||
_id: object._id,
|
_id: object._id,
|
||||||
_reference: object._reference || undefined,
|
|
||||||
name: object.name || undefined,
|
name: object.name || undefined,
|
||||||
state: object.state && object?.state.type ? { type: object.state.type } : undefined,
|
state: object.state && object?.state.type ? { type: object.state.type } : undefined,
|
||||||
tags: object.tags || undefined,
|
tags: object.tags || undefined,
|
||||||
@ -49,7 +48,6 @@ const trimSpotlightObject = (object, objectType) => {
|
|||||||
updatedAt: object.updatedAt || undefined,
|
updatedAt: object.updatedAt || undefined,
|
||||||
objectType: objectType || undefined,
|
objectType: objectType || undefined,
|
||||||
online: object.online || undefined,
|
online: object.online || undefined,
|
||||||
amount: object.amount || undefined,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export const listGCodeFilesRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: ['filament', { path: 'filamentSku', populate: 'filament' }],
|
populate: ['filamentSku'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -58,7 +58,7 @@ export const listGCodeFilesByPropertiesRouteHandler = async (
|
|||||||
model: gcodeFileModel,
|
model: gcodeFileModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['filament', { path: 'filamentSku', populate: 'filament' }],
|
populate: 'filamentSku',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -76,7 +76,7 @@ export const getGCodeFileRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: gcodeFileModel,
|
model: gcodeFileModel,
|
||||||
id,
|
id,
|
||||||
populate: ['filament', { path: 'filamentSku', populate: 'filament' }, 'parts.partSku'],
|
populate: ['filamentSku', 'parts.partSku'],
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`GCodeFile not found with supplied id.`);
|
logger.warn(`GCodeFile not found with supplied id.`);
|
||||||
@ -113,7 +113,6 @@ export const editGCodeFileRouteHandler = async (req, res) => {
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
file: req.body.file,
|
file: req.body.file,
|
||||||
filament: req.body.filament,
|
|
||||||
filamentSku: req.body.filamentSku,
|
filamentSku: req.body.filamentSku,
|
||||||
parts: req.body.parts,
|
parts: req.body.parts,
|
||||||
};
|
};
|
||||||
@ -141,7 +140,6 @@ export const newGCodeFileRouteHandler = async (req, res) => {
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
file: req.body.file,
|
file: req.body.file,
|
||||||
filament: req.body.filament,
|
|
||||||
filamentSku: req.body.filamentSku,
|
filamentSku: req.body.filamentSku,
|
||||||
parts: req.body.parts,
|
parts: req.body.parts,
|
||||||
};
|
};
|
||||||
|
|||||||
490
src/utils.js
490
src/utils.js
@ -14,309 +14,21 @@ import { createEmailRenderAuthCode } from './services/misc/emailRenderAuth.js';
|
|||||||
import { Worker } from 'worker_threads';
|
import { Worker } from 'worker_threads';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { diffJson } from 'diff';
|
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const logger = log4js.getLogger('Utils');
|
const logger = log4js.getLogger('Utils');
|
||||||
logger.level = config.server.logLevel;
|
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) {
|
function buildWildcardRegexPattern(input) {
|
||||||
// Escape all regex special chars except * and ? (which we treat as wildcards)
|
// Escape all regex special chars except * (which we treat as a wildcard)
|
||||||
const escaped = String(input).replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
const escaped = input.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
||||||
// * matches any run of characters, ? matches exactly one character
|
// Convert * to "match anything"
|
||||||
const withWildcards = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
|
const withWildcards = escaped.replace(/\*/g, '.*');
|
||||||
// Anchor so that, without wildcards, this is an exact match
|
// Anchor so that, without *, this is an exact match
|
||||||
return `^${withWildcards}$`;
|
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) {
|
function parseFilter(property, value) {
|
||||||
// Normalize state filter to state.type for schemas with state: { type }
|
// Normalize state filter to state.type for schemas with state: { type }
|
||||||
if (property === 'state') {
|
if (property === 'state') {
|
||||||
@ -326,22 +38,40 @@ function parseFilter(property, value) {
|
|||||||
if (value?._id !== undefined && value?._id !== null) {
|
if (value?._id !== undefined && value?._id !== null) {
|
||||||
return { [property]: { _id: new mongoose.Types.ObjectId(value._id) } };
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
// Non-string values (actual booleans, numbers, objects, etc.) pass through.
|
// Handle booleans
|
||||||
if (typeof value !== 'string') {
|
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',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle actual booleans, numbers, objects, etc.
|
||||||
return { [property]: value };
|
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) {
|
function convertToCamelCase(obj) {
|
||||||
@ -573,30 +303,6 @@ function extractGCodeConfigBlock(fileContent, useCamelCase = true) {
|
|||||||
return useCamelCase ? convertToCamelCase(configObject) : configObject;
|
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) {
|
function getChangedValues(oldObj, newObj, old = false) {
|
||||||
const changes = {};
|
const changes = {};
|
||||||
|
|
||||||
@ -605,15 +311,24 @@ function getChangedValues(oldObj, newObj, old = false) {
|
|||||||
// Check all keys in the new object
|
// Check all keys in the new object
|
||||||
for (const key in combinedObj) {
|
for (const key in combinedObj) {
|
||||||
// Skip if the key is _id or timestamps
|
// Skip if the key is _id or timestamps
|
||||||
if (AUDIT_IGNORED_KEYS.includes(key)) continue;
|
if (key === 'createdAt' || key === 'updatedAt' || key === '_id') continue;
|
||||||
|
|
||||||
const oldVal = oldObj ? oldObj[key] : undefined;
|
const oldVal = oldObj ? oldObj[key] : undefined;
|
||||||
const newVal = newObj ? newObj[key] : undefined;
|
const newVal = newObj ? newObj[key] : undefined;
|
||||||
|
|
||||||
// If both values are objects (but not arrays or null), recurse
|
// If both values are objects (but not arrays or null), recurse
|
||||||
if (isDiffableObject(oldVal) && isDiffableObject(newVal)) {
|
if (
|
||||||
|
oldVal &&
|
||||||
|
newVal &&
|
||||||
|
typeof oldVal === 'object' &&
|
||||||
|
typeof newVal === 'object' &&
|
||||||
|
!Array.isArray(oldVal) &&
|
||||||
|
!Array.isArray(newVal) &&
|
||||||
|
oldVal !== null &&
|
||||||
|
newVal !== null
|
||||||
|
) {
|
||||||
if (oldVal?._id || newVal?._id) {
|
if (oldVal?._id || newVal?._id) {
|
||||||
if (valuesDiffer(oldVal?._id, newVal?._id)) {
|
if (JSON.stringify(oldVal?._id) !== JSON.stringify(newVal?._id)) {
|
||||||
changes[key] = old ? oldVal : newVal;
|
changes[key] = old ? oldVal : newVal;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -623,7 +338,24 @@ function getChangedValues(oldObj, newObj, old = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (valuesDiffer(oldVal, newVal)) {
|
// 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 the old value is different from the new value, include it
|
// If the old value is different from the new value, include it
|
||||||
changes[key] = old ? oldVal : newVal;
|
changes[key] = old ? oldVal : newVal;
|
||||||
}
|
}
|
||||||
@ -694,8 +426,6 @@ async function editAuditLog(oldValue, newValue, parentId, parentType, user) {
|
|||||||
operation: 'edit',
|
operation: 'edit',
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('auditLog', oldValue);
|
|
||||||
|
|
||||||
await auditLog.save();
|
await auditLog.save();
|
||||||
|
|
||||||
await distributeNew(auditLog._id, 'auditLog');
|
await distributeNew(auditLog._id, 'auditLog');
|
||||||
@ -814,52 +544,29 @@ async function distributeDelete(value, type) {
|
|||||||
await natsServer.publish(`${type}s.delete`, value);
|
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) {
|
async function distributeChildUpdate(oldValue, newValue, id, model) {
|
||||||
const oldPopulatedObjects = populateObjects(oldValue, model) || [];
|
const oldPopulatedObjects = populateObjects(oldValue, model) || [];
|
||||||
const oldPopulatedObjectIds = oldPopulatedObjects.map((populate) =>
|
const oldPopulatedObjectIds = oldPopulatedObjects.map((populate) => populate._id.toString());
|
||||||
getReferenceIdString(populate._id)
|
|
||||||
);
|
|
||||||
const newPopulatedObjects = populateObjects(newValue, model) || [];
|
const newPopulatedObjects = populateObjects(newValue, model) || [];
|
||||||
const newPopulatedObjectIds = newPopulatedObjects.map((populate) =>
|
const newPopulatedObjectIds = newPopulatedObjects.map((populate) => populate._id.toString());
|
||||||
getReferenceIdString(populate._id)
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const populated of oldPopulatedObjects) {
|
for (const populated of oldPopulatedObjects) {
|
||||||
const populatedId = getReferenceIdString(populated._id);
|
if (!newPopulatedObjectIds.includes(populated._id.toString())) {
|
||||||
if (!populatedId) continue;
|
|
||||||
if (!newPopulatedObjectIds.includes(populatedId)) {
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Distributing child update for ${populated.ref}s.${populatedId}.events.childUpdate`
|
`Distributing child update for ${populated.ref}s.${populated._id}.events.childUpdate`
|
||||||
);
|
);
|
||||||
await natsServer.publish(`${populated.ref}s.${populatedId}.events.childUpdate`, {
|
await natsServer.publish(`${populated.ref}s.${populated._id}.events.childUpdate`, {
|
||||||
type: 'childUpdate',
|
type: 'childUpdate',
|
||||||
data: { parentId: id, parentType: model.modelName },
|
data: { parentId: id, parentType: model.modelName },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const populated of newPopulatedObjects) {
|
for (const populated of newPopulatedObjects) {
|
||||||
const populatedId = getReferenceIdString(populated._id);
|
if (!oldPopulatedObjectIds.includes(populated._id.toString())) {
|
||||||
if (!populatedId) continue;
|
|
||||||
if (!oldPopulatedObjectIds.includes(populatedId)) {
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Distributing child update for ${populated.ref}s.${populatedId}.events.childUpdate`
|
`Distributing child update for ${populated.ref}s.${populated._id}.events.childUpdate`
|
||||||
);
|
);
|
||||||
await natsServer.publish(`${populated.ref}s.${populatedId}.events.childUpdate`, {
|
await natsServer.publish(`${populated.ref}s.${populated._id}.events.childUpdate`, {
|
||||||
type: 'childUpdate',
|
type: 'childUpdate',
|
||||||
data: { parentId: id, parentType: model.modelName },
|
data: { parentId: id, parentType: model.modelName },
|
||||||
});
|
});
|
||||||
@ -870,12 +577,10 @@ async function distributeChildUpdate(oldValue, newValue, id, model) {
|
|||||||
async function distributeChildDelete(value, id, model) {
|
async function distributeChildDelete(value, id, model) {
|
||||||
const populatedObjects = populateObjects(value, model) || [];
|
const populatedObjects = populateObjects(value, model) || [];
|
||||||
for (const populated of populatedObjects) {
|
for (const populated of populatedObjects) {
|
||||||
const populatedId = getReferenceIdString(populated._id);
|
|
||||||
if (!populatedId) continue;
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Distributing child delete for ${populated.ref}s.${populatedId}.events.childDelete`
|
`Distributing child delete for ${populated.ref}s.${populated._id}.events.childDelete`
|
||||||
);
|
);
|
||||||
await natsServer.publish(`${populated.ref}s.${populatedId}.events.childDelete`, {
|
await natsServer.publish(`${populated.ref}s.${populated._id}.events.childDelete`, {
|
||||||
type: 'childDelete',
|
type: 'childDelete',
|
||||||
data: { parentId: id, parentType: model.modelName },
|
data: { parentId: id, parentType: model.modelName },
|
||||||
});
|
});
|
||||||
@ -885,10 +590,8 @@ async function distributeChildDelete(value, id, model) {
|
|||||||
async function distributeChildNew(value, id, model) {
|
async function distributeChildNew(value, id, model) {
|
||||||
const populatedObjects = populateObjects(value, model) || [];
|
const populatedObjects = populateObjects(value, model) || [];
|
||||||
for (const populated of populatedObjects) {
|
for (const populated of populatedObjects) {
|
||||||
const populatedId = getReferenceIdString(populated._id);
|
logger.debug(`Distributing child new for ${populated.ref}s.${populated._id}.events.childNew`);
|
||||||
if (!populatedId) continue;
|
await natsServer.publish(`${populated.ref}s.${populated._id}.events.childNew`, {
|
||||||
logger.debug(`Distributing child new for ${populated.ref}s.${populatedId}.events.childNew`);
|
|
||||||
await natsServer.publish(`${populated.ref}s.${populatedId}.events.childNew`, {
|
|
||||||
type: 'childNew',
|
type: 'childNew',
|
||||||
data: { parentId: id, parentType: model.modelName },
|
data: { parentId: id, parentType: model.modelName },
|
||||||
});
|
});
|
||||||
@ -1057,36 +760,16 @@ function expandObjectIds(input) {
|
|||||||
return expand(input);
|
return expand(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 needsAnd ? { $and: valid } : Object.assign({}, ...valid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a filter object based on allowed filters and req.query
|
// Returns a filter object based on allowed filters and req.query
|
||||||
function getFilter(query, allowedFilters, parse = true) {
|
function getFilter(query, allowedFilters, parse = true) {
|
||||||
const clauses = [];
|
let filter = {};
|
||||||
for (const [key, value] of Object.entries(query)) {
|
for (const [key, value] of Object.entries(query)) {
|
||||||
if (allowedFilters.includes(key)) {
|
if (allowedFilters.includes(key)) {
|
||||||
clauses.push(parse ? parseFilter(key, value) : { [key]: value });
|
const parsedFilter = parse ? parseFilter(key, value) : { [key]: value };
|
||||||
|
filter = { ...filter, ...parsedFilter };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return mergeFilterClauses(clauses);
|
return filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts a properties argument (string or array) to an array of strings
|
// Converts a properties argument (string or array) to an array of strings
|
||||||
@ -1186,16 +869,15 @@ function buildDeepPopulateSpec(object, model, populated = new Set()) {
|
|||||||
const refModel = model.db.model(refName);
|
const refModel = model.db.model(refName);
|
||||||
const childPopulate = buildDeepPopulateSpec(object, refModel, populated);
|
const childPopulate = buildDeepPopulateSpec(object, refModel, populated);
|
||||||
|
|
||||||
const values = Array.isArray(object[pathname]) ? object[pathname] : [object[pathname]];
|
const id = object[pathname]?._id || object[pathname];
|
||||||
const ids = values.map(getReferenceId).filter(Boolean);
|
|
||||||
|
if (id == null || !id) return;
|
||||||
|
|
||||||
for (const id of ids) {
|
|
||||||
if (childPopulate.length > 0) {
|
if (childPopulate.length > 0) {
|
||||||
populateSpec.push({ path: pathname, populate: childPopulate, ref: refName, _id: id });
|
populateSpec.push({ path: pathname, populate: childPopulate, ref: refName, _id: id });
|
||||||
} else {
|
} else {
|
||||||
populateSpec.push({ path: pathname, ref: refName, _id: id });
|
populateSpec.push({ path: pathname, ref: refName, _id: id });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return populateSpec;
|
return populateSpec;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user