diff --git a/packages/api/src/controllers/animalBatchController.js b/packages/api/src/controllers/animalBatchController.js
index 49921be7de..229eb52999 100644
--- a/packages/api/src/controllers/animalBatchController.js
+++ b/packages/api/src/controllers/animalBatchController.js
@@ -16,10 +16,8 @@
import { Model, transaction } from 'objection';
import AnimalBatchModel from '../models/animalBatchModel.js';
import baseController from './baseController.js';
-import CustomAnimalBreedModel from '../models/customAnimalBreedModel.js';
-import CustomAnimalTypeModel from '../models/customAnimalTypeModel.js';
import { handleObjectionError } from '../util/errorCodes.js';
-import { assignInternalIdentifiers } from '../util/animal.js';
+import { assignInternalIdentifiers, checkAndAddCustomTypeAndBreed } from '../util/animal.js';
import { uploadPublicImage } from '../util/imageUpload.js';
const animalBatchController = {
@@ -60,50 +58,14 @@ const animalBatchController = {
const { farm_id } = req.headers;
const result = [];
- // avoid attempts to add an already created type or breed to the DB
- // where multiple batches have the same type_name or breed_name
- const typeIdsMap = {};
- const typeBreedIdsMap = {};
+ // Create utility object used in type and breed
+ req.body.typeIdsMap = {};
+ req.body.typeBreedIdsMap = {};
for (const animalBatch of req.body) {
- if (animalBatch.type_name) {
- let typeId = typeIdsMap[animalBatch.type_name];
-
- if (!typeId) {
- const newType = await baseController.postWithResponse(
- CustomAnimalTypeModel,
- { type: animalBatch.type_name, farm_id },
- req,
- { trx },
- );
- typeId = newType.id;
- typeIdsMap[animalBatch.type_name] = typeId;
- }
- animalBatch.custom_type_id = typeId;
- delete animalBatch.type_name;
- }
-
- if (animalBatch.breed_name) {
- const typeColumn = animalBatch.default_type_id ? 'default_type_id' : 'custom_type_id';
- const typeId = animalBatch.type_name
- ? typeIdsMap[animalBatch.type_name]
- : animalBatch.default_type_id || animalBatch.custom_type_id;
- const typeBreedKey = `${typeColumn}_${typeId}_${animalBatch.breed_name}`;
- let breedId = typeBreedIdsMap[typeBreedKey];
-
- if (!breedId) {
- const newBreed = await baseController.postWithResponse(
- CustomAnimalBreedModel,
- { farm_id, [typeColumn]: typeId, breed: animalBatch.breed_name },
- req,
- { trx },
- );
- breedId = newBreed.id;
- typeBreedIdsMap[typeBreedKey] = breedId;
- }
- animalBatch.custom_breed_id = breedId;
- delete animalBatch.breed_name;
- }
+ await checkAndAddCustomTypeAndBreed(req, animalBatch, farm_id, trx);
+ // TODO: allow animal group addition on creation like animals
+ // await checkAndAddGroup(req, animal, farm_id, trx);
// Remove farm_id if it happens to be set in animal object since it should be obtained from header
delete animalBatch.farm_id;
@@ -115,8 +77,17 @@ const animalBatchController = {
{ trx },
);
+ // TODO: allow animal group addition on creation like animals
+ // Format group_ids
+ // const groupIdMap =
+ // individualAnimalBatchResult.group_ids?.map((group) => group.animal_group_id) || [];
+ // individualAnimalBatchResult.group_ids = groupIdMap;
+
result.push(individualAnimalBatchResult);
}
+ // delete utility objects
+ delete req.body.typeIdsMap;
+ delete req.body.typeBreedIdsMap;
await trx.commit();
@@ -128,6 +99,80 @@ const animalBatchController = {
};
},
+ editAnimalBatches() {
+ return async (req, res) => {
+ const trx = await transaction.start(Model.knex());
+
+ try {
+ const { farm_id } = req.headers;
+
+ // Create utility object used in type and breed
+ req.body.typeIdsMap = {};
+ req.body.typeBreedIdsMap = {};
+
+ // select only allowed properties to edit
+ for (const animalBatch of req.body) {
+ await checkAndAddCustomTypeAndBreed(req, animalBatch, farm_id, trx);
+ // TODO: allow animal group editing
+ // await checkAndAddGroup(req, animal, farm_id, trx);
+
+ const {
+ id,
+ count,
+ custom_breed_id,
+ custom_type_id,
+ default_breed_id,
+ default_type_id,
+ name,
+ notes,
+ photo_url,
+ organic_status,
+ supplier,
+ price,
+ sex_detail,
+ origin_id,
+ group_ids,
+ animal_batch_use_relationships,
+ } = animalBatch;
+
+ await baseController.upsertGraph(
+ AnimalBatchModel,
+ {
+ id,
+ count,
+ custom_breed_id,
+ custom_type_id,
+ default_breed_id,
+ default_type_id,
+ name,
+ notes,
+ photo_url,
+ organic_status,
+ supplier,
+ price,
+ sex_detail,
+ origin_id,
+ group_ids,
+ animal_batch_use_relationships,
+ },
+ req,
+ { trx },
+ );
+ }
+
+ // delete utility objects
+ delete req.body.typeIdsMap;
+ delete req.body.typeBreedIdsMap;
+
+ await trx.commit();
+ // Do not send result revalidate using tags on frontend
+ return res.status(204).send();
+ } catch (error) {
+ handleObjectionError(error, res, trx);
+ }
+ };
+ },
+
removeAnimalBatches() {
return async (req, res) => {
const trx = await transaction.start(Model.knex());
diff --git a/packages/api/src/controllers/animalController.js b/packages/api/src/controllers/animalController.js
index 974472ee30..ff5c74edea 100644
--- a/packages/api/src/controllers/animalController.js
+++ b/packages/api/src/controllers/animalController.js
@@ -16,14 +16,12 @@
import { Model, transaction } from 'objection';
import AnimalModel from '../models/animalModel.js';
import baseController from './baseController.js';
-import CustomAnimalBreedModel from '../models/customAnimalBreedModel.js';
-import CustomAnimalTypeModel from '../models/customAnimalTypeModel.js';
-import AnimalGroupModel from '../models/animalGroupModel.js';
-import AnimalGroupRelationshipModel from '../models/animalGroupRelationshipModel.js';
-import { assignInternalIdentifiers } from '../util/animal.js';
+import {
+ assignInternalIdentifiers,
+ checkAndAddGroup,
+ checkAndAddCustomTypeAndBreed,
+} from '../util/animal.js';
import { handleObjectionError } from '../util/errorCodes.js';
-import { checkAndTrimString } from '../util/util.js';
-import AnimalUseRelationshipModel from '../models/animalUseRelationshipModel.js';
import { uploadPublicImage } from '../util/imageUpload.js';
const animalController = {
@@ -58,126 +56,140 @@ const animalController = {
addAnimals() {
return async (req, res) => {
const trx = await transaction.start(Model.knex());
-
try {
const { farm_id } = req.headers;
const result = [];
- // avoid attempts to add an already created type or breed to the DB
- // where multiple animals have the same type_name or breed_name
- const typeIdsMap = {};
- const typeBreedIdsMap = {};
+ // Create utility object used in type and breed
+ req.body.typeIdsMap = {};
+ req.body.typeBreedIdsMap = {};
for (const animal of req.body) {
- if (animal.type_name) {
- let typeId = typeIdsMap[animal.type_name];
-
- if (!typeId) {
- const newType = await baseController.postWithResponse(
- CustomAnimalTypeModel,
- { type: animal.type_name, farm_id },
- req,
- { trx },
- );
- typeId = newType.id;
- typeIdsMap[animal.type_name] = typeId;
- }
- animal.custom_type_id = typeId;
- delete animal.type_name;
- }
-
- if (animal.breed_name) {
- const typeColumn = animal.default_type_id ? 'default_type_id' : 'custom_type_id';
- const typeId = animal.type_name
- ? typeIdsMap[animal.type_name]
- : animal.default_type_id || animal.custom_type_id;
- const typeBreedKey = `${typeColumn}_${typeId}_${animal.breed_name}`;
- let breedId = typeBreedIdsMap[typeBreedKey];
-
- if (!breedId) {
- const newBreed = await baseController.postWithResponse(
- CustomAnimalBreedModel,
- { farm_id, [typeColumn]: typeId, breed: animal.breed_name },
- req,
- { trx },
- );
- breedId = newBreed.id;
- typeBreedIdsMap[typeBreedKey] = breedId;
- }
- animal.custom_breed_id = breedId;
- delete animal.breed_name;
- }
+ await checkAndAddCustomTypeAndBreed(req, animal, farm_id, trx);
+ // TODO: Comment out for animals v1?
+ await checkAndAddGroup(req, animal, farm_id, trx);
// Remove farm_id if it happens to be set in animal object since it should be obtained from header
delete animal.farm_id;
- const groupName = checkAndTrimString(animal.group_name);
- delete animal.group_name;
-
- const individualAnimalResult = await baseController.postWithResponse(
+ const individualAnimalResult = await baseController.insertGraphWithResponse(
AnimalModel,
{ ...animal, farm_id },
req,
{ trx },
);
- const groupIds = [];
- if (groupName) {
- let group = await baseController.existsInTable(trx, AnimalGroupModel, {
- name: groupName,
- farm_id,
- deleted: false,
- });
-
- if (!group) {
- group = await baseController.postWithResponse(
- AnimalGroupModel,
- { name: groupName, farm_id },
- req,
- { trx },
- );
- }
-
- groupIds.push(group.id);
-
- // Insert into join table
- await AnimalGroupRelationshipModel.query(trx).insert({
- animal_id: individualAnimalResult.id,
- animal_group_id: group.id,
- });
- }
-
- individualAnimalResult.group_ids = groupIds;
-
- const animalUseRelationships = [];
- if (animal.animal_use_relationships?.length) {
- for (const relationship of animal.animal_use_relationships) {
- animalUseRelationships.push(
- await baseController.postWithResponse(
- AnimalUseRelationshipModel,
- { ...relationship, animal_id: individualAnimalResult.id },
- req,
- { trx },
- ),
- );
- }
- }
-
- individualAnimalResult.animal_use_relationships = animalUseRelationships;
+ // TODO: Comment out for animals v1?
+ // Format group_ids
+ const groupIdMap =
+ individualAnimalResult.group_ids?.map((group) => group.animal_group_id) || [];
+ individualAnimalResult.group_ids = groupIdMap;
result.push(individualAnimalResult);
}
+ // delete utility objects
+ delete req.body.typeIdsMap;
+ delete req.body.typeBreedIdsMap;
+
await trx.commit();
await assignInternalIdentifiers(result, 'animal');
+
return res.status(201).send(result);
} catch (error) {
await handleObjectionError(error, res, trx);
}
};
},
+ editAnimals() {
+ return async (req, res) => {
+ const trx = await transaction.start(Model.knex());
+
+ try {
+ const { farm_id } = req.headers;
+ // Create utility object used in type and breed
+ req.body.typeIdsMap = {};
+ req.body.typeBreedIdsMap = {};
+
+ // select only allowed properties to edit
+ for (const animal of req.body) {
+ await checkAndAddCustomTypeAndBreed(req, animal, farm_id, trx);
+ // TODO: Comment out for animals v1?
+ await checkAndAddGroup(req, animal, farm_id, trx);
+ const {
+ id,
+ default_type_id,
+ custom_type_id,
+ default_breed_id,
+ custom_breed_id,
+ sex_id,
+ name,
+ birth_date,
+ identifier,
+ identifier_color_id,
+ identifier_placement_id,
+ origin_id,
+ dam,
+ sire,
+ brought_in_date,
+ weaning_date,
+ notes,
+ photo_url,
+ identifier_type_id,
+ identifier_type_other,
+ organic_status,
+ supplier,
+ price,
+ group_ids,
+ animal_use_relationships,
+ } = animal;
+
+ await baseController.upsertGraph(
+ AnimalModel,
+ {
+ id,
+ default_type_id,
+ custom_type_id,
+ default_breed_id,
+ custom_breed_id,
+ sex_id,
+ name,
+ birth_date,
+ identifier,
+ identifier_color_id,
+ identifier_placement_id,
+ origin_id,
+ dam,
+ sire,
+ brought_in_date,
+ weaning_date,
+ notes,
+ photo_url,
+ identifier_type_id,
+ identifier_type_other,
+ organic_status,
+ supplier,
+ price,
+ group_ids,
+ animal_use_relationships,
+ },
+ req,
+ { trx },
+ );
+ }
+ // delete utility objects
+ delete req.body.typeIdsMap;
+ delete req.body.typeBreedIdsMap;
+ await trx.commit();
+ // Do not send result revalidate using tags on frontend
+ return res.status(204).send();
+ } catch (error) {
+ handleObjectionError(error, res, trx);
+ }
+ };
+ },
removeAnimals() {
return async (req, res) => {
const trx = await transaction.start(Model.knex());
diff --git a/packages/api/src/middleware/checkAnimalEntities.js b/packages/api/src/middleware/checkAnimalEntities.js
deleted file mode 100644
index 4b477333cf..0000000000
--- a/packages/api/src/middleware/checkAnimalEntities.js
+++ /dev/null
@@ -1,267 +0,0 @@
-import { Model, transaction } from 'objection';
-import { handleObjectionError } from '../util/errorCodes.js';
-
-import CustomAnimalTypeModel from '../models/customAnimalTypeModel.js';
-import DefaultAnimalBreedModel from '../models/defaultAnimalBreedModel.js';
-import CustomAnimalBreedModel from '../models/customAnimalBreedModel.js';
-import AnimalUseModel from '../models/animalUseModel.js';
-
-/**
- * Middleware function to check if the provided animal entities exist and belong to the farm. The IDs must be passed as a comma-separated query string.
- *
- * @param {Object} model - The database model for the correct animal entity
- * @returns {Function} - Express middleware function
- *
- * @example
- * router.delete(
- * '/',
- * checkScope(['delete:animals']),
- * checkAnimalEntities(AnimalModel),
- * AnimalController.deleteAnimals(),
- * );
- *
- */
-export function checkAnimalEntities(model) {
- return async (req, res, next) => {
- const trx = await transaction.start(Model.knex());
-
- try {
- const { farm_id } = req.headers;
- const { ids } = req.query;
-
- if (!ids || !ids.length) {
- await trx.rollback();
- return res.status(400).send('Must send ids');
- }
-
- const idsSet = new Set(ids.split(','));
-
- // Check that all animals/batches exist and belong to the farm
- const invalidIds = [];
-
- for (const id of idsSet) {
- // For query syntax like ids=,,, which will pass the above check
- if (!id || isNaN(Number(id))) {
- await trx.rollback();
- return res.status(400).send('Must send valid ids');
- }
-
- const existingRecord = await model
- .query(trx)
- .findById(id)
- .where({ farm_id })
- .whereNotDeleted(); // prohibiting re-delete
-
- if (!existingRecord) {
- invalidIds.push(id);
- }
- }
-
- if (invalidIds.length) {
- await trx.rollback();
- return res.status(400).json({
- error: 'Invalid ids',
- invalidIds,
- message:
- 'Some entities do not exist, are already deleted, or are not associated with the given farm.',
- });
- }
-
- await trx.commit();
- next();
- } catch (error) {
- handleObjectionError(error, res, trx);
- }
- };
-}
-
-const hasOneValue = (values) => {
- const nonNullValues = values.filter(Boolean);
- return nonNullValues.length === 1;
-};
-
-const allFalsy = (values) => values.every((value) => !value);
-
-export function validateAnimalBatchCreationBody(animalBatchKey) {
- return async (req, res, next) => {
- const trx = await transaction.start(Model.knex());
-
- try {
- const { farm_id } = req.headers;
- const newTypesSet = new Set();
- const newBreedsSet = new Set();
-
- for (const animalOrBatch of req.body) {
- const {
- default_type_id,
- custom_type_id,
- default_breed_id,
- custom_breed_id,
- type_name,
- breed_name,
- } = animalOrBatch;
-
- if (!hasOneValue([default_type_id, custom_type_id, type_name])) {
- await trx.rollback();
- return res
- .status(400)
- .send('Exactly one of default_type_id, custom_type_id, or type_name must be sent');
- }
-
- if (
- !hasOneValue([default_breed_id, custom_breed_id, breed_name]) &&
- !allFalsy([default_breed_id, custom_breed_id, breed_name])
- ) {
- await trx.rollback();
- return res
- .status(400)
- .send('Exactly one of default_breed_id, custom_breed_id and breed_name must be sent');
- }
-
- if (custom_type_id) {
- const customType = await CustomAnimalTypeModel.query().findById(custom_type_id);
-
- if (customType && customType.farm_id !== farm_id) {
- await trx.rollback();
- return res.status(403).send('Forbidden custom type does not belong to this farm');
- }
- }
-
- if (default_breed_id && default_type_id) {
- const defaultBreed = await DefaultAnimalBreedModel.query().findById(default_breed_id);
-
- if (defaultBreed && defaultBreed.default_type_id !== default_type_id) {
- await trx.rollback();
- return res.status(400).send('Breed does not match type');
- }
- }
-
- if (default_breed_id && custom_type_id) {
- await trx.rollback();
- return res.status(400).send('Default breed does not use custom type');
- }
-
- if (custom_breed_id) {
- const customBreed = await CustomAnimalBreedModel.query()
- .whereNotDeleted()
- .findById(custom_breed_id);
-
- if (customBreed && customBreed.farm_id !== farm_id) {
- await trx.rollback();
- return res.status(403).send('Forbidden custom breed does not belong to this farm');
- }
-
- if (customBreed.default_type_id && customBreed.default_type_id !== default_type_id) {
- await trx.rollback();
- return res.status(400).send('Breed does not match type');
- }
-
- if (customBreed.custom_type_id && customBreed.custom_type_id !== custom_type_id) {
- await trx.rollback();
- return res.status(400).send('Breed does not match type');
- }
- }
-
- if (animalBatchKey === 'batch') {
- const { count, sex_detail } = animalOrBatch;
-
- if (sex_detail?.length) {
- let sexCount = 0;
- const sexIdSet = new Set();
- sex_detail.forEach((detail) => {
- sexCount += detail.count;
- sexIdSet.add(detail.sex_id);
- });
- if (sexCount > count) {
- await trx.rollback();
- return res
- .status(400)
- .send('Batch count must be greater than or equal to sex detail count');
- }
- if (sex_detail.length != sexIdSet.size) {
- await trx.rollback();
- return res.status(400).send('Duplicate sex ids in detail');
- }
- }
- }
-
- const relationshipsKey =
- animalBatchKey === 'batch'
- ? 'animal_batch_use_relationships'
- : 'animal_use_relationships';
-
- if (animalOrBatch[relationshipsKey]) {
- if (!Array.isArray(animalOrBatch[relationshipsKey])) {
- return res.status(400).send(`${relationshipsKey} must be an array`);
- }
-
- const otherUse = await AnimalUseModel.query().where({ key: 'OTHER' }).first();
-
- for (const relationship of animalOrBatch[relationshipsKey]) {
- if (relationship.use_id != otherUse.id && relationship.other_use) {
- return res.status(400).send('other_use notes is for other use type');
- }
- }
- }
-
- // Skip the process if type_name and breed_name are not passed
- if (!type_name && !breed_name) {
- continue;
- }
-
- if (type_name) {
- if (default_breed_id || custom_breed_id) {
- await trx.rollback();
- return res
- .status(400)
- .send('Cannot create a new type associated with an existing breed');
- }
-
- newTypesSet.add(type_name);
- }
-
- // newBreedsSet will be used to check if the combination of type + breed exists in DB.
- // skip the process if the type is new (= type_name is passed)
- if (!type_name && breed_name) {
- const breedDetails = custom_type_id
- ? `custom_type_id/${custom_type_id}/${breed_name}`
- : `default_type_id/${default_type_id}/${breed_name}`;
-
- newBreedsSet.add(breedDetails);
- }
- }
-
- if (newTypesSet.size) {
- const record = await CustomAnimalTypeModel.getTypesByFarmAndTypes(
- farm_id,
- [...newTypesSet],
- trx,
- );
-
- if (record.length) {
- await trx.rollback();
- return res.status(409).send('Animal type already exists');
- }
- }
-
- if (newBreedsSet.size) {
- const typeBreedPairs = [...newBreedsSet].map((breed) => breed.split('/'));
- const record = await CustomAnimalBreedModel.getBreedsByFarmAndTypeBreedPairs(
- farm_id,
- typeBreedPairs,
- trx,
- );
-
- if (record.length) {
- await trx.rollback();
- return res.status(409).send('Animal breed already exists');
- }
- }
-
- await trx.commit();
- next();
- } catch (error) {
- handleObjectionError(error, res, trx);
- }
- };
-}
diff --git a/packages/api/src/middleware/validation/checkAnimalOrBatch.js b/packages/api/src/middleware/validation/checkAnimalOrBatch.js
index 69d46aea68..d3f497f39e 100644
--- a/packages/api/src/middleware/validation/checkAnimalOrBatch.js
+++ b/packages/api/src/middleware/validation/checkAnimalOrBatch.js
@@ -13,58 +13,420 @@
* GNU General Public License for more details, see .
*/
+import { Model, transaction } from 'objection';
+import { handleObjectionError } from '../../util/errorCodes.js';
+
import AnimalModel from '../../models/animalModel.js';
import AnimalBatchModel from '../../models/animalBatchModel.js';
+import CustomAnimalTypeModel from '../../models/customAnimalTypeModel.js';
+import DefaultAnimalBreedModel from '../../models/defaultAnimalBreedModel.js';
+import CustomAnimalBreedModel from '../../models/customAnimalBreedModel.js';
+import AnimalUseModel from '../../models/animalUseModel.js';
const AnimalOrBatchModel = {
animal: AnimalModel,
batch: AnimalBatchModel,
};
-export function checkEditAnimalOrBatch(animalOrBatchKey) {
+// Utils
+const hasMultipleValues = (values) => {
+ const nonNullValues = values.filter(Boolean);
+ return !(nonNullValues.length === 1);
+};
+
+const checkIdIsNumber = (id) => {
+ if (!id || isNaN(Number(id))) {
+ throw newCustomError('Must send valid ids');
+ }
+};
+
+const newCustomError = (message, code = 400, body = undefined) => {
+ const error = new Error(message);
+ error.code = code;
+ error.body = body;
+ error.type = 'LiteFarmCustom';
+ return error;
+};
+
+// Body checks
+const checkIsArray = (array, descriptiveErrorText = '') => {
+ if (!Array.isArray(array)) {
+ throw newCustomError(`${descriptiveErrorText} should be an array`);
+ }
+};
+
+const checkValidAnimalOrBatchIds = async (animalOrBatchKey, ids, farm_id, trx) => {
+ if (!ids || !ids.length) {
+ throw newCustomError('Must send ids');
+ }
+
+ const idsSet = new Set(ids.split(','));
+
+ // Check that all animals/batches exist and belong to the farm
+ const invalidIds = [];
+
+ for (const id of idsSet) {
+ // For query syntax like ids=,,, which will pass the above check
+ checkIdIsNumber(id);
+
+ const existingRecord = await AnimalOrBatchModel[animalOrBatchKey]
+ .query(trx)
+ .findById(id)
+ .where({ farm_id })
+ .whereNotDeleted(); // prohibiting re-delete
+
+ if (!existingRecord) {
+ invalidIds.push(id);
+ }
+ }
+
+ if (invalidIds.length) {
+ throw newCustomError(
+ 'Some entities do not exist, are already deleted, or are not associated with the given farm.',
+ 400,
+ { error: 'Invalid ids', invalidIds },
+ );
+ }
+};
+
+// AnimalOrBatch checks
+const checkExactlyOneAnimalTypeProvided = (default_type_id, custom_type_id, type_name) => {
+ if (hasMultipleValues([default_type_id, custom_type_id, type_name])) {
+ throw newCustomError(
+ 'Exactly one of default_type_id, custom_type_id, or type_name must be sent',
+ );
+ }
+};
+
+const checkRecordBelongsToFarm = async (record, farm_id, descriptiveErrorMessage) => {
+ if (record && record.farm_id !== farm_id) {
+ throw newCustomError(`Forbidden ${descriptiveErrorMessage} does not belong to this farm`, 403);
+ }
+};
+
+// For edit mode set required to false
+const checksIfTypeProvided = async (animalOrBatch, farm_id, required = true) => {
+ const { default_type_id, custom_type_id, type_name } = animalOrBatch;
+ if (default_type_id || custom_type_id || type_name || required) {
+ checkExactlyOneAnimalTypeProvided(default_type_id, custom_type_id, type_name);
+ }
+ if (custom_type_id) {
+ const customType = await CustomAnimalTypeModel.query().findById(custom_type_id);
+ if (!customType) {
+ // TODO : new error add test
+ throw newCustomError('Custom type does not exist');
+ }
+ await checkRecordBelongsToFarm(customType, farm_id, 'custom type');
+ }
+ if (type_name) {
+ // TODO: Check type_name does not already exist or replace custom type id?
+ }
+};
+
+const checkExactlyOneAnimalBreedProvided = (default_breed_id, custom_breed_id, breed_name) => {
+ if (hasMultipleValues([default_breed_id, custom_breed_id, breed_name])) {
+ throw newCustomError(
+ 'Exactly one of default_breed_id, custom_breed_id and breed_name must be sent',
+ );
+ }
+};
+
+const checkDefaultBreedMatchesType = async (
+ animalOrBatchRecord,
+ default_breed_id,
+ default_type_id,
+) => {
+ let defaultTypeId = default_type_id;
+ // If not editing type, check record type
+ if (!defaultTypeId && animalOrBatchRecord) {
+ defaultTypeId = animalOrBatchRecord.default_type_id;
+ }
+ if (defaultTypeId) {
+ const defaultBreed = await DefaultAnimalBreedModel.query().findById(default_breed_id);
+ if (defaultBreed && defaultBreed.default_type_id !== defaultTypeId) {
+ throw newCustomError('Breed does not match type');
+ }
+ } else {
+ // TODO: new error untested should prevents need for pre-existing checkDefaultBreedDoesNotUseCustomType
+ throw newCustomError('Default breed must use default type');
+ }
+};
+
+const checkCustomBreedMatchesType = async (
+ animalOrBatchRecord,
+ customBreed,
+ default_type_id,
+ custom_type_id,
+) => {
+ let defaultTypeId = default_type_id;
+ let customTypeId = custom_type_id;
+ // If not editing type, check record type
+ if (!defaultTypeId && !customTypeId && animalOrBatchRecord) {
+ defaultTypeId = animalOrBatchRecord.default_type_id;
+ customTypeId = animalOrBatchRecord.custom_type_id;
+ }
+
+ if (customBreed.default_type_id && customBreed.default_type_id !== defaultTypeId) {
+ throw newCustomError('Breed does not match type');
+ }
+
+ if (customBreed.custom_type_id && customBreed.custom_type_id !== customTypeId) {
+ throw newCustomError('Breed does not match type');
+ }
+};
+
+const checksIfBreedProvided = async (animalOrBatch, farm_id, animalOrBatchRecord = undefined) => {
+ const {
+ default_breed_id,
+ custom_breed_id,
+ breed_name,
+ default_type_id,
+ custom_type_id,
+ } = animalOrBatch;
+ if (default_breed_id || custom_breed_id || breed_name) {
+ checkExactlyOneAnimalBreedProvided(default_breed_id, custom_breed_id, breed_name);
+ }
+ if (default_breed_id) {
+ await checkDefaultBreedMatchesType(animalOrBatchRecord, default_breed_id, default_type_id);
+ }
+ if (custom_breed_id) {
+ const customBreed = await CustomAnimalBreedModel.query()
+ .whereNotDeleted()
+ .findById(custom_breed_id);
+ if (!customBreed) {
+ // TODO : new error add test
+ throw newCustomError('Custom breed does not exist');
+ }
+ await checkRecordBelongsToFarm(customBreed, farm_id, 'custom breed');
+ await checkCustomBreedMatchesType(
+ animalOrBatchRecord,
+ customBreed,
+ default_type_id,
+ custom_type_id,
+ );
+ }
+ if (breed_name) {
+ // TODO: Check breed_name does not already exist or replace custom type id?
+ }
+};
+
+const checkBatchSexDetail = async (
+ animalOrBatch,
+ animalOrBatchKey,
+ animalOrBatchRecord = undefined,
+) => {
+ if (animalOrBatchKey === 'batch') {
+ let count = animalOrBatch.count;
+ let sexDetail = animalOrBatch.sex_detail;
+ if (!count) {
+ count = animalOrBatchRecord.count;
+ }
+ if (!sexDetail) {
+ sexDetail = animalOrBatchRecord.sex_detail;
+ }
+ if (sexDetail?.length) {
+ let sexCount = 0;
+ const sexIdSet = new Set();
+ sexDetail.forEach((detail) => {
+ sexCount += detail.count;
+ sexIdSet.add(detail.sex_id);
+ });
+ if (sexCount > count) {
+ throw newCustomError('Batch count must be greater than or equal to sex detail count');
+ }
+ if (sexDetail.length != sexIdSet.size) {
+ throw newCustomError('Duplicate sex ids in detail');
+ }
+ }
+ }
+};
+
+const checkAnimalUseRelationship = async (animalOrBatch, animalOrBatchKey) => {
+ const relationshipsKey =
+ animalOrBatchKey === 'batch' ? 'animal_batch_use_relationships' : 'animal_use_relationships';
+
+ if (animalOrBatch[relationshipsKey]) {
+ checkIsArray(animalOrBatch[relationshipsKey], relationshipsKey);
+
+ const otherUse = await AnimalUseModel.query().where({ key: 'OTHER' }).first();
+
+ for (const relationship of animalOrBatch[relationshipsKey]) {
+ if (relationship.use_id != otherUse.id && relationship.other_use) {
+ throw newCustomError('other_use notes is for other use type');
+ }
+ }
+ }
+};
+
+const checkAndAddCustomTypesOrBreeds = (animalOrBatch, newTypesSet, newBreedsSet) => {
+ const {
+ type_name,
+ breed_name,
+ custom_type_id,
+ default_type_id,
+ default_breed_id,
+ custom_breed_id,
+ } = animalOrBatch;
+ if (type_name) {
+ if (default_breed_id || custom_breed_id) {
+ throw newCustomError('Cannot create a new type associated with an existing breed');
+ }
+ newTypesSet.add(type_name);
+ }
+
+ // newBreedsSet will be used to check if the combination of type + breed exists in DB.
+ // skip the process if the type is new (= type_name is passed)
+ if (!type_name && breed_name) {
+ const breedDetails = custom_type_id
+ ? `custom_type_id/${custom_type_id}/${breed_name}`
+ : `default_type_id/${default_type_id}/${breed_name}`;
+
+ newBreedsSet.add(breedDetails);
+ }
+};
+
+const checkRemovalDataProvided = (animalOrBatch) => {
+ const { animal_removal_reason_id, removal_date } = animalOrBatch;
+ if (!animal_removal_reason_id || !removal_date) {
+ throw newCustomError('Must send reason and date of removal');
+ }
+};
+
+const getRecordIfExists = async (animalOrBatch, animalOrBatchKey, farm_id) => {
+ return await AnimalOrBatchModel[animalOrBatchKey]
+ .query()
+ .findById(animalOrBatch.id)
+ .where({ farm_id })
+ .whereNotDeleted();
+};
+
+// Post loop checks
+const checkCustomTypeAndBreedConflicts = async (newTypesSet, newBreedsSet, farm_id, trx) => {
+ if (newTypesSet.size) {
+ const record = await CustomAnimalTypeModel.getTypesByFarmAndTypes(
+ farm_id,
+ [...newTypesSet],
+ trx,
+ );
+
+ if (record.length) {
+ throw newCustomError('Animal type already exists', 409);
+ }
+ }
+
+ if (newBreedsSet.size) {
+ const typeBreedPairs = [...newBreedsSet].map((breed) => breed.split('/'));
+ const record = await CustomAnimalBreedModel.getBreedsByFarmAndTypeBreedPairs(
+ farm_id,
+ typeBreedPairs,
+ trx,
+ );
+
+ if (record.length) {
+ throw newCustomError('Animal breed already exists', 409);
+ }
+ }
+};
+
+const checkInvalidIds = async (invalidIds) => {
+ if (invalidIds.length) {
+ throw newCustomError(
+ 'Some animals or batches do not exist or are not associated with the given farm.',
+ 400,
+ { error: 'Invalid ids', invalidIds },
+ );
+ }
+};
+
+export function checkCreateAnimalOrBatch(animalOrBatchKey) {
return async (req, res, next) => {
+ const trx = await transaction.start(Model.knex());
+
try {
const { farm_id } = req.headers;
+ const newTypesSet = new Set();
+ const newBreedsSet = new Set();
+
+ for (const animalOrBatch of req.body) {
+ const { type_name, breed_name } = animalOrBatch;
- if (!Array.isArray(req.body)) {
- return res.status(400).send('Request body should be an array');
+ // also edit
+ await checksIfTypeProvided(animalOrBatch, farm_id);
+ await checksIfBreedProvided(animalOrBatch, farm_id);
+
+ await checkBatchSexDetail(animalOrBatch, animalOrBatchKey);
+ await checkAnimalUseRelationship(animalOrBatch, animalOrBatchKey);
+
+ // Skip the process if type_name and breed_name are not passed
+ if (!type_name && !breed_name) {
+ continue;
+ }
+ checkAndAddCustomTypesOrBreeds(animalOrBatch, newTypesSet, newBreedsSet);
}
+ await checkCustomTypeAndBreedConflicts(newTypesSet, newBreedsSet, farm_id, trx);
+
+ await trx.commit();
+ next();
+ } catch (error) {
+ if (error.type === 'LiteFarmCustom') {
+ console.error(error);
+ await trx.rollback();
+ return error.body
+ ? res.status(error.code).json({ body: error.body })
+ : res.status(error.code).send(error.message);
+ } else {
+ handleObjectionError(error, res, trx);
+ }
+ }
+ };
+}
+
+export function checkEditAnimalOrBatch(animalOrBatchKey) {
+ return async (req, res, next) => {
+ try {
+ const { farm_id } = req.headers;
+
+ checkIsArray(req.body, 'Request body');
// Check that all animals exist and belong to the farm
// Done in its own loop to provide a list of all invalid ids
const invalidIds = [];
for (const animalOrBatch of req.body) {
- if (!animalOrBatch.id) {
- return res.status(400).send('Must send animal or batch id');
- }
-
- const animalOrBatchRecord = await AnimalOrBatchModel[animalOrBatchKey]
- .query()
- .findById(animalOrBatch.id)
- .where({ farm_id })
- .whereNotDeleted();
-
+ checkIdIsNumber(animalOrBatch.id);
+ const animalOrBatchRecord = await getRecordIfExists(
+ animalOrBatch,
+ animalOrBatchKey,
+ farm_id,
+ );
if (!animalOrBatchRecord) {
invalidIds.push(animalOrBatch.id);
+ continue;
}
- }
- if (invalidIds.length) {
- return res.status(400).json({
- error: 'Invalid ids',
- invalidIds,
- message:
- 'Some animals or batches do not exist or are not associated with the given farm.',
- });
+ await checksIfTypeProvided(animalOrBatch, farm_id, false);
+ // nullTypesExistingOnRecord();
+ await checksIfBreedProvided(animalOrBatch, farm_id, animalOrBatchRecord);
+ // nullBreedsExistingOnRecord();
+ await checkBatchSexDetail(animalOrBatch, animalOrBatchKey, animalOrBatchRecord);
}
+ //TODO: should this error be actually in loop and not outside?
+ await checkInvalidIds(invalidIds);
+
next();
} catch (error) {
- console.error(error);
- return res.status(500).json({
- error,
- });
+ if (error.type === 'LiteFarmCustom') {
+ console.error(error);
+ return error.body
+ ? res.status(error.code).json({ ...error.body, message: error.message })
+ : res.status(error.code).send(error.message);
+ } else {
+ console.error(error);
+ return res.status(500).json({
+ error,
+ });
+ }
}
};
}
@@ -72,22 +434,87 @@ export function checkEditAnimalOrBatch(animalOrBatchKey) {
export function checkRemoveAnimalOrBatch(animalOrBatchKey) {
return async (req, res, next) => {
try {
- if (!Array.isArray(req.body)) {
- return res.status(400).send('Request body should be an array');
- }
+ const { farm_id } = req.headers;
+
+ checkIsArray(req.body, 'Request body');
+ // Check that all animals exist and belong to the farm
+ // Done in its own loop to provide a list of all invalid ids
+ const invalidIds = [];
for (const animalOrBatch of req.body) {
- const { animal_removal_reason_id, removal_date } = animalOrBatch;
- if (!animal_removal_reason_id || !removal_date) {
- return res.status(400).send('Must send reason and date of removal');
+ // Removal specific
+ checkRemovalDataProvided(animalOrBatch);
+
+ // From Edit
+ checkIdIsNumber(animalOrBatch.id);
+ const animalOrBatchRecord = await getRecordIfExists(
+ animalOrBatch,
+ animalOrBatchKey,
+ farm_id,
+ );
+ if (!animalOrBatchRecord) {
+ invalidIds.push(animalOrBatch.id);
+ continue;
}
+ // No record should skip this loop
}
- checkEditAnimalOrBatch(animalOrBatchKey)(req, res, next);
+
+ //TODO: should this error be actually in loop and not outside?
+ await checkInvalidIds(invalidIds);
+ next();
} catch (error) {
- console.error(error);
- return res.status(500).json({
- error,
- });
+ if (error.type === 'LiteFarmCustom') {
+ console.error(error);
+ return error.body
+ ? res.status(error.code).json({ ...error.body, message: error.message })
+ : res.status(error.code).send(error.message);
+ } else {
+ console.error(error);
+ return res.status(500).json({
+ error,
+ });
+ }
+ }
+ };
+}
+
+/**
+ * Middleware function to check if the provided animal entities exist and belong to the farm. The IDs must be passed as a comma-separated query string.
+ *
+ * @param {String} animalOrBatchKey - The key to choose a database model for the correct animal entity
+ * @returns {Function} - Express middleware function
+ *
+ * @example
+ * router.delete(
+ * '/',
+ * checkScope(['delete:animals']),
+ * checkDeleteAnimalOrBatch('animal'),
+ * AnimalController.deleteAnimals(),
+ * );
+ *
+ */
+export function checkDeleteAnimalOrBatch(animalOrBatchKey) {
+ return async (req, res, next) => {
+ const trx = await transaction.start(Model.knex());
+
+ try {
+ const { farm_id } = req.headers;
+ const { ids } = req.query;
+
+ await checkValidAnimalOrBatchIds(animalOrBatchKey, ids, farm_id, trx);
+
+ await trx.commit();
+ next();
+ } catch (error) {
+ if (error.type === 'LiteFarmCustom') {
+ console.error(error);
+ await trx.rollback();
+ return error.body
+ ? res.status(error.code).json({ ...error.body, message: error.message })
+ : res.status(error.code).send(error.message);
+ } else {
+ handleObjectionError(error, res, trx);
+ }
}
};
}
diff --git a/packages/api/src/routes/animalBatchRoute.js b/packages/api/src/routes/animalBatchRoute.js
index 80841b2fd6..736b3703eb 100644
--- a/packages/api/src/routes/animalBatchRoute.js
+++ b/packages/api/src/routes/animalBatchRoute.js
@@ -19,22 +19,29 @@ const router = express.Router();
import checkScope from '../middleware/acl/checkScope.js';
import hasFarmAccess from '../middleware/acl/hasFarmAccess.js';
import AnimalBatchController from '../controllers/animalBatchController.js';
-import AnimalBatchModel from '../models/animalBatchModel.js';
-import {
- checkAnimalEntities,
- validateAnimalBatchCreationBody,
-} from '../middleware/checkAnimalEntities.js';
import multerDiskUpload from '../util/fileUpload.js';
import validateFileExtension from '../middleware/validation/uploadImage.js';
-import { checkRemoveAnimalOrBatch } from '../middleware/validation/checkAnimalOrBatch.js';
+import {
+ checkRemoveAnimalOrBatch,
+ checkEditAnimalOrBatch,
+ checkCreateAnimalOrBatch,
+ checkDeleteAnimalOrBatch,
+} from '../middleware/validation/checkAnimalOrBatch.js';
router.get('/', checkScope(['get:animal_batches']), AnimalBatchController.getFarmAnimalBatches());
router.post(
'/',
checkScope(['add:animal_batches']),
- validateAnimalBatchCreationBody('batch'),
+ checkCreateAnimalOrBatch('batch'),
AnimalBatchController.addAnimalBatches(),
);
+router.patch(
+ '/',
+ checkScope(['edit:animal_batches']),
+ // Can't use hasFarmAccess because body is an array & because of non-unique id field
+ checkEditAnimalOrBatch('batch'),
+ AnimalBatchController.editAnimalBatches(),
+);
router.patch(
'/remove',
checkScope(['edit:animal_batches']),
@@ -45,7 +52,7 @@ router.patch(
router.delete(
'/',
checkScope(['delete:animal_batches']),
- checkAnimalEntities(AnimalBatchModel),
+ checkDeleteAnimalOrBatch('batch'),
AnimalBatchController.deleteAnimalBatches(),
);
router.post(
diff --git a/packages/api/src/routes/animalRoute.js b/packages/api/src/routes/animalRoute.js
index 50f869d555..4a55cb2877 100644
--- a/packages/api/src/routes/animalRoute.js
+++ b/packages/api/src/routes/animalRoute.js
@@ -19,22 +19,29 @@ const router = express.Router();
import checkScope from '../middleware/acl/checkScope.js';
import hasFarmAccess from '../middleware/acl/hasFarmAccess.js';
import AnimalController from '../controllers/animalController.js';
-import AnimalModel from '../models/animalModel.js';
-import {
- checkAnimalEntities,
- validateAnimalBatchCreationBody,
-} from '../middleware/checkAnimalEntities.js';
import multerDiskUpload from '../util/fileUpload.js';
import validateFileExtension from '../middleware/validation/uploadImage.js';
-import { checkRemoveAnimalOrBatch } from '../middleware/validation/checkAnimalOrBatch.js';
+import {
+ checkRemoveAnimalOrBatch,
+ checkEditAnimalOrBatch,
+ checkCreateAnimalOrBatch,
+ checkDeleteAnimalOrBatch,
+} from '../middleware/validation/checkAnimalOrBatch.js';
router.get('/', checkScope(['get:animals']), AnimalController.getFarmAnimals());
router.post(
'/',
checkScope(['add:animals']),
- validateAnimalBatchCreationBody(),
+ checkCreateAnimalOrBatch('animal'),
AnimalController.addAnimals(),
);
+router.patch(
+ '/',
+ checkScope(['edit:animals']),
+ checkEditAnimalOrBatch('animal'),
+ // Can't use hasFarmAccess because body is an array & because of non-unique id field
+ AnimalController.editAnimals(),
+);
router.patch(
'/remove',
checkScope(['edit:animals']),
@@ -45,7 +52,7 @@ router.patch(
router.delete(
'/',
checkScope(['delete:animals']),
- checkAnimalEntities(AnimalModel),
+ checkDeleteAnimalOrBatch('animal'),
AnimalController.deleteAnimals(),
);
router.post(
diff --git a/packages/api/src/util/animal.js b/packages/api/src/util/animal.js
index 8365b04467..5e14df22a2 100644
--- a/packages/api/src/util/animal.js
+++ b/packages/api/src/util/animal.js
@@ -14,6 +14,11 @@
*/
import knex from './knex.js';
+import baseController from '../controllers/baseController.js';
+import CustomAnimalBreedModel from '../models/customAnimalBreedModel.js';
+import CustomAnimalTypeModel from '../models/customAnimalTypeModel.js';
+import AnimalGroupModel from '../models/animalGroupModel.js';
+import { checkAndTrimString } from './util.js';
/**
* Assigns internal identifiers to records.
@@ -33,3 +38,101 @@ export const assignInternalIdentifiers = async (records, kind) => {
}),
);
};
+
+/**
+ * Asynchronously checks if the given animal or batch has a type or breed already stored in the database.
+ * If not, it adds the type and/or breed to the database, updates the corresponding IDs, and removes
+ * the type_name or breed_name properties from the animal or batch object.
+ *
+ * @param {Object} req - The request object, containing the body with type and breed maps.
+ * @param {Object} animalOrBatch - The animal or batch object that contains type_name or breed_name properties.
+ * @param {number} farm_id - The ID of the farm to associate with the type or breed.
+ * @param {Object} trx - A transaction object for performing the database operations within a transaction.
+ *
+ * @returns {Promise} - A promise that resolves when the type and breed IDs have been added/updated and the object has been modified.
+ *
+ * @throws {Error} - If any database operation fails.
+ */
+export const checkAndAddCustomTypeAndBreed = async (req, animalOrBatch, farm_id, trx) => {
+ // Avoid attempts to add an already created type or breed to the DB
+ // where multiple animals have the same type_name or breed_name
+ const { typeIdsMap, typeBreedIdsMap } = req.body;
+
+ if (animalOrBatch.type_name) {
+ let typeId = typeIdsMap[animalOrBatch.type_name];
+
+ if (!typeId) {
+ const newType = await baseController.postWithResponse(
+ CustomAnimalTypeModel,
+ { type: animalOrBatch.type_name, farm_id },
+ req,
+ { trx },
+ );
+ typeId = newType.id;
+ typeIdsMap[animalOrBatch.type_name] = typeId;
+ }
+ animalOrBatch.custom_type_id = typeId;
+ delete animalOrBatch.type_name;
+ }
+
+ if (animalOrBatch.breed_name) {
+ const typeColumn = animalOrBatch.default_type_id ? 'default_type_id' : 'custom_type_id';
+ const typeId = animalOrBatch.type_name
+ ? typeIdsMap[animalOrBatch.type_name]
+ : animalOrBatch.default_type_id || animalOrBatch.custom_type_id;
+ const typeBreedKey = `${typeColumn}_${typeId}_${animalOrBatch.breed_name}`;
+ let breedId = typeBreedIdsMap[typeBreedKey];
+
+ if (!breedId) {
+ const newBreed = await baseController.postWithResponse(
+ CustomAnimalBreedModel,
+ { farm_id, [typeColumn]: typeId, breed: animalOrBatch.breed_name },
+ req,
+ { trx },
+ );
+ breedId = newBreed.id;
+ typeBreedIdsMap[typeBreedKey] = breedId;
+ }
+ animalOrBatch.custom_breed_id = breedId;
+ delete animalOrBatch.breed_name;
+ }
+};
+
+/**
+ * Asynchronously checks if the specified group exists in the database for the given farm.
+ * If the group doesn't exist, it creates a new group and associates it with the animal or batch.
+ * The function then adds the group ID to the `group_ids` property of the animal or batch object and removes the `group_name` property.
+ *
+ * @param {Object} req - The request object.
+ * @param {Object} animalOrBatch - The animal or batch object that contains a group_name property.
+ * @param {number} farm_id - The ID of the farm to associate with the group.
+ * @param {Object} trx - A transaction object for performing the database operations within a transaction.
+ *
+ * @returns {Promise} - A promise that resolves when the group has been added or found and the object has been modified.
+ *
+ * @throws {Error} - If any database operation fails.
+ */
+export const checkAndAddGroup = async (req, animalOrBatch, farm_id, trx) => {
+ const groupName = checkAndTrimString(animalOrBatch.group_name);
+ delete animalOrBatch.group_name;
+
+ if (groupName) {
+ let group = await baseController.existsInTable(trx, AnimalGroupModel, {
+ name: groupName,
+ farm_id,
+ deleted: false,
+ });
+
+ if (!group) {
+ group = await baseController.postWithResponse(
+ AnimalGroupModel,
+ { name: groupName, farm_id },
+ req,
+ { trx },
+ );
+ }
+ // Frontend only allows addition of one group at a time
+ // TODO: handle multiple group additions
+ animalOrBatch.group_ids = [{ animal_group_id: group.id }];
+ }
+};
diff --git a/packages/api/tests/animal.test.js b/packages/api/tests/animal.test.js
index 572e02c1e6..f14d64ba73 100644
--- a/packages/api/tests/animal.test.js
+++ b/packages/api/tests/animal.test.js
@@ -14,7 +14,6 @@
*/
import chai from 'chai';
-import util from 'util';
import { faker } from '@faker-js/faker';
import chaiHttp from 'chai-http';
@@ -58,29 +57,24 @@ describe('Animal Tests', () => {
animalRemovalReasonId = animalRemovalReason.id;
});
- function getRequest({ user_id = newOwner.user_id, farm_id = farm.farm_id }, callback) {
- chai
+ async function getRequest({ user_id = newOwner.user_id, farm_id = farm.farm_id }) {
+ return await chai
.request(server)
.get('/animals')
.set('user_id', user_id)
- .set('farm_id', farm_id)
- .end(callback);
+ .set('farm_id', farm_id);
}
- const getRequestAsPromise = util.promisify(getRequest);
-
- function postRequest({ user_id = newOwner.user_id, farm_id = farm.farm_id }, data, callback) {
- chai
+ async function postRequest({ user_id = newOwner.user_id, farm_id = farm.farm_id }, data) {
+ return await chai
.request(server)
.post('/animals')
+ .set('Content-Type', 'application/json')
.set('user_id', user_id)
.set('farm_id', farm_id)
- .send(data)
- .end(callback);
+ .send(data);
}
- const postRequestAsPromise = util.promisify(postRequest);
-
async function removeRequest({ user_id = newOwner.user_id, farm_id = farm.farm_id }, data) {
return await chai
.request(server)
@@ -114,7 +108,7 @@ describe('Animal Tests', () => {
return { ...mocks.fakeUserFarm(), role_id: role };
}
- async function returnUserFarms(role) {
+ async function returnUserFarms(role, farm = undefined) {
const [mainFarm] = await mocks.farmFactory();
const [user] = await mocks.usersFactory();
@@ -168,7 +162,7 @@ describe('Animal Tests', () => {
// Create a third animal belonging to a different farm
await makeAnimal(secondFarm);
- const res = await getRequestAsPromise({
+ const res = await getRequest({
user_id: user.user_id,
farm_id: mainFarm.farm_id,
});
@@ -181,13 +175,13 @@ describe('Animal Tests', () => {
});
expect({
...firstAnimal,
- internal_identifier: 1,
+ internal_identifier: res.body[0].internal_identifier,
group_ids: [],
animal_use_relationships: [],
}).toMatchObject(res.body[0]);
expect({
...secondAnimal,
- internal_identifier: 2,
+ internal_identifier: res.body[1].internal_identifier,
group_ids: [],
animal_use_relationships: [],
}).toMatchObject(res.body[1]);
@@ -199,7 +193,7 @@ describe('Animal Tests', () => {
await makeAnimal(mainFarm);
const [unAuthorizedUser] = await mocks.usersFactory();
- const res = await getRequestAsPromise({
+ const res = await getRequest({
user_id: unAuthorizedUser.user_id,
farm_id: mainFarm.farm_id,
});
@@ -229,7 +223,7 @@ describe('Animal Tests', () => {
custom_breed_id: animalBreed.id,
});
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -253,7 +247,7 @@ describe('Animal Tests', () => {
const { mainFarm, user } = await returnUserFarms(role);
const animal = mocks.fakeAnimal();
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -270,7 +264,7 @@ describe('Animal Tests', () => {
const { mainFarm, user } = await returnUserFarms(1);
const animal = mocks.fakeAnimal();
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -293,9 +287,7 @@ describe('Animal Tests', () => {
farm_id: farm.farm_id,
default_type_id: defaultTypeId,
});
- const res = await postRequestAsPromise({ user_id: user.user_id, farm_id: farm.farm_id }, [
- animal,
- ]);
+ const res = await postRequest({ user_id: user.user_id, farm_id: farm.farm_id }, [animal]);
expect(res.body[0].internal_identifier).toBe(animalCount + batchCount + 1);
}
@@ -308,7 +300,7 @@ describe('Animal Tests', () => {
custom_type_id: null,
});
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -331,7 +323,7 @@ describe('Animal Tests', () => {
custom_type_id: animalType.id,
});
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -356,7 +348,7 @@ describe('Animal Tests', () => {
custom_breed_id: animalBreed.id,
});
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -375,7 +367,7 @@ describe('Animal Tests', () => {
default_breed_id: animalBreed.id,
});
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -397,10 +389,7 @@ describe('Animal Tests', () => {
});
const postAnimalsRequest = async (animals) => {
- const res = await postRequestAsPromise(
- { user_id: owner.user_id, farm_id: farm.farm_id },
- animals,
- );
+ const res = await postRequest({ user_id: owner.user_id, farm_id: farm.farm_id }, animals);
return res;
};
@@ -667,7 +656,7 @@ describe('Animal Tests', () => {
return mocks.fakeAnimal(group_name ? { ...data, group_name } : data);
});
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -729,6 +718,283 @@ describe('Animal Tests', () => {
});
});
+ // EDIT tests
+ describe('Edit animal tests', () => {
+ let animalGroup1;
+ let animalGroup2;
+ let animalSex;
+ let animalIdentifierColor;
+ let animalIdentifierType;
+ let animalOrigin;
+ let animalRemovalReason;
+ let animalUse1;
+ let animalUse2;
+ let animalUse3;
+
+ beforeEach(async () => {
+ [animalGroup1] = await mocks.animal_groupFactory();
+ [animalGroup2] = await mocks.animal_groupFactory();
+ // Populate enums
+ [animalSex] = await mocks.animal_sexFactory();
+ [animalIdentifierColor] = await mocks.animal_identifier_colorFactory();
+ [animalIdentifierType] = await mocks.animal_identifier_typeFactory();
+ [animalOrigin] = await mocks.animal_originFactory();
+ [animalRemovalReason] = await mocks.animal_removal_reasonFactory();
+ [animalUse1] = await mocks.animal_useFactory('OTHER');
+ [animalUse2] = await mocks.animal_useFactory();
+ [animalUse3] = await mocks.animal_useFactory();
+ });
+
+ async function addAnimals(mainFarm, user) {
+ const [customAnimalType] = await mocks.custom_animal_typeFactory({
+ promisedFarm: [mainFarm],
+ });
+
+ // Create two animals, one with a default type and one with a custom type
+ const firstAnimal = mocks.fakeAnimal({
+ name: 'edit test 1',
+ default_type_id: defaultTypeId,
+ animal_use_relationships: [{ use_id: animalUse1.id }],
+ sire: 'Unchanged',
+ group_name: animalGroup1.name,
+ });
+ const secondAnimal = mocks.fakeAnimal({
+ name: 'edit test 2',
+ custom_type_id: customAnimalType.id,
+ animal_use_relationships: [{ use_id: animalUse1.id }],
+ sire: 'Unchanged',
+ });
+
+ const res = await postRequest(
+ {
+ user_id: user.user_id,
+ farm_id: mainFarm.farm_id,
+ },
+ [firstAnimal, secondAnimal],
+ );
+
+ const returnedFirstAnimal = res.body?.find((animal) => animal.name === 'edit test 1');
+ const returnedSecondAnimal = res.body?.find((animal) => animal.name === 'edit test 2');
+
+ return { res, returnedFirstAnimal, returnedSecondAnimal };
+ }
+
+ async function editAnimals(mainFarm, user, returnedFirstAnimal, returnedSecondAnimal) {
+ const [customAnimalType] = await mocks.custom_animal_typeFactory({
+ promisedFarm: [mainFarm],
+ });
+
+ // Make edits to animals - does not test all top level animal columns, but all relationships
+ const updatedFirstAnimal = mocks.fakeAnimal({
+ // should fail
+ extra_non_existant_property: 'hello',
+ id: returnedFirstAnimal.id,
+ default_type_id: defaultTypeId,
+ name: 'Update Name 1',
+ sire: returnedFirstAnimal.sire,
+ sex_id: animalSex.id,
+ identifier: '2',
+ identifier_color_id: animalIdentifierColor.id,
+ origin_id: animalOrigin.id,
+ // should fail
+ animal_removal_reason_id: animalRemovalReason.id,
+ identifier_type_id: animalIdentifierType.id,
+ organic_status: 'Organic',
+ animal_use_relationships: [{ use_id: animalUse2.id }, { use_id: animalUse3.id }],
+ group_ids: [{ animal_group_id: animalGroup2.id }],
+ });
+ const updatedSecondAnimal = mocks.fakeAnimal({
+ id: returnedSecondAnimal.id,
+ custom_type_id: customAnimalType.id,
+ name: 'Update Name 1',
+ sire: returnedSecondAnimal.sire,
+ sex_id: animalSex.id,
+ identifier: '2',
+ identifier_color_id: animalIdentifierColor.id,
+ origin_id: animalOrigin.id,
+ // should fail
+ animal_removal_reason_id: animalRemovalReason.id,
+ identifier_type_id: animalIdentifierType.id,
+ organic_status: 'Organic',
+ animal_use_relationships: [{ use_id: animalUse2.id }, { use_id: animalUse3.id }],
+ group_ids: [{ animal_group_id: animalGroup2.id }],
+ });
+
+ const patchRes = await patchRequest(
+ {
+ user_id: user.user_id,
+ farm_id: mainFarm.farm_id,
+ },
+ [updatedFirstAnimal, updatedSecondAnimal],
+ );
+
+ // Remove or add properties not actually expected from get request
+ [updatedFirstAnimal, updatedSecondAnimal].forEach((animal) => {
+ // Should not cause an error
+ delete animal.extra_non_existant_property;
+ // Should not be able to update on edit
+ animal.animal_removal_reason_id = null;
+ // Return format different than post format
+ animal.group_ids = animal.group_ids.map((groupId) => groupId.animal_group_id);
+ animal.animal_use_relationships.forEach((rel) => {
+ rel.animal_id = animal.id;
+ rel.other_use = null;
+ });
+ });
+
+ return { res: patchRes, updatedFirstAnimal, updatedSecondAnimal };
+ }
+
+ test('Admin users should be able to edit animals', async () => {
+ const roles = [1, 2, 5];
+
+ for (const role of roles) {
+ const { mainFarm, user } = await returnUserFarms(role);
+
+ // Add animals to db
+ const { res: addRes, returnedFirstAnimal, returnedSecondAnimal } = await addAnimals(
+ mainFarm,
+ user,
+ );
+ expect(addRes.status).toBe(201);
+ expect(returnedFirstAnimal).toBeTruthy();
+ expect(returnedSecondAnimal).toBeTruthy();
+
+ // Edit animals in db
+ const { res: editRes, updatedFirstAnimal, updatedSecondAnimal } = await editAnimals(
+ mainFarm,
+ user,
+ returnedFirstAnimal,
+ returnedSecondAnimal,
+ );
+ expect(editRes.status).toBe(204);
+
+ // Get updated animals
+ const { body: animalRecords } = await getRequest({
+ user_id: user.user_id,
+ farm_id: mainFarm.farm_id,
+ });
+ const filteredAnimalRecords = animalRecords.filter((record) =>
+ [returnedFirstAnimal.id, returnedSecondAnimal.id].includes(record.id),
+ );
+
+ // Test data matches expected changes
+ filteredAnimalRecords.forEach((record) => {
+ // Remove properties that were not updated
+ delete record.internal_identifier;
+ // Remove base properties
+ delete record.created_at;
+ delete record.created_by_user_id;
+ delete record.deleted;
+ delete record.updated_at;
+ delete record.updated_by;
+ const updatedRecord = [updatedFirstAnimal, updatedSecondAnimal].find(
+ (animal) => animal.id === record.id,
+ );
+ expect(record).toMatchObject(updatedRecord);
+ });
+ }
+ });
+
+ test('Non-admin users should not be able to edit animals', async () => {
+ const adminRole = 1;
+ const { mainFarm, user: admin } = await returnUserFarms(adminRole);
+ const workerRole = 3;
+ const [user] = await mocks.usersFactory();
+ await mocks.userFarmFactory(
+ {
+ promisedUser: [user],
+ promisedFarm: [mainFarm],
+ },
+ fakeUserFarm(workerRole),
+ );
+
+ // Add animals to db
+ const { res: addRes, returnedFirstAnimal, returnedSecondAnimal } = await addAnimals(
+ mainFarm,
+ admin,
+ );
+ expect(addRes.status).toBe(201);
+ expect(returnedFirstAnimal).toBeTruthy();
+ expect(returnedSecondAnimal).toBeTruthy();
+
+ // Edit animals in db
+ const { res: editRes } = await editAnimals(
+ mainFarm,
+ user,
+ returnedFirstAnimal,
+ returnedSecondAnimal,
+ );
+
+ // Test failure
+ expect(editRes.status).toBe(403);
+ expect(editRes.error.text).toBe(
+ 'User does not have the following permission(s): edit:animals',
+ );
+ });
+
+ test('Should not be able to send out an individual animal instead of an array', async () => {
+ const { mainFarm, user } = await returnUserFarms(1);
+
+ // Add animals to db
+ const { res: addRes, returnedFirstAnimal } = await addAnimals(mainFarm, user);
+ expect(addRes.status).toBe(201);
+ expect(returnedFirstAnimal).toBeTruthy();
+
+ // Change 1 thing
+ returnedFirstAnimal.sire = 'Changed';
+
+ const res = await patchRequest(
+ {
+ user_id: user.user_id,
+ farm_id: mainFarm.farm_id,
+ },
+ {
+ ...returnedFirstAnimal,
+ },
+ );
+
+ // Test for failure
+ expect(res.status).toBe(400);
+ expect(res.error.text).toBe('Request body should be an array');
+ });
+
+ test('Should not be able to edit an animal belonging to a different farm', async () => {
+ const { mainFarm, user } = await returnUserFarms(1);
+ const [secondFarm] = await mocks.farmFactory();
+
+ const animal = await makeAnimal(secondFarm, {
+ default_type_id: defaultTypeId,
+ });
+
+ const res = await patchRequest(
+ {
+ user_id: user.user_id,
+ farm_id: mainFarm.farm_id,
+ },
+ [
+ {
+ id: animal.id,
+ sire: 'Neighbours sire',
+ },
+ ],
+ );
+
+ expect(res).toMatchObject({
+ status: 400,
+ body: {
+ error: 'Invalid ids',
+ invalidIds: [animal.id],
+ },
+ });
+
+ // Check database
+ const animalRecord = await AnimalModel.query().findById(animal.id);
+ expect(animalRecord.sire).toBeNull();
+ });
+ });
+
+ // REMOVE tests
describe('Remove animal tests', () => {
test('Admin users should be able to remove animals', async () => {
const roles = [1, 2, 5];
@@ -828,7 +1094,6 @@ describe('Animal Tests', () => {
user_id: user.user_id,
farm_id: mainFarm.farm_id,
},
-
{
id: animal.id,
animal_removal_reason_id: animalRemovalReasonId,
@@ -837,12 +1102,8 @@ describe('Animal Tests', () => {
},
);
- expect(res).toMatchObject({
- status: 400,
- error: {
- text: 'Request body should be an array',
- },
- });
+ expect(res.status).toBe(400);
+ expect(res.error.text).toBe('Request body should be an array');
});
test('Should not be able to remove an animal without providng a removal_date', async () => {
@@ -865,7 +1126,6 @@ describe('Animal Tests', () => {
},
],
);
-
expect(res.status).toBe(400);
expect(res.error.text).toBe('Must send reason and date of removal');
diff --git a/packages/api/tests/animal_batch.test.js b/packages/api/tests/animal_batch.test.js
index c957414412..2c2528040c 100644
--- a/packages/api/tests/animal_batch.test.js
+++ b/packages/api/tests/animal_batch.test.js
@@ -14,7 +14,6 @@
*/
import chai from 'chai';
-import util from 'util';
import { faker } from '@faker-js/faker';
import chaiHttp from 'chai-http';
@@ -57,29 +56,23 @@ describe('Animal Batch Tests', () => {
animalRemovalReasonId = animalRemovalReason.id;
});
- function getRequest({ user_id = newOwner.user_id, farm_id = farm.farm_id }, callback) {
- chai
+ async function getRequest({ user_id = newOwner.user_id, farm_id = farm.farm_id }) {
+ return await chai
.request(server)
.get('/animal_batches')
.set('user_id', user_id)
- .set('farm_id', farm_id)
- .end(callback);
+ .set('farm_id', farm_id);
}
- const getRequestAsPromise = util.promisify(getRequest);
-
- function postRequest({ user_id = newOwner.user_id, farm_id = farm.farm_id }, data, callback) {
- chai
+ async function postRequest({ user_id = newOwner.user_id, farm_id = farm.farm_id }, data) {
+ return await chai
.request(server)
.post('/animal_batches')
.set('user_id', user_id)
.set('farm_id', farm_id)
- .send(data)
- .end(callback);
+ .send(data);
}
- const postRequestAsPromise = util.promisify(postRequest);
-
async function removeRequest({ user_id = newOwner.user_id, farm_id = farm.farm_id }, data) {
return await chai
.request(server)
@@ -189,7 +182,7 @@ describe('Animal Batch Tests', () => {
default_type_id: defaultTypeId,
});
- const res = await getRequestAsPromise({
+ const res = await getRequest({
user_id: user.user_id,
farm_id: mainFarm.farm_id,
});
@@ -224,7 +217,7 @@ describe('Animal Batch Tests', () => {
});
const [unAuthorizedUser] = await mocks.usersFactory();
- const res = await getRequestAsPromise({
+ const res = await getRequest({
user_id: unAuthorizedUser.user_id,
farm_id: mainFarm.farm_id,
});
@@ -274,7 +267,7 @@ describe('Animal Batch Tests', () => {
],
});
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -303,7 +296,7 @@ describe('Animal Batch Tests', () => {
default_type_id: defaultTypeId,
});
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -325,7 +318,7 @@ describe('Animal Batch Tests', () => {
default_type_id: defaultTypeId,
});
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -348,7 +341,7 @@ describe('Animal Batch Tests', () => {
farm_id: farm.farm_id,
default_type_id: defaultTypeId,
});
- const res = await postRequestAsPromise({ user_id: user.user_id, farm_id: farm.farm_id }, [
+ const res = await postRequest({ user_id: user.user_id, farm_id: farm.farm_id }, [
animalBatch,
]);
@@ -364,7 +357,7 @@ describe('Animal Batch Tests', () => {
custom_type_id: null,
});
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -387,7 +380,7 @@ describe('Animal Batch Tests', () => {
custom_type_id: animalType.id,
});
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -412,7 +405,7 @@ describe('Animal Batch Tests', () => {
custom_breed_id: animalBreed.id,
});
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -433,7 +426,7 @@ describe('Animal Batch Tests', () => {
default_breed_id: animalBreed.id,
});
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -464,7 +457,7 @@ describe('Animal Batch Tests', () => {
],
});
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
@@ -486,7 +479,7 @@ describe('Animal Batch Tests', () => {
});
const postAnimalBatchesRequest = async (animalBatches) => {
- const res = await postRequestAsPromise(
+ const res = await postRequest(
{ user_id: owner.user_id, farm_id: farm.farm_id },
animalBatches,
);
@@ -690,6 +683,314 @@ describe('Animal Batch Tests', () => {
});
});
+ // EDIT tests
+ describe('Edit animal batch tests', () => {
+ let animalGroup1;
+ let animalGroup2;
+ let animalSex1;
+ let animalSex2;
+ let animalIdentifierColor;
+ let animalIdentifierType;
+ let animalOrigin;
+ let animalRemovalReason;
+ let animalUse1;
+ let animalUse2;
+ let animalUse3;
+
+ beforeEach(async () => {
+ [animalGroup1] = await mocks.animal_groupFactory();
+ [animalGroup2] = await mocks.animal_groupFactory();
+ // Populate enums
+ [animalSex1] = await mocks.animal_sexFactory();
+ [animalSex2] = await mocks.animal_sexFactory();
+ [animalIdentifierColor] = await mocks.animal_identifier_colorFactory();
+ [animalIdentifierType] = await mocks.animal_identifier_typeFactory();
+ [animalOrigin] = await mocks.animal_originFactory();
+ [animalRemovalReason] = await mocks.animal_removal_reasonFactory();
+ [animalUse1] = await mocks.animal_useFactory('OTHER');
+ [animalUse2] = await mocks.animal_useFactory();
+ [animalUse3] = await mocks.animal_useFactory();
+ });
+
+ async function addAnimalBatches(mainFarm, user) {
+ const [customAnimalType] = await mocks.custom_animal_typeFactory({
+ promisedFarm: [mainFarm],
+ });
+
+ // Create two batchess, one with a default type and one with a custom type
+ const firstBatch = mocks.fakeAnimalBatch({
+ name: 'edit test 1',
+ default_type_id: defaultTypeId,
+ animal_batch_use_relationships: [{ use_id: animalUse1.id }],
+ sire: 'Unchanged',
+ count: 4,
+ sex_detail: [
+ {
+ sex_id: animalSex1.id,
+ count: 2,
+ },
+ {
+ sex_id: animalSex2.id,
+ count: 2,
+ },
+ ],
+ group_name: animalGroup1.name,
+ });
+ const secondBatch = mocks.fakeAnimalBatch({
+ name: 'edit test 2',
+ custom_type_id: customAnimalType.id,
+ animal_batch_use_relationships: [{ use_id: animalUse1.id }],
+ sire: 'Unchanged',
+ count: 5,
+ });
+
+ const res = await postRequest(
+ {
+ user_id: user.user_id,
+ farm_id: mainFarm.farm_id,
+ },
+ [firstBatch, secondBatch],
+ );
+
+ const returnedFirstBatch = res.body?.find((batch) => batch.name === 'edit test 1');
+ const returnedSecondBatch = res.body?.find((batch) => batch.name === 'edit test 2');
+
+ return { res, returnedFirstBatch, returnedSecondBatch };
+ }
+
+ async function editAnimalBatches(mainFarm, user, returnedFirstBatch, returnedSecondBatch) {
+ const [customAnimalType] = await mocks.custom_animal_typeFactory({
+ promisedFarm: [mainFarm],
+ });
+
+ // Make edits to batches - does not test all top level batch columns, but all relationships
+ const updatedFirstBatch = mocks.fakeAnimalBatch({
+ // should fail
+ extra_non_existant_property: 'hello',
+ id: returnedFirstBatch.id,
+ default_type_id: defaultTypeId,
+ name: 'Update Name 1',
+ sire: returnedFirstBatch.sire,
+ sex_detail: [
+ {
+ id: returnedFirstBatch.sex_detail.find((detail) => detail.sex_id === animalSex1.id)?.id,
+ animal_batch_id: returnedFirstBatch.id,
+ sex_id: animalSex1.id,
+ count: 2,
+ },
+ {
+ id: returnedFirstBatch.sex_detail.find((detail) => detail.sex_id === animalSex2.id)?.id,
+ animal_batch_id: returnedFirstBatch.id,
+ sex_id: animalSex2.id,
+ count: 3,
+ },
+ ],
+ count: 5,
+ origin_id: animalOrigin.id,
+ // should fail
+ animal_removal_reason_id: animalRemovalReason.id,
+ organic_status: 'Organic',
+ animal_batch_use_relationships: [{ use_id: animalUse2.id }, { use_id: animalUse3.id }],
+ group_ids: [{ animal_group_id: animalGroup2.id }],
+ });
+ const updatedSecondBatch = mocks.fakeAnimalBatch({
+ id: returnedSecondBatch.id,
+ custom_type_id: customAnimalType.id,
+ name: 'Update Name 1',
+ sire: returnedSecondBatch.sire,
+ sex_detail: [
+ {
+ sex_id: animalSex1.id,
+ count: 2,
+ },
+ {
+ sex_id: animalSex2.id,
+ count: 3,
+ },
+ ],
+ count: 5,
+ origin_id: animalOrigin.id,
+ // should fail
+ animal_removal_reason_id: animalRemovalReason.id,
+ organic_status: 'Organic',
+ animal_batch_use_relationships: [{ use_id: animalUse2.id }, { use_id: animalUse3.id }],
+ group_ids: [{ animal_group_id: animalGroup2.id }],
+ });
+
+ const patchRes = await patchRequest(
+ {
+ user_id: user.user_id,
+ farm_id: mainFarm.farm_id,
+ },
+ [updatedFirstBatch, updatedSecondBatch],
+ );
+
+ // Remove or add properties not actually expected from get request
+ [updatedFirstBatch, updatedSecondBatch].forEach((batch) => {
+ // Should not cause an error
+ delete batch.extra_non_existant_property;
+ // Should not be able to update on edit
+ batch.animal_removal_reason_id = null;
+ // Return format different than post format
+ batch.group_ids = batch.group_ids.map((groupId) => groupId.animal_group_id);
+ batch.animal_batch_use_relationships.forEach((rel) => {
+ rel.animal_batch_id = batch.id;
+ rel.other_use = null;
+ });
+ });
+
+ return { res: patchRes, updatedFirstBatch, updatedSecondBatch };
+ }
+
+ test('Admin users should be able to edit batches', async () => {
+ const roles = [1, 2, 5];
+
+ for (const role of roles) {
+ const { mainFarm, user } = await returnUserFarms(role);
+
+ // Add batches to db
+ const { res: addRes, returnedFirstBatch, returnedSecondBatch } = await addAnimalBatches(
+ mainFarm,
+ user,
+ );
+ expect(addRes.status).toBe(201);
+ expect(returnedFirstBatch).toBeTruthy();
+ expect(returnedSecondBatch).toBeTruthy();
+
+ // Edit batches in db
+ const { res: editRes, updatedFirstBatch, updatedSecondBatch } = await editAnimalBatches(
+ mainFarm,
+ user,
+ returnedFirstBatch,
+ returnedSecondBatch,
+ );
+ expect(editRes.status).toBe(204);
+
+ // Get updated batches
+ const { body: batchRecords } = await getRequest({
+ user_id: user.user_id,
+ farm_id: mainFarm.farm_id,
+ });
+ const filteredBatchRecords = batchRecords.filter((record) =>
+ [returnedFirstBatch.id, returnedSecondBatch.id].includes(record.id),
+ );
+
+ // Test data matches expected changes
+ filteredBatchRecords.forEach((record) => {
+ // Remove properties that were not updated
+ delete record.internal_identifier;
+ // Remove base properties
+ delete record.created_at;
+ delete record.created_by_user_id;
+ delete record.deleted;
+ delete record.updated_at;
+ delete record.updated_by;
+ const updatedRecord = [updatedFirstBatch, updatedSecondBatch].find(
+ (batch) => batch.id === record.id,
+ );
+ expect(record).toMatchObject(updatedRecord);
+ });
+ }
+ });
+
+ test('Non-admin users should not be able to edit batches', async () => {
+ const adminRole = 1;
+ const { mainFarm, user: admin } = await returnUserFarms(adminRole);
+ const workerRole = 3;
+ const [user] = await mocks.usersFactory();
+ await mocks.userFarmFactory(
+ {
+ promisedUser: [user],
+ promisedFarm: [mainFarm],
+ },
+ fakeUserFarm(workerRole),
+ );
+
+ // Add animals to db
+ const { res: addRes, returnedFirstBatch, returnedSecondBatch } = await addAnimalBatches(
+ mainFarm,
+ admin,
+ );
+ expect(addRes.status).toBe(201);
+ expect(returnedFirstBatch).toBeTruthy();
+ expect(returnedSecondBatch).toBeTruthy();
+
+ // Edit animals in db
+ const { res: editRes } = await editAnimalBatches(
+ mainFarm,
+ user,
+ returnedFirstBatch,
+ returnedSecondBatch,
+ );
+
+ // Test failure
+ expect(editRes.status).toBe(403);
+ expect(editRes.error.text).toBe(
+ 'User does not have the following permission(s): edit:animal_batches',
+ );
+ });
+
+ test('Should not be able to send out an individual batch instead of an array', async () => {
+ const { mainFarm, user } = await returnUserFarms(1);
+
+ // Add animals to db
+ const { res: addRes, returnedFirstBatch } = await addAnimalBatches(mainFarm, user);
+ expect(addRes.status).toBe(201);
+ expect(returnedFirstBatch).toBeTruthy();
+
+ // Change 1 thing
+ returnedFirstBatch.sire = 'Changed';
+
+ const res = await patchRequest(
+ {
+ user_id: user.user_id,
+ farm_id: mainFarm.farm_id,
+ },
+ {
+ ...returnedFirstBatch,
+ },
+ );
+
+ expect(res.status).toBe(400);
+ expect(res.error.text).toBe('Request body should be an array');
+ });
+
+ test('Should not be able to edit a batch belonging to a different farm', async () => {
+ const { mainFarm, user } = await returnUserFarms(1);
+ const [secondFarm] = await mocks.farmFactory();
+
+ const batch = await makeAnimalBatch(secondFarm, {
+ default_type_id: defaultTypeId,
+ });
+
+ const res = await patchRequest(
+ {
+ user_id: user.user_id,
+ farm_id: mainFarm.farm_id,
+ },
+ [
+ {
+ id: batch.id,
+ sire: 'Neighbours sire',
+ },
+ ],
+ );
+
+ expect(res).toMatchObject({
+ status: 400,
+ body: {
+ error: 'Invalid ids',
+ invalidIds: [batch.id],
+ },
+ });
+
+ // Check database
+ const batchRecord = await AnimalBatchModel.query().findById(batch.id);
+ expect(batchRecord.sire).toBeNull();
+ });
+ });
+
+ // REMOVE tests
describe('Remove animal batch tests', () => {
test('Admin users should be able to remove animal batches', async () => {
const roles = [1, 2, 5];
@@ -815,12 +1116,8 @@ describe('Animal Batch Tests', () => {
},
);
- expect(res).toMatchObject({
- status: 400,
- error: {
- text: 'Request body should be an array',
- },
- });
+ expect(res.status).toBe(400);
+ expect(res.error.text).toBe('Request body should be an array');
// Check database
const batchRecord = await AnimalBatchModel.query().findById(animalBatch.id);
@@ -848,7 +1145,6 @@ describe('Animal Batch Tests', () => {
},
],
);
-
expect(res.status).toBe(400);
expect(res.error.text).toBe('Must send reason and date of removal');
diff --git a/packages/api/tests/mock.factories.js b/packages/api/tests/mock.factories.js
index 96800282a8..99720506d9 100644
--- a/packages/api/tests/mock.factories.js
+++ b/packages/api/tests/mock.factories.js
@@ -2477,8 +2477,8 @@ async function animal_removal_reasonFactory() {
return knex('animal_removal_reason').insert({ key: faker.lorem.word() }).returning('*');
}
-async function animal_useFactory() {
- return knex('animal_use').insert({ key: faker.lorem.word() }).returning('*');
+async function animal_useFactory(key = faker.lorem.word()) {
+ return knex('animal_use').insert({ key }).returning('*');
}
async function animal_type_use_relationshipFactory({
diff --git a/packages/api/tests/testEnvironment.js b/packages/api/tests/testEnvironment.js
index b6ddb36f88..6866416212 100644
--- a/packages/api/tests/testEnvironment.js
+++ b/packages/api/tests/testEnvironment.js
@@ -121,6 +121,8 @@ async function tableCleanup(knex) {
DELETE FROM "pesticide";
DELETE FROM "task_type";
DELETE FROM "farmDataSchedule";
+ DELETE FROM "animal_use_relationship";
+ DELETE FROM "animal_batch_use_relationship";
DELETE FROM "animal_group_relationship";
DELETE FROM "animal_batch_group_relationship";
DELETE FROM "animal_group";