Skip to content

Commit

Permalink
Merge pull request #3125 from LiteFarmOrg/LF-4081/Support_animal_inte…
Browse files Browse the repository at this point in the history
…rnal_identifier

LF-4081: Support animal internal identifier
  • Loading branch information
antsgar authored Feb 22, 2024
2 parents 98390d9 + 747f76b commit 86ceb47
Show file tree
Hide file tree
Showing 14 changed files with 394 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2024 LiteFarm.org
* This file is part of LiteFarm.
*
* LiteFarm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LiteFarm is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

import { fileURLToPath } from 'url';
import path, { dirname } from 'path';
import fs from 'fs';

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export const up = async function (knex) {
try {
await knex.schema.alterTable('animal', (table) => {
table.string('photo_url');
table.dropChecks(['name_identifier_check']);
});

await knex.schema.alterTable('animal_batch', (table) => {
table.string('photo_url');
table.string('name').nullable().alter();
});

// Create animal_union_batch_id_view VIEW
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const sqlFilePath = path.join(
__dirname,
'../sql/20240207214919_animal_union_batch_id_view.sql',
);
const sqlQuery = fs.readFileSync(sqlFilePath).toString();
await knex.raw(sqlQuery);
} catch (error) {
console.error('Error in migration up:', error);
throw error; // Rethrow the error to ensure the migration fails
}
};

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export const down = async (knex) => {
await knex.schema.dropView('animal_union_batch_id_view');

await knex.schema.alterTable('animal', (table) => {
table.dropColumn('photo_url');
// ?? does not work in alterTable
table.check('name IS NOT NULL OR identifier IS NOT NULL', [], 'name_identifier_check');
});

await knex.schema.alterTable('animal_batch', (table) => {
table.dropColumn('photo_url');
table.string('name').notNullable().alter();
});
};
25 changes: 25 additions & 0 deletions packages/api/db/sql/20240207214919_animal_union_batch_id_view.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
CREATE VIEW animal_union_batch_id_view AS
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY farm_id ORDER BY created_at)::INTEGER AS internal_identifier
FROM (
SELECT
id,
farm_id,
FALSE AS batch,
created_at
FROM
animal a

UNION ALL

SELECT
id,
farm_id,
TRUE AS batch,
created_at
FROM
animal_batch ab
) animal_union_batch_id_view
ORDER BY
created_at;
13 changes: 9 additions & 4 deletions packages/api/src/controllers/animalBatchController.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@ import DefaultAnimalBreedModel from '../models/defaultAnimalBreedModel.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';

