Add utility functions for ObjectId handling and enhance expandObjectIds functionality
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
This commit introduces several utility functions in `utils.js` to improve ObjectId handling, including `isByte`, `isTwelveByteBuffer`, and `objectIdToString`. The `expandObjectIds` function is updated to better handle various ObjectId formats, including BSON ObjectIds and serialized 12-byte buffers. Additionally, comprehensive tests are added in `utils.expandObjectIds.test.js` to verify the correct behavior of these enhancements, ensuring robust handling of ObjectId conversions and expansions.
This commit is contained in:
parent
e2a135aa3d
commit
61ec9deecb
45
src/__tests__/utils.expandObjectIds.test.js
Normal file
45
src/__tests__/utils.expandObjectIds.test.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { describe, expect, it } from '@jest/globals';
|
||||||
|
import { mongoose } from 'mongoose';
|
||||||
|
import { ObjectId as BsonObjectId } from 'mongodb';
|
||||||
|
import { expandObjectIds } from '../utils.js';
|
||||||
|
|
||||||
|
const HEX = '692f07c4cb1de2995c4d543a';
|
||||||
|
const BYTES = [105, 47, 7, 196, 203, 29, 226, 153, 92, 77, 84, 58];
|
||||||
|
|
||||||
|
describe('expandObjectIds', () => {
|
||||||
|
it('stores mongoose ObjectIds as hex text', () => {
|
||||||
|
const id = new mongoose.Types.ObjectId(HEX);
|
||||||
|
const result = expandObjectIds({ location: id, _id: id });
|
||||||
|
|
||||||
|
expect(result.location).toEqual({ _id: HEX });
|
||||||
|
expect(result._id).toBe(HEX);
|
||||||
|
expect(result.location._id).not.toHaveProperty('buffer');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores BSON ObjectIds as hex text even when they are not mongoose instances', () => {
|
||||||
|
const id = new BsonObjectId(HEX);
|
||||||
|
expect(id instanceof mongoose.Types.ObjectId).toBe(false);
|
||||||
|
|
||||||
|
const result = expandObjectIds({ product: id });
|
||||||
|
|
||||||
|
expect(result.product).toEqual({ _id: HEX });
|
||||||
|
expect(typeof result.product._id).toBe('string');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts serialized 12-byte buffer objects to hex text', () => {
|
||||||
|
const serialized = {
|
||||||
|
buffer: Object.fromEntries(BYTES.map((byte, index) => [String(index), byte])),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = expandObjectIds({ sku: serialized });
|
||||||
|
|
||||||
|
expect(result.sku).toEqual({ _id: HEX });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts 12-byte Buffers to hex text instead of expanding index keys', () => {
|
||||||
|
const result = expandObjectIds({ sku: Buffer.from(HEX, 'hex') });
|
||||||
|
|
||||||
|
expect(result.sku).toEqual({ _id: HEX });
|
||||||
|
expect(result.sku).not.toHaveProperty('0');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -44,15 +44,41 @@ const NEIGHBORS_CACHE_TTL_SECONDS = 60;
|
|||||||
const NEIGHBORS_CACHE_WINDOW = 25;
|
const NEIGHBORS_CACHE_WINDOW = 25;
|
||||||
const NEIGHBORS_CACHE_PREFIX = 'neighbors';
|
const NEIGHBORS_CACHE_PREFIX = 'neighbors';
|
||||||
|
|
||||||
const mergeObjectUpdates = (target, source) =>
|
const isMergeableObject = (value) => _.isPlainObject(value);
|
||||||
_.mergeWith(target, source, (objValue, srcValue, key) => {
|
|
||||||
|
const mergeObjectUpdates = (target, source) => {
|
||||||
|
if (!isMergeableObject(source)) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMergeableObject(target)) {
|
||||||
|
target = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(source)) {
|
||||||
|
const srcValue = source[key];
|
||||||
|
const objValue = target[key];
|
||||||
|
|
||||||
|
// Key exists on source (including explicit undefined) — use source.
|
||||||
|
// Keys omitted from source keep the target value.
|
||||||
|
if (srcValue === undefined) {
|
||||||
|
target[key] = undefined;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(objValue) || Array.isArray(srcValue)) {
|
if (Array.isArray(objValue) || Array.isArray(srcValue)) {
|
||||||
return srcValue;
|
target[key] = srcValue;
|
||||||
|
} else if (key === 'permissions') {
|
||||||
|
target[key] = srcValue;
|
||||||
|
} else if (isMergeableObject(objValue) && isMergeableObject(srcValue)) {
|
||||||
|
mergeObjectUpdates(objValue, srcValue);
|
||||||
|
} else {
|
||||||
|
target[key] = srcValue;
|
||||||
}
|
}
|
||||||
if (key === 'permissions' && srcValue !== undefined) {
|
}
|
||||||
return srcValue;
|
|
||||||
}
|
return target;
|
||||||
});
|
};
|
||||||
|
|
||||||
export const retrieveObjectCache = async ({ model, id, populate = [] }) => {
|
export const retrieveObjectCache = async ({ model, id, populate = [] }) => {
|
||||||
if (!model || !id) return undefined;
|
if (!model || !id) return undefined;
|
||||||
@ -739,9 +765,7 @@ function flattenRefIds(ids) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchBasicObjectsByIds(refName, ids) {
|
async function fetchBasicObjectsByIds(refName, ids) {
|
||||||
const uniqueIds = [
|
const uniqueIds = [...new Map(flattenRefIds(ids).map((id) => [id.toString(), id])).values()];
|
||||||
...new Map(flattenRefIds(ids).map((id) => [id.toString(), id])).values(),
|
|
||||||
];
|
|
||||||
if (uniqueIds.length === 0) return [];
|
if (uniqueIds.length === 0) return [];
|
||||||
|
|
||||||
const withObjectType = (value) => {
|
const withObjectType = (value) => {
|
||||||
@ -846,10 +870,7 @@ export const getPropertyValues = async ({ model, property, filter = {} }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
pipeline.push(
|
pipeline.push({ $match: { [property]: { $ne: null } } }, { $group: { _id: `$${property}` } });
|
||||||
{ $match: { [property]: { $ne: null } } },
|
|
||||||
{ $group: { _id: `$${property}` } }
|
|
||||||
);
|
|
||||||
const ids = (await model.aggregate(pipeline)).map((row) => row._id);
|
const ids = (await model.aggregate(pipeline)).map((row) => row._id);
|
||||||
return fetchBasicObjectsByIds(pathInfo.ref, ids);
|
return fetchBasicObjectsByIds(pathInfo.ref, ids);
|
||||||
}
|
}
|
||||||
@ -1251,6 +1272,7 @@ export const editObject = async ({ model, id, updateData, user, populate, recalc
|
|||||||
object: updatedObject,
|
object: updatedObject,
|
||||||
populate,
|
populate,
|
||||||
});
|
});
|
||||||
|
console.log('updatedObject', updatedObject);
|
||||||
await invalidateNeighborsCacheForObject({ model, id });
|
await invalidateNeighborsCacheForObject({ model, id });
|
||||||
|
|
||||||
if (model.recalculate && recalculate == true) {
|
if (model.recalculate && recalculate == true) {
|
||||||
@ -1299,7 +1321,10 @@ export const editObjects = async ({ model, updates, user, populate, recalculate
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Reusable function to create a new object
|
// Reusable function to create a new object
|
||||||
export const newObject = async ({ model, newData, user = null }, distributeChanges = true) => {
|
export const newObject = async (
|
||||||
|
{ model, newData, user = null, recalculate = true },
|
||||||
|
distributeChanges = true
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const parentType = model.modelName ? model.modelName : 'unknown';
|
const parentType = model.modelName ? model.modelName : 'unknown';
|
||||||
|
|
||||||
@ -1324,7 +1349,7 @@ export const newObject = async ({ model, newData, user = null }, distributeChang
|
|||||||
populate: [],
|
populate: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (model.recalculate) {
|
if (model.recalculate && recalculate == true) {
|
||||||
logger.debug(`Recalculating ${model.modelName}`);
|
logger.debug(`Recalculating ${model.modelName}`);
|
||||||
await model.recalculate(created, user);
|
await model.recalculate(created, user);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ jest.unstable_mockModule('../../utils.js', () => ({
|
|||||||
generateId: jest.fn(() => () => 'test-id'),
|
generateId: jest.fn(() => () => 'test-id'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { aggregateRollups, editObject } = await import('../../database.js');
|
const { aggregateRollups, editObject, newObject, deleteObject } = await import('../../database.js');
|
||||||
const { listingModel } = await import('../sales/listing.schema.js');
|
const { listingModel } = await import('../sales/listing.schema.js');
|
||||||
const { listingVarientModel } = await import('../sales/listingvarient.schema.js');
|
const { listingVarientModel } = await import('../sales/listingvarient.schema.js');
|
||||||
const { productSkuModel } = await import('../management/productsku.schema.js');
|
const { productSkuModel } = await import('../management/productsku.schema.js');
|
||||||
@ -35,6 +35,257 @@ const productSkuId = new mongoose.Types.ObjectId();
|
|||||||
const stockLocationId = new mongoose.Types.ObjectId();
|
const stockLocationId = new mongoose.Types.ObjectId();
|
||||||
const varientId = new mongoose.Types.ObjectId();
|
const varientId = new mongoose.Types.ObjectId();
|
||||||
|
|
||||||
|
const mockFind = (docs) => ({
|
||||||
|
sort: () => ({ lean: async () => docs }),
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listing.recalculate', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
editObject.mockReset();
|
||||||
|
newObject.mockReset();
|
||||||
|
deleteObject.mockReset();
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls recalculate on each listing varient', async () => {
|
||||||
|
const recalculate = jest.spyOn(listingVarientModel, 'recalculate').mockResolvedValue();
|
||||||
|
jest.spyOn(listingVarientModel, 'find').mockReturnValue(
|
||||||
|
mockFind([{ _id: varientId, listing: listingId }])
|
||||||
|
);
|
||||||
|
|
||||||
|
const listing = { _id: listingId, stockLocation: stockLocationId };
|
||||||
|
await listingModel.recalculate(listing, 'user-1');
|
||||||
|
|
||||||
|
expect(listingVarientModel.find).toHaveBeenCalledWith({ listing: listingId });
|
||||||
|
expect(recalculate).toHaveBeenCalledWith({ _id: varientId, listing: listingId }, 'user-1');
|
||||||
|
expect(newObject).not.toHaveBeenCalled();
|
||||||
|
expect(deleteObject).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates listing varients from product skus when none exist', async () => {
|
||||||
|
const skuBId = new mongoose.Types.ObjectId();
|
||||||
|
const createdVarientId = new mongoose.Types.ObjectId();
|
||||||
|
const productSkus = [{ _id: productSkuId }, { _id: skuBId }];
|
||||||
|
const syncedVarients = [
|
||||||
|
{ _id: varientId, listing: listingId, product: productId, productSku: productSkuId },
|
||||||
|
{ _id: createdVarientId, listing: listingId, product: productId, productSku: skuBId },
|
||||||
|
];
|
||||||
|
|
||||||
|
const recalculate = jest.spyOn(listingVarientModel, 'recalculate').mockResolvedValue();
|
||||||
|
jest
|
||||||
|
.spyOn(listingVarientModel, 'find')
|
||||||
|
.mockReturnValueOnce(mockFind([]))
|
||||||
|
.mockReturnValueOnce(mockFind(syncedVarients));
|
||||||
|
jest.spyOn(productSkuModel, 'find').mockReturnValue(mockFind(productSkus));
|
||||||
|
newObject.mockResolvedValue({ _id: createdVarientId });
|
||||||
|
|
||||||
|
await listingModel.recalculate({ _id: listingId, product: productId }, 'user-1');
|
||||||
|
|
||||||
|
expect(productSkuModel.find).toHaveBeenCalledWith({ product: productId });
|
||||||
|
expect(editObject).not.toHaveBeenCalled();
|
||||||
|
expect(deleteObject).not.toHaveBeenCalled();
|
||||||
|
expect(newObject).toHaveBeenCalledTimes(2);
|
||||||
|
expect(newObject).toHaveBeenCalledWith({
|
||||||
|
model: listingVarientModel,
|
||||||
|
newData: expect.objectContaining({
|
||||||
|
listing: listingId,
|
||||||
|
product: productId,
|
||||||
|
productSku: productSkuId,
|
||||||
|
state: { type: 'draft' },
|
||||||
|
}),
|
||||||
|
user: 'user-1',
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
expect(recalculate).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates missing listing varients when one already matches a sku', async () => {
|
||||||
|
const skuBId = new mongoose.Types.ObjectId();
|
||||||
|
const skuCId = new mongoose.Types.ObjectId();
|
||||||
|
const existingVarient = {
|
||||||
|
_id: varientId,
|
||||||
|
listing: listingId,
|
||||||
|
product: productId,
|
||||||
|
productSku: productSkuId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const recalculate = jest.spyOn(listingVarientModel, 'recalculate').mockResolvedValue();
|
||||||
|
jest
|
||||||
|
.spyOn(listingVarientModel, 'find')
|
||||||
|
.mockReturnValueOnce(mockFind([existingVarient]))
|
||||||
|
.mockReturnValueOnce(
|
||||||
|
mockFind([
|
||||||
|
existingVarient,
|
||||||
|
{ _id: new mongoose.Types.ObjectId(), listing: listingId, product: productId, productSku: skuBId },
|
||||||
|
{ _id: new mongoose.Types.ObjectId(), listing: listingId, product: productId, productSku: skuCId },
|
||||||
|
])
|
||||||
|
);
|
||||||
|
jest
|
||||||
|
.spyOn(productSkuModel, 'find')
|
||||||
|
.mockReturnValue(mockFind([{ _id: productSkuId }, { _id: skuBId }, { _id: skuCId }]));
|
||||||
|
newObject.mockResolvedValue({ _id: new mongoose.Types.ObjectId() });
|
||||||
|
|
||||||
|
await listingModel.recalculate({ _id: listingId, product: productId }, 'user-1');
|
||||||
|
|
||||||
|
expect(editObject).not.toHaveBeenCalled();
|
||||||
|
expect(newObject).toHaveBeenCalledTimes(2);
|
||||||
|
expect(newObject).toHaveBeenCalledWith({
|
||||||
|
model: listingVarientModel,
|
||||||
|
newData: expect.objectContaining({
|
||||||
|
listing: listingId,
|
||||||
|
product: productId,
|
||||||
|
productSku: skuBId,
|
||||||
|
state: { type: 'draft' },
|
||||||
|
}),
|
||||||
|
user: 'user-1',
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
expect(recalculate).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rebuilds listing varients when an existing varient is missing a product sku', async () => {
|
||||||
|
const skuBId = new mongoose.Types.ObjectId();
|
||||||
|
jest.spyOn(listingVarientModel, 'recalculate').mockResolvedValue();
|
||||||
|
jest
|
||||||
|
.spyOn(listingVarientModel, 'find')
|
||||||
|
.mockReturnValueOnce(
|
||||||
|
mockFind([{ _id: varientId, listing: listingId, product: productId }])
|
||||||
|
)
|
||||||
|
.mockReturnValueOnce(
|
||||||
|
mockFind([
|
||||||
|
{ _id: varientId, listing: listingId, product: productId, productSku: productSkuId },
|
||||||
|
{
|
||||||
|
_id: new mongoose.Types.ObjectId(),
|
||||||
|
listing: listingId,
|
||||||
|
product: productId,
|
||||||
|
productSku: skuBId,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
jest
|
||||||
|
.spyOn(productSkuModel, 'find')
|
||||||
|
.mockReturnValue(mockFind([{ _id: productSkuId }, { _id: skuBId }]));
|
||||||
|
editObject.mockResolvedValue({});
|
||||||
|
newObject.mockResolvedValue({ _id: new mongoose.Types.ObjectId() });
|
||||||
|
|
||||||
|
await listingModel.recalculate({ _id: listingId, product: productId }, 'user-1');
|
||||||
|
|
||||||
|
expect(editObject).toHaveBeenCalledWith({
|
||||||
|
model: listingVarientModel,
|
||||||
|
id: varientId,
|
||||||
|
updateData: expect.objectContaining({
|
||||||
|
product: productId,
|
||||||
|
productSku: productSkuId,
|
||||||
|
}),
|
||||||
|
user: 'user-1',
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
expect(newObject).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not rebuild varients when they already match the product skus', async () => {
|
||||||
|
const existingVarient = {
|
||||||
|
_id: varientId,
|
||||||
|
listing: listingId,
|
||||||
|
product: productId,
|
||||||
|
productSku: productSkuId,
|
||||||
|
};
|
||||||
|
const recalculate = jest.spyOn(listingVarientModel, 'recalculate').mockResolvedValue();
|
||||||
|
jest.spyOn(listingVarientModel, 'find').mockReturnValue(mockFind([existingVarient]));
|
||||||
|
jest.spyOn(productSkuModel, 'find').mockReturnValue(mockFind([{ _id: productSkuId }]));
|
||||||
|
|
||||||
|
await listingModel.recalculate(
|
||||||
|
{ _id: listingId, product: productId, stockLocation: stockLocationId },
|
||||||
|
'user-1'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(newObject).not.toHaveBeenCalled();
|
||||||
|
expect(deleteObject).not.toHaveBeenCalled();
|
||||||
|
expect(editObject).not.toHaveBeenCalled();
|
||||||
|
expect(recalculate).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates and updates listing varients to match product skus when a product differs', async () => {
|
||||||
|
const otherProductId = new mongoose.Types.ObjectId();
|
||||||
|
const skuBId = new mongoose.Types.ObjectId();
|
||||||
|
const skuCId = new mongoose.Types.ObjectId();
|
||||||
|
const createdVarientId = new mongoose.Types.ObjectId();
|
||||||
|
const existingVarients = [{ _id: varientId, listing: listingId, product: otherProductId }];
|
||||||
|
const productSkus = [{ _id: productSkuId }, { _id: skuBId }, { _id: skuCId }];
|
||||||
|
const syncedVarients = [
|
||||||
|
{ _id: varientId, listing: listingId, product: productId, productSku: productSkuId },
|
||||||
|
{ _id: createdVarientId, listing: listingId, product: productId, productSku: skuBId },
|
||||||
|
{ _id: new mongoose.Types.ObjectId(), listing: listingId, product: productId, productSku: skuCId },
|
||||||
|
];
|
||||||
|
|
||||||
|
const recalculate = jest.spyOn(listingVarientModel, 'recalculate').mockResolvedValue();
|
||||||
|
jest
|
||||||
|
.spyOn(listingVarientModel, 'find')
|
||||||
|
.mockReturnValueOnce(mockFind(existingVarients))
|
||||||
|
.mockReturnValueOnce(mockFind(syncedVarients));
|
||||||
|
jest.spyOn(productSkuModel, 'find').mockReturnValue(mockFind(productSkus));
|
||||||
|
editObject.mockResolvedValue({});
|
||||||
|
newObject.mockResolvedValue({ _id: createdVarientId });
|
||||||
|
|
||||||
|
await listingModel.recalculate({ _id: listingId, product: productId }, 'user-1');
|
||||||
|
|
||||||
|
expect(editObject).toHaveBeenCalledWith({
|
||||||
|
model: listingVarientModel,
|
||||||
|
id: varientId,
|
||||||
|
updateData: expect.objectContaining({
|
||||||
|
product: productId,
|
||||||
|
productSku: productSkuId,
|
||||||
|
}),
|
||||||
|
user: 'user-1',
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
expect(newObject).toHaveBeenCalledTimes(2);
|
||||||
|
expect(newObject).toHaveBeenCalledWith({
|
||||||
|
model: listingVarientModel,
|
||||||
|
newData: expect.objectContaining({
|
||||||
|
listing: listingId,
|
||||||
|
product: productId,
|
||||||
|
productSku: skuBId,
|
||||||
|
state: { type: 'draft' },
|
||||||
|
}),
|
||||||
|
user: 'user-1',
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
expect(deleteObject).not.toHaveBeenCalled();
|
||||||
|
expect(recalculate).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes extra listing varients when the product has fewer skus', async () => {
|
||||||
|
const otherProductId = new mongoose.Types.ObjectId();
|
||||||
|
const extraVarientId = new mongoose.Types.ObjectId();
|
||||||
|
const existingVarients = [
|
||||||
|
{ _id: varientId, listing: listingId, product: otherProductId },
|
||||||
|
{ _id: extraVarientId, listing: listingId, product: otherProductId },
|
||||||
|
];
|
||||||
|
|
||||||
|
jest.spyOn(listingVarientModel, 'recalculate').mockResolvedValue();
|
||||||
|
jest
|
||||||
|
.spyOn(listingVarientModel, 'find')
|
||||||
|
.mockReturnValueOnce(mockFind(existingVarients))
|
||||||
|
.mockReturnValueOnce(
|
||||||
|
mockFind([{ _id: varientId, listing: listingId, product: productId, productSku: productSkuId }])
|
||||||
|
);
|
||||||
|
jest.spyOn(productSkuModel, 'find').mockReturnValue(mockFind([{ _id: productSkuId }]));
|
||||||
|
editObject.mockResolvedValue({});
|
||||||
|
deleteObject.mockResolvedValue({});
|
||||||
|
|
||||||
|
await listingModel.recalculate({ _id: listingId, product: productId }, 'user-1');
|
||||||
|
|
||||||
|
expect(editObject).toHaveBeenCalledTimes(1);
|
||||||
|
expect(newObject).not.toHaveBeenCalled();
|
||||||
|
expect(deleteObject).toHaveBeenCalledWith({
|
||||||
|
model: listingVarientModel,
|
||||||
|
id: extraVarientId,
|
||||||
|
user: 'user-1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('listingVarient.recalculate', () => {
|
describe('listingVarient.recalculate', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
aggregateRollups.mockReset();
|
aggregateRollups.mockReset();
|
||||||
|
|||||||
@ -1,7 +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 } from '../../database.js';
|
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
||||||
|
|
||||||
// Define the main partStock schema
|
// Define the main partStock schema
|
||||||
const partStockSchema = new Schema(
|
const partStockSchema = new Schema(
|
||||||
@ -11,6 +11,7 @@ const partStockSchema = new Schema(
|
|||||||
type: { type: String, required: true },
|
type: { type: String, required: true },
|
||||||
progress: { type: Number, required: false },
|
progress: { type: Number, required: false },
|
||||||
},
|
},
|
||||||
|
part: { type: mongoose.Schema.Types.ObjectId, ref: 'part', required: true },
|
||||||
partSku: { type: mongoose.Schema.Types.ObjectId, ref: 'partSku', required: true },
|
partSku: { type: mongoose.Schema.Types.ObjectId, ref: 'partSku', required: true },
|
||||||
stockLocation: {
|
stockLocation: {
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
@ -32,6 +33,13 @@ const partStockSchema = new Schema(
|
|||||||
|
|
||||||
partStockSchema.index({ sourceType: 'text', 'state.type': 'text' });
|
partStockSchema.index({ sourceType: 'text', 'state.type': 'text' });
|
||||||
|
|
||||||
|
partStockSchema.pre('validate', async function () {
|
||||||
|
if (!this.part && this.partSku) {
|
||||||
|
const sku = await mongoose.model('partSku').findById(this.partSku).select('part').lean();
|
||||||
|
if (sku?.part) this.part = sku.part;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const rollupConfigs = [
|
const rollupConfigs = [
|
||||||
{
|
{
|
||||||
name: 'totalCurrentQuantity',
|
name: 'totalCurrentQuantity',
|
||||||
@ -61,6 +69,22 @@ partStockSchema.statics.history = async function (from, to) {
|
|||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
partStockSchema.statics.recalculate = async function (partStock, user) {
|
||||||
|
if (!partStock?._id) return;
|
||||||
|
if (partStock.state?.type === 'draft' || partStock.state?.type === 'consumed') return;
|
||||||
|
if ((Number(partStock.currentQuantity) || 0) > 0) return;
|
||||||
|
|
||||||
|
await editObject({
|
||||||
|
model: this,
|
||||||
|
id: partStock._id,
|
||||||
|
updateData: {
|
||||||
|
state: { ...(partStock.state || {}), type: 'consumed', progress: 0 },
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
|||||||
@ -3,12 +3,25 @@ import { generateId } from '../../utils.js';
|
|||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
||||||
|
|
||||||
const partStockUsageSchema = new Schema({
|
const partStockListItemSchema = new Schema({
|
||||||
partStock: { type: Schema.Types.ObjectId, ref: 'partStock', required: false },
|
part: { type: Schema.Types.ObjectId, ref: 'part', required: true },
|
||||||
partSku: { type: Schema.Types.ObjectId, ref: 'partSku', required: true },
|
partSku: { type: Schema.Types.ObjectId, ref: 'partSku', required: true },
|
||||||
quantity: { type: Number, required: true },
|
partStocks: [{ type: Schema.Types.ObjectId, ref: 'partStock', required: false }],
|
||||||
|
requiredQuantity: { type: Number, required: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
partStockListItemSchema.virtual('remainingQuantity').get(function () {
|
||||||
|
const required = this.requiredQuantity || 0;
|
||||||
|
const stocks = Array.isArray(this.partStocks) ? this.partStocks : [];
|
||||||
|
const available = stocks.reduce(
|
||||||
|
(sum, stock) => sum + (Number(stock?.currentQuantity) || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return required - available;
|
||||||
|
});
|
||||||
|
|
||||||
|
partStockListItemSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
const toId = (value) => {
|
const toId = (value) => {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
if (typeof value === 'object' && value._id) return String(value._id);
|
if (typeof value === 'object' && value._id) return String(value._id);
|
||||||
@ -24,6 +37,7 @@ const productStockSchema = new Schema(
|
|||||||
progress: { type: Number, required: false },
|
progress: { type: Number, required: false },
|
||||||
},
|
},
|
||||||
postedAt: { type: Date, required: false },
|
postedAt: { type: Date, required: false },
|
||||||
|
product: { type: mongoose.Schema.Types.ObjectId, ref: 'product', required: true },
|
||||||
productSku: { type: mongoose.Schema.Types.ObjectId, ref: 'productSku', required: true },
|
productSku: { type: mongoose.Schema.Types.ObjectId, ref: 'productSku', required: true },
|
||||||
stockLocation: {
|
stockLocation: {
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
@ -37,13 +51,28 @@ const productStockSchema = new Schema(
|
|||||||
timestamp: { type: Date, default: Date.now },
|
timestamp: { type: Date, default: Date.now },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
partStocks: [partStockUsageSchema],
|
partStockList: [partStockListItemSchema],
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
productStockSchema.index({ 'state.type': 'text' });
|
productStockSchema.index({ 'state.type': 'text' });
|
||||||
|
|
||||||
|
productStockSchema.pre('validate', async function () {
|
||||||
|
if (!this.product && this.productSku) {
|
||||||
|
const sku = await mongoose.model('productSku').findById(this.productSku).select('product').lean();
|
||||||
|
if (sku?.product) this.product = sku.product;
|
||||||
|
}
|
||||||
|
if (this.partStockList?.length) {
|
||||||
|
for (const item of this.partStockList) {
|
||||||
|
if (!item.part && item.partSku) {
|
||||||
|
const sku = await mongoose.model('partSku').findById(item.partSku).select('part').lean();
|
||||||
|
if (sku?.part) item.part = sku.part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const rollupConfigs = [
|
const rollupConfigs = [
|
||||||
{
|
{
|
||||||
name: 'totalCurrentQuantity',
|
name: 'totalCurrentQuantity',
|
||||||
@ -56,9 +85,9 @@ const rollupConfigs = [
|
|||||||
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
|
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'posted',
|
name: 'new',
|
||||||
filter: { 'state.type': 'posted' },
|
filter: { 'state.type': 'new' },
|
||||||
rollups: [{ name: 'posted', property: 'state.type', operation: 'count' }],
|
rollups: [{ name: 'new', property: 'state.type', operation: 'count' }],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -83,13 +112,30 @@ productStockSchema.statics.history = async function (from, to) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
productStockSchema.statics.recalculate = async function (productStock, user) {
|
productStockSchema.statics.recalculate = async function (productStock, user) {
|
||||||
|
if (
|
||||||
|
productStock?._id &&
|
||||||
|
productStock.state?.type !== 'draft' &&
|
||||||
|
productStock.state?.type !== 'consumed' &&
|
||||||
|
(Number(productStock.currentQuantity) || 0) <= 0
|
||||||
|
) {
|
||||||
|
await editObject({
|
||||||
|
model: this,
|
||||||
|
id: productStock._id,
|
||||||
|
updateData: {
|
||||||
|
state: { ...(productStock.state || {}), type: 'consumed', progress: 0 },
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const productSkuId = toId(productStock?.productSku);
|
const productSkuId = toId(productStock?.productSku);
|
||||||
const stockLocationId = toId(productStock?.stockLocation);
|
const stockLocationId = toId(productStock?.stockLocation);
|
||||||
if (!productSkuId || !stockLocationId) {
|
if (!productSkuId || !stockLocationId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let productId = toId(productStock?.productSku?.product);
|
let productId = toId(productStock?.product) || toId(productStock?.productSku?.product);
|
||||||
if (!productId) {
|
if (!productId) {
|
||||||
const productSku = await mongoose.model('productSku').findById(productSkuId).select('product').lean();
|
const productSku = await mongoose.model('productSku').findById(productSkuId).select('product').lean();
|
||||||
productId = toId(productSku?.product);
|
productId = toId(productSku?.product);
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const parentStockModelNames = {
|
|||||||
const initialStockStates = {
|
const initialStockStates = {
|
||||||
filamentStock: 'unconsumed',
|
filamentStock: 'unconsumed',
|
||||||
partStock: 'new',
|
partStock: 'new',
|
||||||
productStock: 'posted',
|
productStock: 'new',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStartingAmount = (parentType, parentStock) => {
|
const getStartingAmount = (parentType, parentStock) => {
|
||||||
@ -75,7 +75,7 @@ const getStockEventTotal = async (parentId, parentType) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildParentUpdateData = (parentType, parentStock, events) => {
|
const buildParentUpdateData = (parentType, parentStock, events, stockEvent) => {
|
||||||
const updateData = {};
|
const updateData = {};
|
||||||
let currentAmount;
|
let currentAmount;
|
||||||
|
|
||||||
@ -87,8 +87,16 @@ const buildParentUpdateData = (parentType, parentStock, events) => {
|
|||||||
updateData.currentWeight = { net, gross };
|
updateData.currentWeight = { net, gross };
|
||||||
currentAmount = net;
|
currentAmount = net;
|
||||||
} else {
|
} else {
|
||||||
updateData.currentQuantity = events.total;
|
const eventValue = Number(stockEvent?.value);
|
||||||
currentAmount = events.total;
|
if (Number.isFinite(eventValue) && eventValue < 0) {
|
||||||
|
updateData.currentQuantity = Math.max(
|
||||||
|
0,
|
||||||
|
(Number(parentStock.currentQuantity) || 0) + eventValue
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
updateData.currentQuantity = events.total;
|
||||||
|
}
|
||||||
|
currentAmount = updateData.currentQuantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = buildParentState(
|
const state = buildParentState(
|
||||||
@ -164,7 +172,7 @@ const appendParentHistoryIfChanged = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const recalculateParentStock = async (parentType, parentId, user) => {
|
const recalculateParentStock = async (parentType, parentId, user, stockEvent) => {
|
||||||
if (!parentType || !parentId) return;
|
if (!parentType || !parentId) return;
|
||||||
|
|
||||||
const modelName = parentStockModelNames[parentType];
|
const modelName = parentStockModelNames[parentType];
|
||||||
@ -174,7 +182,6 @@ const recalculateParentStock = async (parentType, parentId, user) => {
|
|||||||
const parentStock = await getObject({
|
const parentStock = await getObject({
|
||||||
model: parentModel,
|
model: parentModel,
|
||||||
id: parentId,
|
id: parentId,
|
||||||
cached: true,
|
|
||||||
});
|
});
|
||||||
if (!parentStock || parentStock.error) return;
|
if (!parentStock || parentStock.error) return;
|
||||||
|
|
||||||
@ -187,10 +194,10 @@ const recalculateParentStock = async (parentType, parentId, user) => {
|
|||||||
updateData: appendParentHistoryIfChanged(
|
updateData: appendParentHistoryIfChanged(
|
||||||
parentType,
|
parentType,
|
||||||
parentStock,
|
parentStock,
|
||||||
buildParentUpdateData(parentType, parentStock, events)
|
buildParentUpdateData(parentType, parentStock, events, stockEvent)
|
||||||
),
|
),
|
||||||
user,
|
user,
|
||||||
recalculate: parentType === 'productStock',
|
recalculate: parentType === 'productStock' || parentType === 'partStock',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -217,7 +224,7 @@ const stockEventSchema = new Schema(
|
|||||||
ownerType: {
|
ownerType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
enum: ['user', 'subJob', 'stockAudit', 'stockTransfer'],
|
enum: ['user', 'subJob', 'stockAudit', 'stockTransfer', 'productStock'],
|
||||||
},
|
},
|
||||||
history: [
|
history: [
|
||||||
{
|
{
|
||||||
@ -253,7 +260,7 @@ stockEventSchema.statics.recalculate = async function (stockEvent, user) {
|
|||||||
|
|
||||||
const parentType = stockEvent.parentType;
|
const parentType = stockEvent.parentType;
|
||||||
const parentId = stockEvent.parent?._id || stockEvent.parent;
|
const parentId = stockEvent.parent?._id || stockEvent.parent;
|
||||||
await recalculateParentStock(parentType, parentId, user);
|
await recalculateParentStock(parentType, parentId, user, stockEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add virtual id getter
|
// Add virtual id getter
|
||||||
|
|||||||
@ -23,7 +23,7 @@ const productSkuSchema = new Schema(
|
|||||||
overridePrice: { type: Boolean, default: false },
|
overridePrice: { type: Boolean, default: false },
|
||||||
margin: { type: Number, required: false },
|
margin: { type: Number, required: false },
|
||||||
amount: { type: Number, required: false },
|
amount: { type: Number, required: false },
|
||||||
parts: [partSkuUsageSchema],
|
parts: { type: [partSkuUsageSchema], default: [] },
|
||||||
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
||||||
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
||||||
priceWithTax: { type: Number, required: false },
|
priceWithTax: { type: Number, required: false },
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
|
import { editObject, newObject, deleteObject } from '../../database.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
const listingSchema = new Schema(
|
const listingSchema = new Schema(
|
||||||
@ -72,4 +73,102 @@ listingSchema.set('toJSON', {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const refId = (value) => value?._id ?? value;
|
||||||
|
|
||||||
|
listingSchema.statics.recalculate = async function (listing, user) {
|
||||||
|
const listingId = refId(listing);
|
||||||
|
if (!listingId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listingVarientModel = mongoose.model('listingVarient');
|
||||||
|
const productSkuModel = mongoose.model('productSku');
|
||||||
|
const listingProductId = refId(listing.product);
|
||||||
|
|
||||||
|
const findVarients = () =>
|
||||||
|
listingVarientModel.find({ listing: listingId }).sort({ createdAt: 1 }).lean();
|
||||||
|
|
||||||
|
let varients = await findVarients();
|
||||||
|
|
||||||
|
if (listingProductId) {
|
||||||
|
const productSkus = await productSkuModel
|
||||||
|
.find({ product: listingProductId })
|
||||||
|
.sort({ createdAt: 1 })
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
const varientsBySkuId = new Map();
|
||||||
|
const unmatchedVarients = [];
|
||||||
|
for (const varient of varients) {
|
||||||
|
const skuId = refId(varient.productSku);
|
||||||
|
if (skuId && !varientsBySkuId.has(String(skuId))) {
|
||||||
|
varientsBySkuId.set(String(skuId), varient);
|
||||||
|
} else {
|
||||||
|
unmatchedVarients.push(varient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sku of productSkus) {
|
||||||
|
const skuId = sku._id;
|
||||||
|
const varientUpdateData = {
|
||||||
|
product: listingProductId,
|
||||||
|
productSku: skuId,
|
||||||
|
};
|
||||||
|
const existingVarient = varientsBySkuId.get(String(skuId)) || unmatchedVarients.shift();
|
||||||
|
|
||||||
|
if (existingVarient) {
|
||||||
|
varientsBySkuId.delete(String(skuId));
|
||||||
|
const existingProductId = refId(existingVarient.product);
|
||||||
|
const existingSkuId = refId(existingVarient.productSku);
|
||||||
|
if (
|
||||||
|
String(existingProductId) === String(listingProductId) &&
|
||||||
|
String(existingSkuId) === String(skuId)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const varientResult = await editObject({
|
||||||
|
model: listingVarientModel,
|
||||||
|
id: existingVarient._id,
|
||||||
|
updateData: varientUpdateData,
|
||||||
|
user,
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
if (varientResult.error) {
|
||||||
|
throw varientResult;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const varientResult = await newObject({
|
||||||
|
model: listingVarientModel,
|
||||||
|
newData: {
|
||||||
|
...varientUpdateData,
|
||||||
|
listing: listingId,
|
||||||
|
state: { type: listing.state?.type || 'draft' },
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
if (varientResult.error) {
|
||||||
|
throw varientResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const extra of [...varientsBySkuId.values(), ...unmatchedVarients]) {
|
||||||
|
const deleteResult = await deleteObject({
|
||||||
|
model: listingVarientModel,
|
||||||
|
id: extra._id,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
if (deleteResult.error) {
|
||||||
|
throw deleteResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
varients = await findVarients();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const varient of varients) {
|
||||||
|
await listingVarientModel.recalculate(varient, user);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const listingModel = mongoose.model('listing', listingSchema);
|
export const listingModel = mongoose.model('listing', listingSchema);
|
||||||
|
|||||||
@ -35,7 +35,14 @@ const listingVarientSchema = new Schema(
|
|||||||
);
|
);
|
||||||
|
|
||||||
listingVarientSchema.index({ currency: 'text', 'state.type': 'text' });
|
listingVarientSchema.index({ currency: 'text', 'state.type': 'text' });
|
||||||
listingVarientSchema.index({ listing: 1, externalReference: 1 }, { unique: true, sparse: true });
|
listingVarientSchema.index(
|
||||||
|
{ listing: 1, externalReference: 1 },
|
||||||
|
{
|
||||||
|
unique: true,
|
||||||
|
name: 'listing_1_externalReference_1',
|
||||||
|
partialFilterExpression: { externalReference: { $type: 'string', $gt: '' } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
listingVarientSchema.virtual('id').get(function () {
|
listingVarientSchema.virtual('id').get(function () {
|
||||||
return this._id;
|
return this._id;
|
||||||
@ -121,3 +128,22 @@ listingVarientSchema.statics.recalculate = async function (listingVarient, user)
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const listingVarientModel = mongoose.model('listingVarient', listingVarientSchema);
|
export const listingVarientModel = mongoose.model('listingVarient', listingVarientSchema);
|
||||||
|
|
||||||
|
async function replaceSparseExternalReferenceIndex() {
|
||||||
|
try {
|
||||||
|
const indexes = await listingVarientModel.collection.indexes();
|
||||||
|
const current = indexes.find((idx) => idx.name === 'listing_1_externalReference_1');
|
||||||
|
if (current && (current.sparse || !current.partialFilterExpression)) {
|
||||||
|
await listingVarientModel.collection.dropIndex('listing_1_externalReference_1');
|
||||||
|
}
|
||||||
|
await listingVarientModel.createIndexes();
|
||||||
|
} catch {
|
||||||
|
// Collection/index may not exist until Mongo is connected.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mongoose.connection.readyState === 1) {
|
||||||
|
replaceSparseExternalReferenceIndex();
|
||||||
|
} else {
|
||||||
|
mongoose.connection.once('open', replaceSparseExternalReferenceIndex);
|
||||||
|
}
|
||||||
|
|||||||
@ -19,6 +19,13 @@ import {
|
|||||||
const logger = log4js.getLogger('Part Stocks');
|
const logger = log4js.getLogger('Part Stocks');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
const PART_STOCK_POPULATE = [
|
||||||
|
{ path: 'part' },
|
||||||
|
{ path: 'partSku', populate: 'part' },
|
||||||
|
{ path: 'stockLocation' },
|
||||||
|
{ path: 'source' },
|
||||||
|
];
|
||||||
|
|
||||||
export const listPartStocksRouteHandler = async (
|
export const listPartStocksRouteHandler = async (
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
@ -39,7 +46,7 @@ export const listPartStocksRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: [{ path: 'partSku' }, { path: 'stockLocation' }, { path: 'source' }],
|
populate: PART_STOCK_POPULATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -63,7 +70,7 @@ export const listPartStocksByPropertiesRouteHandler = async (
|
|||||||
model: partStockModel,
|
model: partStockModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['partSku', 'stockLocation', 'source'],
|
populate: PART_STOCK_POPULATE,
|
||||||
masterFilter,
|
masterFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -98,7 +105,7 @@ export const getPartStockRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: partStockModel,
|
model: partStockModel,
|
||||||
id,
|
id,
|
||||||
populate: [{ path: 'partSku' }, { path: 'stockLocation' }, { path: 'source' }],
|
populate: PART_STOCK_POPULATE,
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`Part Stock not found with supplied id.`);
|
logger.warn(`Part Stock not found with supplied id.`);
|
||||||
@ -120,7 +127,7 @@ export const editPartStockRouteHandler = async (req, res) => {
|
|||||||
id,
|
id,
|
||||||
updateData,
|
updateData,
|
||||||
user: req.user,
|
user: req.user,
|
||||||
populate: [{ path: 'partSku' }, { path: 'stockLocation' }, { path: 'source' }],
|
populate: PART_STOCK_POPULATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
@ -165,6 +172,7 @@ export const newPartStockRouteHandler = async (req, res) => {
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
startingQuantity: req.body.startingQuantity,
|
startingQuantity: req.body.startingQuantity,
|
||||||
currentQuantity: req.body.currentQuantity,
|
currentQuantity: req.body.currentQuantity,
|
||||||
|
part: req.body.part,
|
||||||
partSku: req.body.partSku,
|
partSku: req.body.partSku,
|
||||||
state: req.body.state,
|
state: req.body.state,
|
||||||
sourceType: req.body.sourceType,
|
sourceType: req.body.sourceType,
|
||||||
|
|||||||
@ -18,9 +18,74 @@ import {
|
|||||||
getObjectNeighbors,
|
getObjectNeighbors,
|
||||||
} from '../../database/database.js';
|
} from '../../database/database.js';
|
||||||
import { productSkuModel } from '../../database/schemas/management/productsku.schema.js';
|
import { productSkuModel } from '../../database/schemas/management/productsku.schema.js';
|
||||||
|
import { stockEventModel } from '../../database/schemas/inventory/stockevent.schema.js';
|
||||||
const logger = log4js.getLogger('Product Stocks');
|
const logger = log4js.getLogger('Product Stocks');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
const toStockId = (value) => {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (typeof value === 'object' && value._id != null) return value._id;
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPartStockAvailable = (partStock, remainingById) => {
|
||||||
|
const id = String(toStockId(partStock) || '');
|
||||||
|
if (id && remainingById.has(id)) return remainingById.get(id);
|
||||||
|
return Math.max(0, Number(partStock?.currentQuantity) || 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const consumePartStocksForItem = (item, remainingById) => {
|
||||||
|
let remaining = Number(item?.requiredQuantity) || 0;
|
||||||
|
const partStocks = Array.isArray(item?.partStocks) ? item.partStocks : [];
|
||||||
|
const deductions = [];
|
||||||
|
|
||||||
|
for (const partStock of partStocks) {
|
||||||
|
if (remaining <= 0) break;
|
||||||
|
const partStockId = toStockId(partStock);
|
||||||
|
if (partStockId == null) continue;
|
||||||
|
|
||||||
|
const available = getPartStockAvailable(partStock, remainingById);
|
||||||
|
if (available <= 0) continue;
|
||||||
|
|
||||||
|
const subtracted = Math.min(available, remaining);
|
||||||
|
remaining -= subtracted;
|
||||||
|
remainingById.set(String(partStockId), available - subtracted);
|
||||||
|
deductions.push({ partStockId, subtracted });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { remaining, deductions };
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPartStockConsumptionEvent = async ({
|
||||||
|
partStockId,
|
||||||
|
subtracted,
|
||||||
|
productStockId,
|
||||||
|
user,
|
||||||
|
}) => {
|
||||||
|
return newObject({
|
||||||
|
model: stockEventModel,
|
||||||
|
newData: {
|
||||||
|
value: -Math.abs(subtracted),
|
||||||
|
unit: 'qty',
|
||||||
|
parent: partStockId,
|
||||||
|
parentType: 'partStock',
|
||||||
|
owner: productStockId,
|
||||||
|
ownerType: 'productStock',
|
||||||
|
timestamp: new Date(),
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const PRODUCT_STOCK_POPULATE = [
|
||||||
|
'product',
|
||||||
|
{ path: 'partStockList.part' },
|
||||||
|
{ path: 'partStockList.partSku', populate: 'part' },
|
||||||
|
{ path: 'partStockList.partStocks' },
|
||||||
|
{ path: 'productSku', populate: ['parts.part', 'parts.partSku', 'product'] },
|
||||||
|
{ path: 'stockLocation' },
|
||||||
|
];
|
||||||
|
|
||||||
export const listProductStocksRouteHandler = async (
|
export const listProductStocksRouteHandler = async (
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
@ -41,11 +106,7 @@ export const listProductStocksRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: [
|
populate: PRODUCT_STOCK_POPULATE,
|
||||||
{ path: 'productSku' },
|
|
||||||
{ path: 'partStocks.partStock' },
|
|
||||||
{ path: 'stockLocation' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -69,7 +130,7 @@ export const listProductStocksByPropertiesRouteHandler = async (
|
|||||||
model: productStockModel,
|
model: productStockModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['productSku', 'partStocks.partStock', 'stockLocation'],
|
populate: PRODUCT_STOCK_POPULATE,
|
||||||
masterFilter,
|
masterFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -104,12 +165,7 @@ export const getProductStockRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: productStockModel,
|
model: productStockModel,
|
||||||
id,
|
id,
|
||||||
populate: [
|
populate: PRODUCT_STOCK_POPULATE,
|
||||||
{ path: 'partStocks.partSku' },
|
|
||||||
{ path: 'partStocks.partStock' },
|
|
||||||
{ path: 'productSku' },
|
|
||||||
{ path: 'stockLocation' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`Product Stock not found with supplied id.`);
|
logger.warn(`Product Stock not found with supplied id.`);
|
||||||
@ -139,12 +195,11 @@ export const editProductStockRouteHandler = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
|
product: req.body?.product,
|
||||||
|
productSku: req.body?.productSku,
|
||||||
stockLocation: req.body?.stockLocation,
|
stockLocation: req.body?.stockLocation,
|
||||||
partStocks: req.body?.partStocks?.map((partStock) => ({
|
currentQuantity: req.body?.currentQuantity,
|
||||||
quantity: partStock.quantity,
|
partStockList: req.body?.partStockList,
|
||||||
partStock: partStock.partStock,
|
|
||||||
partSku: partStock.partSku,
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await editObject({
|
const result = await editObject({
|
||||||
@ -152,12 +207,7 @@ export const editProductStockRouteHandler = async (req, res) => {
|
|||||||
id,
|
id,
|
||||||
updateData,
|
updateData,
|
||||||
user: req.user,
|
user: req.user,
|
||||||
populate: [
|
populate: PRODUCT_STOCK_POPULATE,
|
||||||
{ path: 'partStocks.partSku' },
|
|
||||||
{ path: 'partStocks.partStock' },
|
|
||||||
{ path: 'productSku' },
|
|
||||||
{ path: 'stockLocation' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
@ -203,16 +253,19 @@ export const newProductStockRouteHandler = async (req, res) => {
|
|||||||
model: productSkuModel,
|
model: productSkuModel,
|
||||||
id: productSkuId,
|
id: productSkuId,
|
||||||
});
|
});
|
||||||
|
const currentQuantity = Number(req.body.currentQuantity) || 0;
|
||||||
const newData = {
|
const newData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
currentQuantity: req.body.currentQuantity,
|
currentQuantity,
|
||||||
|
product: req.body.product,
|
||||||
productSku: req.body.productSku,
|
productSku: req.body.productSku,
|
||||||
state: req.body.state ?? { type: 'draft' },
|
state: req.body.state ?? { type: 'draft' },
|
||||||
stockLocation: req.body.stockLocation,
|
stockLocation: req.body.stockLocation,
|
||||||
partStocks: (productSku.parts || []).map((part) => ({
|
partStockList: (productSku.parts || []).map((part) => ({
|
||||||
|
part: part.part,
|
||||||
partSku: part.partSku,
|
partSku: part.partSku,
|
||||||
quantity: part.quantity,
|
requiredQuantity: (Number(part.quantity) || 0) * currentQuantity,
|
||||||
partStock: undefined,
|
partStocks: [],
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
const result = await newObject({
|
const result = await newObject({
|
||||||
@ -305,9 +358,72 @@ export const postProductStockRouteHandler = async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const productStock = await getObject({
|
||||||
|
model: productStockModel,
|
||||||
|
id,
|
||||||
|
populate: PRODUCT_STOCK_POPULATE,
|
||||||
|
});
|
||||||
|
if (productStock?.error) {
|
||||||
|
logger.error('Error loading product stock to post:', productStock.error);
|
||||||
|
res.status(productStock.code || 500).send(productStock);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const partStockList = Array.isArray(productStock?.partStockList)
|
||||||
|
? productStock.partStockList
|
||||||
|
: [];
|
||||||
|
const remainingById = new Map();
|
||||||
|
const deductions = [];
|
||||||
|
|
||||||
|
for (const item of partStockList) {
|
||||||
|
const consumed = consumePartStocksForItem(item, remainingById);
|
||||||
|
if (consumed.remaining > 0) {
|
||||||
|
logger.error('Insufficient part stock to post product stock.');
|
||||||
|
res.status(400).send({
|
||||||
|
error: 'Insufficient part stock to post product stock.',
|
||||||
|
code: 400,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deductions.push(...consumed.deductions);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const deduction of deductions) {
|
||||||
|
const stockEventResult = await createPartStockConsumptionEvent({
|
||||||
|
partStockId: deduction.partStockId,
|
||||||
|
subtracted: deduction.subtracted,
|
||||||
|
productStockId: id,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
if (stockEventResult?.error) {
|
||||||
|
logger.error('Error creating stock event:', stockEventResult.error);
|
||||||
|
res.status(stockEventResult.code || 500).send(stockEventResult);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialStockEventResult = await newObject({
|
||||||
|
model: stockEventModel,
|
||||||
|
newData: {
|
||||||
|
value: productStock.currentQuantity,
|
||||||
|
unit: 'qty',
|
||||||
|
parent: { _id: id },
|
||||||
|
parentType: 'productStock',
|
||||||
|
owner: { _id: req.user._id },
|
||||||
|
ownerType: 'user',
|
||||||
|
},
|
||||||
|
recalculate: true,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
if (initialStockEventResult?.error) {
|
||||||
|
logger.error('Error creating initial stock event:', initialStockEventResult.error);
|
||||||
|
res.status(initialStockEventResult.code || 500).send(initialStockEventResult);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
state: { type: 'posted' },
|
state: { type: 'new' },
|
||||||
postedAt: new Date(),
|
postedAt: new Date(),
|
||||||
};
|
};
|
||||||
const result = await editObject({
|
const result = await editObject({
|
||||||
@ -315,6 +431,7 @@ export const postProductStockRouteHandler = async (req, res) => {
|
|||||||
id,
|
id,
|
||||||
updateData,
|
updateData,
|
||||||
user: req.user,
|
user: req.user,
|
||||||
|
populate: PRODUCT_STOCK_POPULATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
@ -326,6 +443,7 @@ export const postProductStockRouteHandler = async (req, res) => {
|
|||||||
logger.debug(`Posted product stock with ID: ${id}`);
|
logger.debug(`Posted product stock with ID: ${id}`);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProductStockNeighborsRouteHandler = async (
|
export const getProductStockNeighborsRouteHandler = async (
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
@ -357,4 +475,3 @@ export const getProductStockNeighborsRouteHandler = async (
|
|||||||
logger.debug(`Retrieved productStock neighbors for ID: ${id}`);
|
logger.debug(`Retrieved productStock neighbors for ID: ${id}`);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -153,6 +153,7 @@ async function executePostedLine(transferId, line, user) {
|
|||||||
const dest = await createStock(
|
const dest = await createStock(
|
||||||
partStockModel,
|
partStockModel,
|
||||||
{
|
{
|
||||||
|
part: src.part,
|
||||||
partSku: src.partSku,
|
partSku: src.partSku,
|
||||||
currentQuantity: line.quantity,
|
currentQuantity: line.quantity,
|
||||||
state: { type: 'new' },
|
state: { type: 'new' },
|
||||||
@ -187,11 +188,12 @@ async function executePostedLine(transferId, line, user) {
|
|||||||
const dest = await createStock(
|
const dest = await createStock(
|
||||||
productStockModel,
|
productStockModel,
|
||||||
{
|
{
|
||||||
|
product: src.product,
|
||||||
productSku: src.productSku,
|
productSku: src.productSku,
|
||||||
currentQuantity: line.quantity,
|
currentQuantity: line.quantity,
|
||||||
state: { type: 'posted' },
|
state: { type: 'new' },
|
||||||
postedAt: new Date(),
|
postedAt: new Date(),
|
||||||
partStocks: [],
|
partStockList: [],
|
||||||
stockLocation: toLocId,
|
stockLocation: toLocId,
|
||||||
},
|
},
|
||||||
user
|
user
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export const EXPORT_FILTER_BY_TYPE = {
|
|||||||
filament: ['material', 'material._id', 'vendor', 'name', 'diameter', 'cost'],
|
filament: ['material', 'material._id', 'vendor', 'name', 'diameter', 'cost'],
|
||||||
filamentSku: ['filament', 'filament.vendor', 'costTaxRate'],
|
filamentSku: ['filament', 'filament.vendor', 'costTaxRate'],
|
||||||
material: ['name', 'tags'],
|
material: ['name', 'tags'],
|
||||||
partStock: ['partSku'],
|
partStock: ['part', 'partSku'],
|
||||||
partSku: ['part', 'vendor', 'priceTaxRate', 'costTaxRate'],
|
partSku: ['part', 'vendor', 'priceTaxRate', 'costTaxRate'],
|
||||||
productCategory: ['name'],
|
productCategory: ['name'],
|
||||||
product: ['productCategory', 'productCategory._id', 'vendor', 'priceTaxRate', 'costTaxRate'],
|
product: ['productCategory', 'productCategory._id', 'vendor', 'priceTaxRate', 'costTaxRate'],
|
||||||
|
|||||||
@ -179,9 +179,20 @@ export const editListingRouteHandler = async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const marketplaceId = result.marketplace?._id || result.marketplace;
|
const checkStatesResult = await checkStates({ model: listingModel, id, states: ['draft'] });
|
||||||
if (marketplaceId) {
|
|
||||||
pushToMarketplace(marketplaceId, { _id: id }, req.user, { isNew: false });
|
if (checkStatesResult.error) {
|
||||||
|
logger.error('Error checking listing states:', checkStatesResult.error);
|
||||||
|
res.status(checkStatesResult.code).send(checkStatesResult);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkStatesResult == false) {
|
||||||
|
const marketplaceId = result.marketplace?._id || result.marketplace;
|
||||||
|
if (marketplaceId) {
|
||||||
|
pushToMarketplace(marketplaceId, { _id: id }, req.user, { isNew: false });
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Edited listing with ID: ${id}`);
|
logger.debug(`Edited listing with ID: ${id}`);
|
||||||
@ -212,21 +223,6 @@ export const newListingRouteHandler = async (req, res) => {
|
|||||||
return res.status(result.code).send(result);
|
return res.status(result.code).send(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
await newObject({
|
|
||||||
model: listingVarientModel,
|
|
||||||
newData: {
|
|
||||||
listing: result._id,
|
|
||||||
state: { type: 'draft' },
|
|
||||||
product: req.body.product || undefined,
|
|
||||||
},
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
logger.debug(`Created default listing varient for listing ${result._id}`);
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn(`Failed to create default listing varient: ${err.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newMarketplaceId = result.marketplace?._id || result.marketplace;
|
const newMarketplaceId = result.marketplace?._id || result.marketplace;
|
||||||
if (newMarketplaceId) {
|
if (newMarketplaceId) {
|
||||||
pushToMarketplace(newMarketplaceId, { _id: result._id }, req.user, { isNew: true });
|
pushToMarketplace(newMarketplaceId, { _id: result._id }, req.user, { isNew: true });
|
||||||
|
|||||||
71
src/utils.js
71
src/utils.js
@ -1524,31 +1524,84 @@ function flatternObjectIds(object) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isByte(value) {
|
||||||
|
const num = Number(value);
|
||||||
|
return Number.isInteger(num) && num >= 0 && num <= 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTwelveByteBuffer(val) {
|
||||||
|
if (!val) return false;
|
||||||
|
if (Buffer.isBuffer(val) || val instanceof Uint8Array) {
|
||||||
|
return val.length === 12;
|
||||||
|
}
|
||||||
|
if (typeof val !== 'object' || Array.isArray(val)) return false;
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
if (!isByte(val[i])) return false;
|
||||||
|
}
|
||||||
|
const indexKeys = Object.keys(val).filter((key) => /^\d+$/.test(key));
|
||||||
|
return indexKeys.length === 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectIdToString(val) {
|
||||||
|
if (val == null) return val;
|
||||||
|
if (typeof val === 'string') return val;
|
||||||
|
if (typeof val.toHexString === 'function') return val.toHexString();
|
||||||
|
if (val instanceof mongoose.Types.ObjectId) return val.toString();
|
||||||
|
if (isTwelveByteBuffer(val)) {
|
||||||
|
const bytes =
|
||||||
|
Buffer.isBuffer(val) || val instanceof Uint8Array
|
||||||
|
? val
|
||||||
|
: Array.from({ length: 12 }, (_, i) => Number(val[i]));
|
||||||
|
return Buffer.from(bytes).toString('hex');
|
||||||
|
}
|
||||||
|
if (val && typeof val === 'object' && isTwelveByteBuffer(val.buffer || val.id)) {
|
||||||
|
return objectIdToString(val.buffer || val.id);
|
||||||
|
}
|
||||||
|
return String(val);
|
||||||
|
}
|
||||||
|
|
||||||
function expandObjectIds(input) {
|
function expandObjectIds(input) {
|
||||||
const excludedFields = ['createdAt', 'updatedAt', 'name', '_id'];
|
const excludedFields = ['createdAt', 'updatedAt', 'name', '_id'];
|
||||||
// Helper to check if a value is an ObjectId or a 24-char hex string
|
|
||||||
function isObjectId(val) {
|
function isObjectId(val) {
|
||||||
// Check for MongoDB ObjectId instance
|
if (val == null || typeof val === 'boolean' || typeof val === 'number') return false;
|
||||||
|
if (val instanceof Date) return false;
|
||||||
if (val instanceof mongoose.Types.ObjectId) return true;
|
if (val instanceof mongoose.Types.ObjectId) return true;
|
||||||
// Check for exactly 24 hex characters (no special characters)
|
|
||||||
if (typeof val === 'string' && /^[a-fA-F\d]{24}$/.test(val)) return true;
|
if (typeof val === 'string' && /^[a-fA-F\d]{24}$/.test(val)) return true;
|
||||||
|
if (typeof val === 'object' && (val._bsontype === 'ObjectId' || val._bsontype === 'ObjectID')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof val.toHexString === 'function' && isTwelveByteBuffer(val.id || val.buffer)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isTwelveByteBuffer(val)) return true;
|
||||||
|
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
||||||
|
const keys = Object.keys(val);
|
||||||
|
if (
|
||||||
|
keys.length > 0 &&
|
||||||
|
keys.every((key) => key === 'buffer' || key === 'id') &&
|
||||||
|
isTwelveByteBuffer(val.buffer || val.id)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursive function
|
|
||||||
function expand(value) {
|
function expand(value) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value.map(expand);
|
return value.map(expand);
|
||||||
} else if (value instanceof Date) {
|
} else if (value instanceof Date) {
|
||||||
return value;
|
return value;
|
||||||
} else if (value && typeof value === 'object' && !(value instanceof mongoose.Types.ObjectId)) {
|
} else if (isObjectId(value)) {
|
||||||
|
return { _id: objectIdToString(value) };
|
||||||
|
} else if (value && typeof value === 'object') {
|
||||||
var result = {};
|
var result = {};
|
||||||
for (const [key, val] of Object.entries(value)) {
|
for (const [key, val] of Object.entries(value)) {
|
||||||
if (excludedFields.includes(key)) {
|
if (excludedFields.includes(key)) {
|
||||||
// Do not expand keys that are excluded
|
result[key] = val == null ? val : key === '_id' ? objectIdToString(val) : val.toString();
|
||||||
result[key] = val == null ? val : val.toString();
|
|
||||||
} else if (isObjectId(val)) {
|
} else if (isObjectId(val)) {
|
||||||
result[key] = { _id: val };
|
result[key] = { _id: objectIdToString(val) };
|
||||||
} else if (Array.isArray(val)) {
|
} else if (Array.isArray(val)) {
|
||||||
result[key] = val.map(expand);
|
result[key] = val.map(expand);
|
||||||
} else if (val instanceof Date) {
|
} else if (val instanceof Date) {
|
||||||
@ -1560,8 +1613,6 @@ function expandObjectIds(input) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} else if (isObjectId(value)) {
|
|
||||||
return { _id: value };
|
|
||||||
} else {
|
} else {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user