Implemented job editing functionality by adding an editJobRouteHandler and associated logic to update draft jobs. Enhanced job schema with a recalculate method to manage subjob creation and updates based on job quantity and printer assignments. Updated tests to cover new editing capabilities and ensure proper state checks.
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good

This commit is contained in:
Tom Butcher 2026-07-18 15:19:11 +01:00
parent 5419b3fbd1
commit e112ec4386
4 changed files with 195 additions and 35 deletions

View File

@ -1,7 +1,15 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const { Schema } = mongoose;
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
import {
aggregateRollups,
aggregateRollupsHistory,
listObjects,
editObject,
newObject,
deleteObject,
} from '../../database.js';
import { subJobModel } from './subjob.schema.js';
const jobSchema = new mongoose.Schema(
{
@ -84,6 +92,95 @@ jobSchema.statics.history = async function (from, to) {
return results;
};
const normalizeId = (value) => value?._id ?? value;
jobSchema.statics.recalculate = async function (job, user) {
const jobId = job._id || job;
if (!jobId || job?.state?.type !== 'draft') {
return;
}
const quantity = job.quantity || 1;
const printers = (job.printers || []).map((printer) => normalizeId(printer));
const gcodeFile = normalizeId(job.gcodeFile);
const existingSubJobs = await listObjects({
model: subJobModel,
filter: { job: jobId },
sort: 'number',
order: 'ascend',
pagination: false,
});
if (existingSubJobs?.error) {
throw existingSubJobs;
}
const existingSubJobsByNumber = new Map(
existingSubJobs.map((subJob) => [subJob.number, subJob])
);
var printerCount = 0;
for (let i = 0; i < quantity; i++) {
const subJobNumber = i + 1;
const printer = printers[printerCount];
const subJobUpdateData = {
updatedAt: new Date(),
printer,
gcodeFile,
number: subJobNumber,
};
const existingSubJob = existingSubJobsByNumber.get(subJobNumber);
if (existingSubJob) {
const subJobResult = await editObject({
model: subJobModel,
id: existingSubJob._id,
updateData: subJobUpdateData,
user,
});
if (subJobResult.error) {
throw subJobResult;
}
} else {
const subJobResult = await newObject({
model: subJobModel,
newData: {
createdAt: new Date(),
...subJobUpdateData,
job: jobId,
state: { type: 'draft' },
},
user,
});
if (subJobResult.error) {
throw subJobResult;
}
}
if (printerCount >= printers.length - 1) {
printerCount = 0;
} else {
printerCount += 1;
}
}
for (const existingSubJob of existingSubJobs) {
if (existingSubJob.number > quantity) {
const deleteResult = await deleteObject({
model: subJobModel,
id: existingSubJob._id,
user,
});
if (deleteResult.error) {
throw deleteResult;
}
}
}
};
jobSchema.virtual('id').get(function () {
return this._id;
});

View File

@ -8,6 +8,7 @@ import {
getJobRouteHandler,
newJobRouteHandler,
deleteJobRouteHandler,
editJobRouteHandler,
getJobStatsRouteHandler,
getJobHistoryRouteHandler,
searchJobsRouteHandler
@ -52,6 +53,10 @@ router.get('/:id', isAuthenticated, (req, res) => {
getJobRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => {
editJobRouteHandler(req, res);
});
router.delete('/:id', isAuthenticated, async (req, res) => {
deleteJobRouteHandler(req, res);
});

View File

@ -6,10 +6,12 @@ jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
newObject: jest.fn(),
editObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
checkStates: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/production/job.schema.js', () => ({
@ -33,10 +35,15 @@ jest.unstable_mockModule('log4js', () => ({
}));
// Import handlers after mocking
const { listJobsRouteHandler, getJobRouteHandler, newJobRouteHandler, deleteJobRouteHandler } =
await import('../jobs.js');
const {
listJobsRouteHandler,
getJobRouteHandler,
newJobRouteHandler,
editJobRouteHandler,
deleteJobRouteHandler,
} = await import('../jobs.js');
const { listObjects, getObject, newObject, deleteObject } = await import(
const { listObjects, getObject, newObject, editObject, deleteObject, checkStates } = await import(
'../../../database/database.js'
);
const { jobModel } = await import('../../../database/schemas/production/job.schema.js');
@ -72,7 +79,7 @@ describe('Job Service Route Handlers', () => {
});
describe('newJobRouteHandler', () => {
it('should create a new job and corresponding subjobs', async () => {
it('should create a new job', async () => {
req.body = {
quantity: 2,
printers: ['p1', 'p2'],
@ -80,12 +87,11 @@ describe('Job Service Route Handlers', () => {
};
const mockJob = { _id: 'job123' };
newObject.mockResolvedValueOnce(mockJob); // For Job
newObject.mockResolvedValue({ _id: 'subjob' }); // For SubJobs
newObject.mockResolvedValueOnce(mockJob);
await newJobRouteHandler(req, res);
expect(newObject).toHaveBeenCalledTimes(3); // 1 Job + 2 SubJobs
expect(newObject).toHaveBeenCalledTimes(1);
expect(res.send).toHaveBeenCalledWith(mockJob);
});
@ -100,6 +106,40 @@ describe('Job Service Route Handlers', () => {
});
});
describe('editJobRouteHandler', () => {
it('should update a draft job', async () => {
req.params.id = '507f1f77bcf86cd799439011';
req.body = {
quantity: 2,
printers: ['p1', 'p2'],
gcodeFile: 'file123',
};
checkStates.mockResolvedValue(true);
editObject.mockResolvedValue({ _id: '507f1f77bcf86cd799439011', ...req.body });
await editJobRouteHandler(req, res);
expect(checkStates).toHaveBeenCalledWith(
expect.objectContaining({ states: ['draft'] })
);
expect(editObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalled();
});
it('should fail if job is not in draft state', async () => {
req.params.id = '507f1f77bcf86cd799439011';
checkStates.mockResolvedValue(false);
await editJobRouteHandler(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.send).toHaveBeenCalledWith(
expect.objectContaining({ error: 'Job is not in draft state.' })
);
});
});
describe('deleteJobRouteHandler', () => {
it('should delete a job', async () => {
req.params.id = '507f1f77bcf86cd799439011';

View File

@ -8,11 +8,12 @@ import {
listObjects,
listObjectsByProperties,
newObject,
editObject,
getModelStats,
getModelHistory,
searchObjects
searchObjects,
checkStates
} from '../../database/database.js';
import { subJobModel } from '../../database/schemas/production/subjob.schema.js';
const logger = log4js.getLogger('Jobs');
logger.level = config.server.logLevel;
@ -117,34 +118,51 @@ export const newJobRouteHandler = async (req, res) => {
logger.debug(`New job with ID: ${result._id}`);
var printerCount = 0;
res.send(result);
};
for (let i = 0; i < newData.quantity; i++) {
const newSubJobData = {
createdAt: new Date(),
export const editJobRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Job with ID: ${id}`);
const checkStatesResult = await checkStates({ model: jobModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking job states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Job is not in draft state.');
res.status(400).send({ error: 'Job is not in draft state.', code: 400 });
return;
}
const updateData = {
updatedAt: new Date(),
printer: newData.printers[printerCount],
quantity: req.body.quantity,
printers: req.body.printers,
gcodeFile: req.body.gcodeFile,
number: i + 1,
job: result._id,
state: { type: 'draft' },
};
const subJobResult = await newObject({
model: subJobModel,
newData: newSubJobData,
const result = await editObject({
model: jobModel,
id,
updateData,
user: req.user,
populate: ['gcodeFile', 'printers'],
});
if (subJobResult.error) {
logger.error('No sub job created:', result.error);
return res.status(result.code).send(result);
}
if (printerCount >= newData.printers.length - 1) {
printerCount = 0;
} else {
printerCount += 1;
}
if (result.error) {
logger.error('Error editing job:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Edited job with ID: ${id}`);
res.send(result);
};