const animalBatchController = {
getFarmAnimalBatches() {
return async (req, res) => {
try {
const { farm_id } = req.headers;
const rows = await AnimalBatchModel.query()
.where({ farm_id })
.whereNotDeleted()
.withGraphFetched('sex_detail');
.select('animal_batch.*', 'animal_union_batch_id_view.internal_identifier')
.where({ 'animal_batch.farm_id': farm_id })
.joinRelated('animal_union_batch_id_view')
.withGraphFetched('sex_detail')
.whereNotDeleted();
return res.status(200).send(rows);
} catch (error) {
console.error(error);
Expand Down Expand Up @@ -127,7 +130,7 @@ const animalBatchController = {
// Remove farm_id if it happens to be set in animal object since it should be obtained from header
delete animalBatch.farm_id;

const individualAnimalBatchResult = await baseController.insertGraph(
const individualAnimalBatchResult = await baseController.insertGraphWithResponse(
AnimalBatchModel,
{ ...animalBatch, farm_id },
req,
Expand All @@ -138,6 +141,8 @@ const animalBatchController = {
}

await trx.commit();

await assignInternalIdentifiers(result, 'batch');
return res.status(201).send(result);
} catch (error) {
await handleObjectionError(error, res, trx);
Expand Down
14 changes: 8 additions & 6 deletions packages/api/src/controllers/animalController.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@ import DefaultAnimalBreedModel from '../models/defaultAnimalBreedModel.js';
import CustomAnimalBreedModel from '../models/customAnimalBreedModel.js';
import DefaultAnimalTypeModel from '../models/defaultAnimalTypeModel.js';
import CustomAnimalTypeModel from '../models/customAnimalTypeModel.js';
import { assignInternalIdentifiers } from '../util/animal.js';

const animalController = {
getFarmAnimals() {
return async (req, res) => {
try {
const { farm_id } = req.headers;
const rows = await AnimalModel.query().where({ farm_id }).whereNotDeleted();
const rows = await AnimalModel.query()
.select('animal.*', 'animal_union_batch_id_view.internal_identifier')
.joinRelated('animal_union_batch_id_view')
.where({ 'animal.farm_id': farm_id })
.whereNotDeleted();
return res.status(200).send(rows);
} catch (error) {
console.error(error);
Expand All @@ -51,11 +56,6 @@ const animalController = {
}

for (const animal of req.body) {
if (!animal.identifier && !animal.name) {
await trx.rollback();
return res.status(400).send('Should send either animal name or identifier');
}

if (!animal.default_type_id && !animal.custom_type_id) {
await trx.rollback();
return res.status(400).send('Should send either default_type_id or custom_type_id');
Expand Down Expand Up @@ -147,6 +147,8 @@ const animalController = {
}

await trx.commit();

await assignInternalIdentifiers(result, 'animal');
return res.status(201).send(result);
} catch (error) {
console.error(error);
Expand Down
6 changes: 4 additions & 2 deletions packages/api/src/controllers/baseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,13 @@ export default {
.upsertGraph(data, { insertMissing: true });
},

async insertGraph(model, data, req, { trx, context = {} } = {}) {
// send back the resource that was just created
async insertGraphWithResponse(model, data, req, { trx, context = {} } = {}) {
return await model
.query(trx)
.context({ user_id: req?.auth?.user_id, ...context })
.insertGraph(removeAdditionalPropertiesWithRelations(model, data));
.insertGraph(removeAdditionalPropertiesWithRelations(model, data))
.returning('*');
},

// fetch an object and all of its related objects
Expand Down
15 changes: 13 additions & 2 deletions packages/api/src/models/animalBatchModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import Model from './baseFormatModel.js';
import baseModel from './baseModel.js';
import AnimalBatchSexDetailModel from './animalBatchSexDetailModel.js';
import animalUnionBatchIdViewModel from './animalUnionBatchIdViewModel.js';

class AnimalBatchModel extends baseModel {
static get tableName() {
Expand All @@ -32,7 +33,7 @@ class AnimalBatchModel extends baseModel {
static get jsonSchema() {
return {
type: 'object',
required: ['farm_id', 'name', 'count'],
required: ['farm_id', 'count'],
oneOf: [
{
required: ['default_type_id'],
Expand All @@ -49,8 +50,9 @@ class AnimalBatchModel extends baseModel {
default_type_id: { type: ['integer', 'null'] },
farm_id: { type: 'string' },
id: { type: 'integer' },
name: { type: 'string' },
name: { type: ['string', 'null'] },
notes: { type: ['string', 'null'] },
photo_url: { type: ['string', 'null'] },
...this.baseProperties,
},
additionalProperties: false,
Expand All @@ -68,6 +70,15 @@ class AnimalBatchModel extends baseModel {
to: 'animal_batch_sex_detail.animal_batch_id',
},
},
animal_union_batch_id_view: {
relation: Model.BelongsToOneRelation,
modelClass: animalUnionBatchIdViewModel,
join: {
from: 'animal_batch.id',
to: 'animal_union_batch_id_view.id',
},
filter: (query) => query.where('animal_union_batch_id_view.batch', true),
},
};
}
}
Expand Down
29 changes: 20 additions & 9 deletions packages/api/src/models/animalModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

import Model from './baseFormatModel.js';
import baseModel from './baseModel.js';
import animalUnionBatchIdViewModel from './animalUnionBatchIdViewModel.js';

class Animal extends baseModel {
static get tableName() {
Expand All @@ -31,18 +33,12 @@ class Animal extends baseModel {
return {
type: 'object',
required: ['farm_id'],
anyOf: [
oneOf: [
{
required: ['default_type_id', 'name'],
required: ['default_type_id'],
},
{
required: ['custom_type_id', 'name'],
},
{
required: ['default_type_id', 'identifier'],
},
{
required: ['custom_type_id', 'identifier'],
required: ['custom_type_id'],
},
],
properties: {
Expand All @@ -64,11 +60,26 @@ class Animal extends baseModel {
brought_in_date: { type: ['string', 'null'], format: 'date' },
weaning_date: { type: ['string', 'null'], format: 'date' },
notes: { type: ['string', 'null'] },
photo_url: { type: ['string', 'null'] },
...this.baseProperties,
},
additionalProperties: false,
};
}

static get relationMappings() {
return {
animal_union_batch_id_view: {
relation: Model.BelongsToOneRelation,
modelClass: animalUnionBatchIdViewModel,
join: {
from: 'animal.id',
to: 'animal_union_batch_id_view.id',
},
filter: (query) => query.where('animal_union_batch_id_view.batch', false),
},
};
}
}

export default Animal;
24 changes: 24 additions & 0 deletions packages/api/src/models/animalUnionBatchIdViewModel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2024 LiteFarm.org
* This file is part of LiteFarm.
*
* LiteFarm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LiteFarm is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

import Model from './baseFormatModel.js';

class animalUnionBatchIdViewModel extends Model {
static get tableName() {
return 'animal_union_batch_id_view';
}
}

export default animalUnionBatchIdViewModel;
35 changes: 35 additions & 0 deletions packages/api/src/util/animal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2024 LiteFarm.org
* This file is part of LiteFarm.
*
* LiteFarm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LiteFarm is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

import knex from './knex.js';

/**
* Assigns internal identifiers to records.
* @param {Array<Object>} records - The array of animals or animal batches to which internal identifiers will be assigned.
* Each record is expected to contain an 'id' property.
* @param {string} kind - The kind of records being processed ('animal' or 'batch').
*/
export const assignInternalIdentifiers = async (records, kind) => {
await Promise.all(
records.map(async (record) => {
const [internalIdentifier] = await knex('animal_union_batch_id_view')
.pluck('internal_identifier')
.where('id', record.id)
.andWhere({ batch: kind === 'batch' });

record.internal_identifier = internalIdentifier;
}),
);
};
Loading

0 comments on commit 86ceb47

Please sign in to comment.