Implemented Product SKU.
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good

This commit is contained in:
Tom Butcher 2026-03-07 22:42:01 +00:00
parent 866c29f33f
commit 4ea168f17f
9 changed files with 282 additions and 0 deletions

View File

@ -0,0 +1,26 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const { Schema } = mongoose;
// Define the main product SKU schema
const productSkuSchema = new Schema(
{
_reference: { type: String, default: () => generateId()() },
sku: { type: String, required: true },
product: { type: Schema.Types.ObjectId, ref: 'product', required: true },
name: { type: String, required: true },
description: { type: String, required: false },
},
{ timestamps: true }
);
// Add virtual id getter
productSkuSchema.virtual('id').get(function () {
return this._id;
});
// Configure JSON serialization to include virtuals
productSkuSchema.set('toJSON', { virtuals: true });
// Create and export the model
export const productSkuModel = mongoose.model('productSku', productSkuSchema);

View File

@ -5,6 +5,7 @@ import { filamentModel } from './management/filament.schema.js';
import { gcodeFileModel } from './production/gcodefile.schema.js'; import { gcodeFileModel } from './production/gcodefile.schema.js';
import { partModel } from './management/part.schema.js'; import { partModel } from './management/part.schema.js';
import { productModel } from './management/product.schema.js'; import { productModel } from './management/product.schema.js';
import { productSkuModel } from './management/productsku.schema.js';
import { vendorModel } from './management/vendor.schema.js'; import { vendorModel } from './management/vendor.schema.js';
import { filamentStockModel } from './inventory/filamentstock.schema.js'; import { filamentStockModel } from './inventory/filamentstock.schema.js';
import { purchaseOrderModel } from './inventory/purchaseorder.schema.js'; import { purchaseOrderModel } from './inventory/purchaseorder.schema.js';
@ -73,6 +74,13 @@ export const models = {
referenceField: '_reference', referenceField: '_reference',
label: 'Product', label: 'Product',
}, },
SKU: {
model: productSkuModel,
idField: '_id',
type: 'productSku',
referenceField: '_reference',
label: 'Product SKU',
},
VEN: { VEN: {
model: vendorModel, model: vendorModel,
idField: '_id', idField: '_id',

View File

@ -17,6 +17,7 @@ import {
spotlightRoutes, spotlightRoutes,
partRoutes, partRoutes,
productRoutes, productRoutes,
productSkuRoutes,
vendorRoutes, vendorRoutes,
materialRoutes, materialRoutes,
partStockRoutes, partStockRoutes,
@ -133,6 +134,7 @@ app.use('/gcodefiles', gcodeFileRoutes);
app.use('/filaments', filamentRoutes); app.use('/filaments', filamentRoutes);
app.use('/parts', partRoutes); app.use('/parts', partRoutes);
app.use('/products', productRoutes); app.use('/products', productRoutes);
app.use('/productskus', productSkuRoutes);
app.use('/vendors', vendorRoutes); app.use('/vendors', vendorRoutes);
app.use('/materials', materialRoutes); app.use('/materials', materialRoutes);
app.use('/partstocks', partStockRoutes); app.use('/partstocks', partStockRoutes);

View File

@ -11,6 +11,7 @@ import filamentRoutes from './management/filaments.js';
import spotlightRoutes from './misc/spotlight.js'; import spotlightRoutes from './misc/spotlight.js';
import partRoutes from './management/parts.js'; import partRoutes from './management/parts.js';
import productRoutes from './management/products.js'; import productRoutes from './management/products.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';
import partStockRoutes from './inventory/partstocks.js'; import partStockRoutes from './inventory/partstocks.js';
@ -56,6 +57,7 @@ export {
spotlightRoutes, spotlightRoutes,
partRoutes, partRoutes,
productRoutes, productRoutes,
productSkuRoutes,
vendorRoutes, vendorRoutes,
materialRoutes, materialRoutes,
partStockRoutes, partStockRoutes,

View File

@ -0,0 +1,59 @@
import express from 'express';
import { isAuthenticated } from '../../keycloak.js';
import { getFilter, convertPropertiesString } from '../../utils.js';
const router = express.Router();
import {
listProductSkusRouteHandler,
getProductSkuRouteHandler,
editProductSkuRouteHandler,
newProductSkuRouteHandler,
deleteProductSkuRouteHandler,
listProductSkusByPropertiesRouteHandler,
getProductSkuStatsRouteHandler,
getProductSkuHistoryRouteHandler,
} from '../../services/management/productskus.js';
router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['_id', 'sku', 'product', 'product._id', 'name'];
const filter = getFilter(req.query, allowedFilters);
listProductSkusRouteHandler(req, res, page, limit, property, filter, search, sort, order);
});
router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = ['product', 'product._id'];
const filter = getFilter(req.query, allowedFilters, false);
let masterFilter = {};
if (req.query.masterFilter) {
masterFilter = JSON.parse(req.query.masterFilter);
}
listProductSkusByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
});
router.post('/', isAuthenticated, (req, res) => {
newProductSkuRouteHandler(req, res);
});
router.get('/stats', isAuthenticated, (req, res) => {
getProductSkuStatsRouteHandler(req, res);
});
router.get('/history', isAuthenticated, (req, res) => {
getProductSkuHistoryRouteHandler(req, res);
});
router.get('/:id', isAuthenticated, (req, res) => {
getProductSkuRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => {
editProductSkuRouteHandler(req, res);
});
router.delete('/:id', isAuthenticated, async (req, res) => {
deleteProductSkuRouteHandler(req, res);
});
export default router;

View File

@ -0,0 +1,182 @@
import config from '../../config.js';
import { productSkuModel } from '../../database/schemas/management/productsku.schema.js';
import log4js from 'log4js';
import mongoose from 'mongoose';
import {
deleteObject,
listObjects,
getObject,
editObject,
newObject,
listObjectsByProperties,
getModelStats,
getModelHistory,
} from '../../database/database.js';
const logger = log4js.getLogger('Product SKUs');
logger.level = config.server.logLevel;
export const listProductSkusRouteHandler = async (
req,
res,
page = 1,
limit = 25,
property = '',
filter = {},
search = '',
sort = '',
order = 'ascend'
) => {
const result = await listObjects({
model: productSkuModel,
page,
limit,
property,
filter,
search,
sort,
order,
populate: ['product'],
});
if (result?.error) {
logger.error('Error listing product SKUs.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of product SKUs (Page ${page}, Limit ${limit}). Count: ${result.length}.`);
res.send(result);
};
export const listProductSkusByPropertiesRouteHandler = async (
req,
res,
properties = '',
filter = {},
masterFilter = {}
) => {
const result = await listObjectsByProperties({
model: productSkuModel,
properties,
filter,
populate: ['product'],
masterFilter,
});
if (result?.error) {
logger.error('Error listing product SKUs.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of product SKUs. Count: ${result.length}`);
res.send(result);
};
export const getProductSkuRouteHandler = async (req, res) => {
const id = req.params.id;
const result = await getObject({
model: productSkuModel,
id,
populate: ['product'],
});
if (result?.error) {
logger.warn(`Product SKU not found with supplied id.`);
return res.status(result.code).send(result);
}
logger.debug(`Retrieved product SKU with ID: ${id}`);
res.send(result);
};
export const editProductSkuRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Product SKU with ID: ${id}`);
const updateData = {
updatedAt: new Date(),
sku: req.body?.sku,
product: req.body?.product,
name: req.body?.name,
description: req.body?.description,
};
const result = await editObject({
model: productSkuModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error editing product SKU:', result.error);
res.status(result.code || 500).send(result);
return;
}
logger.debug(`Edited product SKU with ID: ${id}`);
res.send(result);
};
export const newProductSkuRouteHandler = async (req, res) => {
const newData = {
sku: req.body?.sku,
product: req.body?.product,
name: req.body?.name,
description: req.body?.description,
};
const result = await newObject({
model: productSkuModel,
newData,
user: req.user,
});
if (result.error) {
logger.error('No product SKU created:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`New product SKU with ID: ${result._id}`);
res.send(result);
};
export const deleteProductSkuRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Product SKU with ID: ${id}`);
const result = await deleteObject({
model: productSkuModel,
id,
user: req.user,
});
if (result.error) {
logger.error('No product SKU deleted:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Deleted product SKU with ID: ${id}`);
res.send(result);
};
export const getProductSkuStatsRouteHandler = async (req, res) => {
const result = await getModelStats({ model: productSkuModel });
if (result?.error) {
logger.error('Error fetching product SKU stats:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Product SKU stats:', result);
res.send(result);
};
export const getProductSkuHistoryRouteHandler = async (req, res) => {
const from = req.query.from;
const to = req.query.to;
const result = await getModelHistory({ model: productSkuModel, from, to });
if (result?.error) {
logger.error('Error fetching product SKU history:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Product SKU history:', result);
res.send(result);
};

View File

@ -65,6 +65,7 @@ function getModelFilterFields(objectType) {
filamentStock: ['filament'], filamentStock: ['filament'],
partStock: ['part'], partStock: ['part'],
productStock: ['product'], productStock: ['product'],
productSku: ['product'],
purchaseOrder: ['vendor'], purchaseOrder: ['vendor'],
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'], orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'],
shipment: ['order._id', 'orderType', 'courierService._id'], shipment: ['order._id', 'orderType', 'courierService._id'],

View File

@ -71,6 +71,7 @@ function getModelFilterFields(objectType) {
filamentStock: ['filament'], filamentStock: ['filament'],
partStock: ['part'], partStock: ['part'],
productStock: ['product'], productStock: ['product'],
productSku: ['product'],
purchaseOrder: ['vendor'], purchaseOrder: ['vendor'],
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'], orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'],
shipment: ['order._id', 'orderType', 'courierService._id'], shipment: ['order._id', 'orderType', 'courierService._id'],

View File

@ -345,6 +345,7 @@ function getModelFilterFields(objectType) {
filamentStock: ['filament'], filamentStock: ['filament'],
partStock: ['part'], partStock: ['part'],
productStock: ['product'], productStock: ['product'],
productSku: ['product'],
purchaseOrder: ['vendor'], purchaseOrder: ['vendor'],
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'], orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'],
shipment: ['order._id', 'orderType', 'courierService._id'], shipment: ['order._id', 'orderType', 'courierService._id'],