Compare commits

..

2 Commits

Author SHA1 Message Date
8974d1e9da Add quantity field to document job schema and update related routes and services
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
This commit introduces a new `quantity` field to the document job schema, ensuring it is required with a default value of 1. The routes and services for managing document jobs are updated to include this new field, allowing for its inclusion in both creation and editing processes. Additionally, the allowed filters and sorters are modified to accommodate the new `quantity` field, enhancing the overall functionality and flexibility of document job management.
2026-08-22 20:02:28 +01:00
0470f62e05 Refactor schema type handling and enhance utility functions in utils and database modules
This commit introduces several utility functions to improve schema type handling, including `getEmbeddedSchemaType` and `getObjectIdSchemaTypeFromPath`. The `getFilterFieldKind` function is updated to utilize these new utilities for better clarity and functionality. Additionally, the `getArrayParentPaths` function is renamed to `getArrayUnwindPaths` for improved readability, and the logic for fetching unique IDs is enhanced with a new `flattenRefIds` function. These changes streamline the code and enhance the overall management of schema references and array paths.
2026-08-22 20:02:21 +01:00
5 changed files with 107 additions and 26 deletions

View File

@ -657,21 +657,26 @@ export const listPropertyValues = async ({ model, property, filter = {}, search
return await model.aggregate(aggregateCommand); return await model.aggregate(aggregateCommand);
}; };
function getEmbeddedSchemaType(path) {
if (!path) return null;
return path.embeddedSchemaType ?? path.$embeddedSchemaType ?? path.caster ?? null;
}
function getObjectRefPathInfo(model, property) { function getObjectRefPathInfo(model, property) {
if (!model?.schema || !property) return null; if (!model?.schema || !property) return null;
const path = model.schema.path(property); const path = model.schema.path(property);
if (!path) return null; if (!path) return null;
const schemaType = path.instance === 'Array' ? path.caster : path; const schemaType = path.instance === 'Array' ? getEmbeddedSchemaType(path) : path;
if (!schemaType) return null; if (!schemaType) return null;
const instance = schemaType.instance; const instance = schemaType.instance;
if (instance !== 'ObjectID' && instance !== 'ObjectId') return null; if (instance !== 'ObjectID' && instance !== 'ObjectId') return null;
return { return {
ref: schemaType.options?.ref, ref: schemaType.options?.ref ?? path.options?.ref,
refPath: schemaType.options?.refPath, refPath: schemaType.options?.refPath ?? path.options?.refPath,
}; };
} }
@ -690,28 +695,52 @@ function inferTypeFieldFromRefFunction(refFn) {
return matches.find((name) => name.endsWith('Type') || name === 'type') || matches[0] || null; return matches.find((name) => name.endsWith('Type') || name === 'type') || matches[0] || null;
} }
function getArrayParentPaths(model, property) { function getArrayUnwindPaths(model, property) {
if (!model?.schema || !property?.includes('.')) return []; if (!model?.schema || !property) return [];
const parents = []; const paths = [];
const parts = property.split('.'); const parts = property.split('.');
for (let i = 1; i < parts.length; i++) { for (let i = 1; i < parts.length; i++) {
const parentPath = parts.slice(0, i).join('.'); const parentPath = parts.slice(0, i).join('.');
const parentSchemaPath = model.schema.path(parentPath); const parentSchemaPath = model.schema.path(parentPath);
if (parentSchemaPath?.instance === 'Array') { if (parentSchemaPath?.instance === 'Array') {
parents.push(parentPath); paths.push(parentPath);
} }
} }
return parents;
const path = model.schema.path(property);
if (path?.instance === 'Array') {
paths.push(property);
}
return paths;
}
function flattenRefIds(ids) {
const flattened = [];
const visit = (value) => {
if (value == null) return;
if (Array.isArray(value)) {
value.forEach(visit);
return;
}
if (
typeof value === 'object' &&
!(value instanceof mongoose.Types.ObjectId) &&
!(value instanceof Date) &&
value._id != null
) {
visit(value._id);
return;
}
flattened.push(value);
};
visit(ids);
return flattened;
} }
async function fetchBasicObjectsByIds(refName, ids) { async function fetchBasicObjectsByIds(refName, ids) {
const uniqueIds = [ const uniqueIds = [
...new Map( ...new Map(flattenRefIds(ids).map((id) => [id.toString(), id])).values(),
(ids || [])
.filter((id) => id != null)
.map((id) => [id.toString(), id])
).values(),
]; ];
if (uniqueIds.length === 0) return []; if (uniqueIds.length === 0) return [];
@ -762,10 +791,10 @@ export const getPropertyValues = async ({ model, property, filter = {} }) => {
pipeline.push({ $match: convertedFilter }); pipeline.push({ $match: convertedFilter });
} }
for (const parentPath of getArrayParentPaths(model, property)) { for (const unwindPath of getArrayUnwindPaths(model, property)) {
pipeline.push({ pipeline.push({
$unwind: { $unwind: {
path: `$${parentPath}`, path: `$${unwindPath}`,
preserveNullAndEmptyArrays: false, preserveNullAndEmptyArrays: false,
}, },
}); });
@ -803,6 +832,28 @@ export const getPropertyValues = async ({ model, property, filter = {} }) => {
} }
if (hasFixedRef) { if (hasFixedRef) {
const unwindPaths = getArrayUnwindPaths(model, property);
if (unwindPaths.length > 0) {
const pipeline = [];
if (Object.keys(convertedFilter).length > 0) {
pipeline.push({ $match: convertedFilter });
}
for (const unwindPath of unwindPaths) {
pipeline.push({
$unwind: {
path: `$${unwindPath}`,
preserveNullAndEmptyArrays: false,
},
});
}
pipeline.push(
{ $match: { [property]: { $ne: null } } },
{ $group: { _id: `$${property}` } }
);
const ids = (await model.aggregate(pipeline)).map((row) => row._id);
return fetchBasicObjectsByIds(pathInfo.ref, ids);
}
const ids = await model.distinct(property, convertedFilter); const ids = await model.distinct(property, convertedFilter);
return fetchBasicObjectsByIds(pathInfo.ref, ids); return fetchBasicObjectsByIds(pathInfo.ref, ids);
} }

View File

@ -31,6 +31,12 @@ const documentJobSchema = new Schema(
ref: 'documentPrinter', ref: 'documentPrinter',
required: true, required: true,
}, },
quantity: {
type: Number,
required: true,
default: 1,
min: 1,
},
content: { content: {
type: String, type: String,
required: false, required: false,

View File

@ -7,6 +7,7 @@ const router = express.Router();
const listAllowedFilters = [ const listAllowedFilters = [
'name', 'name',
'quantity',
'width', 'width',
'height', 'height',
'state', 'state',
@ -14,7 +15,7 @@ const listAllowedFilters = [
'updatedAt', 'updatedAt',
'_reference', '_reference',
]; ];
const listAllowedSorters = ['name', 'state', 'createdAt', 'updatedAt']; const listAllowedSorters = ['name', 'quantity', 'state', 'createdAt', 'updatedAt'];
const propertiesAllowedFilters = []; const propertiesAllowedFilters = [];
import { import {
listDocumentJobsRouteHandler, listDocumentJobsRouteHandler,

View File

@ -113,6 +113,9 @@ export const editDocumentJobRouteHandler = async (req, res) => {
logger.trace(`Document Job with ID: ${id}`); logger.trace(`Document Job with ID: ${id}`);
const updateData = {}; const updateData = {};
if (req.body.quantity != null) {
updateData.quantity = req.body.quantity;
}
// Create audit log before updating // Create audit log before updating
const result = await editObject({ const result = await editObject({
model: documentJobModel, model: documentJobModel,
@ -142,6 +145,7 @@ export const newDocumentJobRouteHandler = async (req, res) => {
objectType: req.body.objectType, objectType: req.body.objectType,
object: req.body.object, object: req.body.object,
content: req.body.content, content: req.body.content,
quantity: req.body.quantity ?? 1,
state: { type: 'draft' }, state: { type: 'draft' },
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),

View File

@ -93,8 +93,19 @@ function getBaseProperty(property) {
return property; return property;
} }
function isObjectIdSchemaType(path) { function getEmbeddedSchemaType(path) {
return path?.instance === 'ObjectId' || path?.instance === 'ObjectID'; if (!path) return null;
return path.embeddedSchemaType ?? path.$embeddedSchemaType ?? path.caster ?? null;
}
function getObjectIdSchemaTypeFromPath(path) {
if (!path) return null;
const schemaType = path.instance === 'Array' ? getEmbeddedSchemaType(path) : path;
if (!schemaType) return null;
if (schemaType.instance === 'ObjectId' || schemaType.instance === 'ObjectID') {
return schemaType;
}
return null;
} }
function getSchemaPathFromModels(property, model = null) { function getSchemaPathFromModels(property, model = null) {
@ -112,12 +123,14 @@ function getFilterFieldKind(property, model = null) {
const inspectPath = (path) => { const inspectPath = (path) => {
if (!path) return null; if (!path) return null;
if (isObjectIdPath(property) || isObjectIdSchemaType(path)) { const objectIdType = getObjectIdSchemaTypeFromPath(path);
if (property.endsWith('._id') && property !== '_id' && path.options?.ref) { if (isObjectIdPath(property) || objectIdType) {
const ref = objectIdType?.options?.ref ?? path.options?.ref;
if (property.endsWith('._id') && property !== '_id' && ref) {
return { kind: 'objectId', property: baseProperty }; return { kind: 'objectId', property: baseProperty };
} }
if (path.options?.ref) { if (ref) {
return { kind: 'objectRef', property: baseProperty, ref: path.options.ref }; return { kind: 'objectRef', property: baseProperty, ref };
} }
return { kind: 'objectId', property: baseProperty }; return { kind: 'objectId', property: baseProperty };
} }
@ -162,7 +175,8 @@ function buildRegexOp(pattern, useOptions = true) {
function getSchemaRefName(property, model = null) { function getSchemaRefName(property, model = null) {
const baseProperty = getBaseProperty(property); const baseProperty = getBaseProperty(property);
const path = model?.schema?.path(baseProperty) ?? getSchemaPathFromModels(baseProperty); const path = model?.schema?.path(baseProperty) ?? getSchemaPathFromModels(baseProperty);
return path?.options?.ref ?? path?.caster?.options?.ref ?? null; const embeddedType = getEmbeddedSchemaType(path);
return path?.options?.ref ?? embeddedType?.options?.ref ?? null;
} }
function getRefModelEntryFromSchemaRef(refName) { function getRefModelEntryFromSchemaRef(refName) {
@ -1264,6 +1278,7 @@ function filterDistributeKeys(value, keys = DISTRIBUTE_KEYS) {
result[key] = val; result[key] = val;
} }
} }
console.log('filterDistributeKeys', result);
return result; return result;
} }
@ -1276,11 +1291,11 @@ async function distributeStats(value, type) {
} }
async function distributeNew(value, type) { async function distributeNew(value, type) {
await natsServer.publish(`${type}s.new`, filterDistributeKeys(value)); await natsServer.publish(`${type}s.new`, value);
} }
async function distributeDelete(value, type) { async function distributeDelete(value, type) {
await natsServer.publish(`${type}s.delete`, filterDistributeKeys(value)); await natsServer.publish(`${type}s.delete`, value);
} }
function getReferenceId(value) { function getReferenceId(value) {
@ -1589,7 +1604,11 @@ function normalizeFilterQuery(query = {}) {
// qs turns `order._id=` into `{ order: { _id } }`. Lift it so it can be parsed // qs turns `order._id=` into `{ order: { _id } }`. Lift it so it can be parsed
// as the order object filter instead of a nested query object. // as the order object filter instead of a nested query object.
if (queryClean.order && typeof queryClean.order === 'object' && !Array.isArray(queryClean.order)) { if (
queryClean.order &&
typeof queryClean.order === 'object' &&
!Array.isArray(queryClean.order)
) {
if (queryClean.order._id !== undefined) { if (queryClean.order._id !== undefined) {
queryClean['order._id'] = queryClean.order._id; queryClean['order._id'] = queryClean.order._id;
} }