From 52507bf186ce2ef31712e006789132fabbd0a729 Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Mon, 3 Feb 2025 10:57:04 -0500 Subject: [PATCH 1/9] LF-4703 Add base properties to farm_addon --- ...44322_add_base_properties_to_farm_addon.js | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js diff --git a/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js b/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js new file mode 100644 index 0000000000..1e6feb6c89 --- /dev/null +++ b/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025 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 . + */ + +export const up = async function (knex) { + await knex.schema.alterTable('farm_addon', (t) => { + t.boolean('deleted').notNullable().defaultTo(false); + t.string('created_by_user_id').references('user_id').inTable('users'); + t.string('updated_by_user_id').references('user_id').inTable('users'); + t.dateTime('created_at').notNullable().defaultTo(new Date('2000/1/1').toISOString()); + t.dateTime('updated_at').notNullable().defaultTo(new Date('2000/1/1').toISOString()); + }); +}; + +export const down = async function (knex) { + await knex.schema.alterTable('farm_addon', (t) => { + t.dropColumns([ + 'deleted', + 'created_by_user_id', + 'updated_by_user_id', + 'created_at', + 'updated_at', + ]); + }); +}; From 51fe2bbf30c0420fdfc5b0538215f0c37c70a9ee Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Mon, 3 Feb 2025 10:58:02 -0500 Subject: [PATCH 2/9] LF-4703 Convert model to base property model --- packages/api/src/models/farmAddonModel.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/api/src/models/farmAddonModel.js b/packages/api/src/models/farmAddonModel.js index 3299ee10e2..668f7d9b0f 100644 --- a/packages/api/src/models/farmAddonModel.js +++ b/packages/api/src/models/farmAddonModel.js @@ -14,11 +14,11 @@ */ import Model from './baseFormatModel.js'; - import AddonPartner from './addonPartnerModel.js'; import Farm from './farmModel.js'; +import baseModel from './baseModel.js'; -class FarmAddon extends Model { +class FarmAddon extends baseModel { /** * Identifies the database table for this Model. * @static @@ -50,6 +50,7 @@ class FarmAddon extends Model { addon_partner_id: { type: 'integer' }, org_uuid: { type: 'string' }, org_pk: { type: 'integer' }, + ...this.baseProperties, }, additionalProperties: false, }; From 70bdada77945fe8c5fd7707027d98c91daa4b924 Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Mon, 3 Feb 2025 20:02:15 -0500 Subject: [PATCH 3/9] LF-4703 Add uniquess partial index to only allow 1 entry per farm and partner combination --- ...44322_add_base_properties_to_farm_addon.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js b/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js index 1e6feb6c89..6e0286200c 100644 --- a/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js +++ b/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js @@ -15,17 +15,39 @@ export const up = async function (knex) { await knex.schema.alterTable('farm_addon', (t) => { + // Knex apparently does not rename indexes aliong with table names + t.dropPrimary('farm_external_integration_pkey'); + }); + await knex.schema.alterTable('farm_addon', (t) => { + t.increments('id').primary(); t.boolean('deleted').notNullable().defaultTo(false); t.string('created_by_user_id').references('user_id').inTable('users'); t.string('updated_by_user_id').references('user_id').inTable('users'); t.dateTime('created_at').notNullable().defaultTo(new Date('2000/1/1').toISOString()); t.dateTime('updated_at').notNullable().defaultTo(new Date('2000/1/1').toISOString()); }); + + // Add the partial unique index using raw SQL + // Knex partial indexes not working correctly + // TODO: Delete this index when multiple organizations are allowed per partner id + await knex.raw(` + CREATE UNIQUE INDEX farm_addon_uniqueness_composite + ON farm_addon(farm_id, addon_partner_id) + WHERE deleted = false; + `); }; export const down = async function (knex) { await knex.schema.alterTable('farm_addon', (t) => { + t.dropIndex(['farm_id', 'addon_partner_id'], 'farm_addon_uniqueness_composite'); + t.dropPrimary(); + }); + await knex.schema.alterTable('farm_addon', (t) => { + t.primary(['farm_id', 'addon_partner_id'], { + constraintName: 'farm_external_integration_pkey', + }); t.dropColumns([ + 'id', 'deleted', 'created_by_user_id', 'updated_by_user_id', From e9afd1f2286d13427feadadb49fcc1665d914b9a Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Mon, 10 Feb 2025 08:25:22 -0500 Subject: [PATCH 4/9] LF-4703 Add default and notNullabel to user base properties, also a comment explaining lack of data migration --- ...0203144322_add_base_properties_to_farm_addon.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js b/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js index 6e0286200c..1f3e8a7291 100644 --- a/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js +++ b/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js @@ -21,12 +21,22 @@ export const up = async function (knex) { await knex.schema.alterTable('farm_addon', (t) => { t.increments('id').primary(); t.boolean('deleted').notNullable().defaultTo(false); - t.string('created_by_user_id').references('user_id').inTable('users'); - t.string('updated_by_user_id').references('user_id').inTable('users'); + t.string('created_by_user_id') + .references('user_id') + .inTable('users') + .notNullable() + .defaultTo(1); + t.string('updated_by_user_id') + .references('user_id') + .inTable('users') + .notNullable() + .defaultTo(1); t.dateTime('created_at').notNullable().defaultTo(new Date('2000/1/1').toISOString()); t.dateTime('updated_at').notNullable().defaultTo(new Date('2000/1/1').toISOString()); }); + // No need to check for duplicates and mark deleted due to prior composite index + // Add the partial unique index using raw SQL // Knex partial indexes not working correctly // TODO: Delete this index when multiple organizations are allowed per partner id From 7cc6cfd65a63d6c479ba42e30372ee4ccbd4f9ca Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Mon, 10 Feb 2025 08:26:52 -0500 Subject: [PATCH 5/9] LF-4703 Add existence check to addon create middleware --- .../api/src/middleware/validation/checkFarmAddon.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/api/src/middleware/validation/checkFarmAddon.js b/packages/api/src/middleware/validation/checkFarmAddon.js index 28a134a7ec..27fa417312 100644 --- a/packages/api/src/middleware/validation/checkFarmAddon.js +++ b/packages/api/src/middleware/validation/checkFarmAddon.js @@ -15,11 +15,13 @@ import { validate } from 'uuid'; import AddonPartnerModel from '../../models/addonPartnerModel.js'; +import FarmAddonModel from '../../models/farmAddonModel.js'; import { ENSEMBLE_BRAND } from '../../util/ensemble.js'; export function checkFarmAddon() { return async (req, res, next) => { try { + const { farm_id } = req.headers; const { addon_partner_id, org_uuid } = req.body; if (!org_uuid || !org_uuid.length) { @@ -36,6 +38,17 @@ export function checkFarmAddon() { return res.status(400).send('Only Ensemble Scientific is supported'); } + // Check if addon exists + const existingAddon = await FarmAddonModel.query() + .findOne({ farm_id, addon_partner_id }) + .whereNotDeleted(); + + if (existingAddon) { + return res + .status(400) + .send('An existing addon with this partner already exists on your farm'); + } + next(); } catch (error) { console.log(error); From 630c9402c5816767e17479a8db47e0e590b1cbbf Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Mon, 10 Feb 2025 10:44:16 -0500 Subject: [PATCH 6/9] Revert "LF-4703 Add existence check to addon create middleware" This reverts commit f0334bf492f8bc92569d0ab43d3b4321211e0377. --- .../api/src/middleware/validation/checkFarmAddon.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/api/src/middleware/validation/checkFarmAddon.js b/packages/api/src/middleware/validation/checkFarmAddon.js index 27fa417312..28a134a7ec 100644 --- a/packages/api/src/middleware/validation/checkFarmAddon.js +++ b/packages/api/src/middleware/validation/checkFarmAddon.js @@ -15,13 +15,11 @@ import { validate } from 'uuid'; import AddonPartnerModel from '../../models/addonPartnerModel.js'; -import FarmAddonModel from '../../models/farmAddonModel.js'; import { ENSEMBLE_BRAND } from '../../util/ensemble.js'; export function checkFarmAddon() { return async (req, res, next) => { try { - const { farm_id } = req.headers; const { addon_partner_id, org_uuid } = req.body; if (!org_uuid || !org_uuid.length) { @@ -38,17 +36,6 @@ export function checkFarmAddon() { return res.status(400).send('Only Ensemble Scientific is supported'); } - // Check if addon exists - const existingAddon = await FarmAddonModel.query() - .findOne({ farm_id, addon_partner_id }) - .whereNotDeleted(); - - if (existingAddon) { - return res - .status(400) - .send('An existing addon with this partner already exists on your farm'); - } - next(); } catch (error) { console.log(error); From 2d42a8c549cbe28b9a48cde6afc14153c5f0db2e Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Mon, 10 Feb 2025 19:09:55 -0500 Subject: [PATCH 7/9] LF-4703 Update model methods using basecontroller for base ofbject properties --- .../src/controllers/farmAddonController.js | 13 +++----- packages/api/src/models/farmAddonModel.js | 33 ++++++++++++------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/api/src/controllers/farmAddonController.js b/packages/api/src/controllers/farmAddonController.js index 6615f3535a..0a1572e504 100644 --- a/packages/api/src/controllers/farmAddonController.js +++ b/packages/api/src/controllers/farmAddonController.js @@ -19,8 +19,7 @@ import { getValidEnsembleOrg } from '../util/ensemble.js'; const farmAddonController = { addFarmAddon() { return async (req, res) => { - const { farm_id } = req.headers; - const { addon_partner_id, org_uuid } = req.body; + const { org_uuid } = req.body; try { const organisation = await getValidEnsembleOrg(org_uuid); @@ -29,12 +28,10 @@ const farmAddonController = { return res.status(404).send('Organisation not found'); } - await FarmAddonModel.upsertFarmAddon({ - farm_id, - addon_partner_id, - org_uuid, - org_pk: organisation.pk, - }); + await FarmAddonModel.upsertFarmAddon({ + req, + org_pk: organisation.pk, + }); return res.status(200).send(); } catch (error) { diff --git a/packages/api/src/models/farmAddonModel.js b/packages/api/src/models/farmAddonModel.js index 668f7d9b0f..fc236a4f51 100644 --- a/packages/api/src/models/farmAddonModel.js +++ b/packages/api/src/models/farmAddonModel.js @@ -17,6 +17,7 @@ import Model from './baseFormatModel.js'; import AddonPartner from './addonPartnerModel.js'; import Farm from './farmModel.js'; import baseModel from './baseModel.js'; +import baseController from '../controllers/baseController.js'; class FarmAddon extends baseModel { /** @@ -31,10 +32,10 @@ class FarmAddon extends baseModel { /** * Identifies the primary key fields for this Model. * @static - * @returns {string[]} Names of the primary key fields. + * @returns {string} Names of the primary key fields. */ static get idColumn() { - return ['farm_id', 'addon_partner_id']; + return 'id'; } /** @@ -46,6 +47,7 @@ class FarmAddon extends baseModel { return { type: 'object', properties: { + id: { type: 'integer' }, farm_id: { type: 'string' }, addon_partner_id: { type: 'integer' }, org_uuid: { type: 'string' }, @@ -100,18 +102,27 @@ class FarmAddon extends baseModel { .first(); } - static async upsertFarmAddon({ farm_id, addon_partner_id, org_uuid, org_pk }) { - const existingAddon = await this.query().findOne({ farm_id, addon_partner_id }); + static async upsertFarmAddon({ req, org_pk }) { + const { farm_id } = req.headers; + const { addon_partner_id, org_uuid } = req.body; + + const existingAddon = await this.query() + .findOne({ farm_id, addon_partner_id }) + .whereNotDeleted(); if (existingAddon) { - return this.query().patch({ org_uuid, org_pk }).where({ farm_id, addon_partner_id }); + return baseController.patch(FarmAddon, existingAddon.id, { org_uuid, org_pk }, req); } else { - return this.query().insert({ - farm_id, - addon_partner_id, - org_uuid, - org_pk, - }); + return baseController.post( + FarmAddon, + { + farm_id, + addon_partner_id, + org_uuid, + org_pk, + }, + req, + ); } } } From c56065af4632b16cbfe9a86d7f6b897e9b3c98d3 Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Tue, 11 Feb 2025 08:38:13 -0500 Subject: [PATCH 8/9] LF-4703 Delete deleted entries on rollback to meet previous PK uniqueness --- .../20250203144322_add_base_properties_to_farm_addon.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js b/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js index 1f3e8a7291..006985bc8b 100644 --- a/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js +++ b/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js @@ -15,7 +15,7 @@ export const up = async function (knex) { await knex.schema.alterTable('farm_addon', (t) => { - // Knex apparently does not rename indexes aliong with table names + // Knex apparently does not rename indexes along with table names t.dropPrimary('farm_external_integration_pkey'); }); await knex.schema.alterTable('farm_addon', (t) => { @@ -48,6 +48,7 @@ export const up = async function (knex) { }; export const down = async function (knex) { + await knex('farm_addon').where({ deleted: true }).del(); await knex.schema.alterTable('farm_addon', (t) => { t.dropIndex(['farm_id', 'addon_partner_id'], 'farm_addon_uniqueness_composite'); t.dropPrimary(); From 439db587069428a73a5b560e4390d05e1ee0edc2 Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Wed, 12 Feb 2025 09:51:02 -0500 Subject: [PATCH 9/9] LF-4703 Add missing whereNotDeleted() to get queries on farm_addon and unused variables error --- packages/api/src/controllers/farmAddonController.js | 13 +++++++------ packages/api/src/models/farmAddonModel.js | 2 ++ packages/api/src/util/ensemble.js | 3 +++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/api/src/controllers/farmAddonController.js b/packages/api/src/controllers/farmAddonController.js index 0a1572e504..bae569d302 100644 --- a/packages/api/src/controllers/farmAddonController.js +++ b/packages/api/src/controllers/farmAddonController.js @@ -19,7 +19,7 @@ import { getValidEnsembleOrg } from '../util/ensemble.js'; const farmAddonController = { addFarmAddon() { return async (req, res) => { - const { org_uuid } = req.body; + const { org_uuid } = req.body; try { const organisation = await getValidEnsembleOrg(org_uuid); @@ -28,10 +28,10 @@ const farmAddonController = { return res.status(404).send('Organisation not found'); } - await FarmAddonModel.upsertFarmAddon({ - req, - org_pk: organisation.pk, - }); + await FarmAddonModel.upsertFarmAddon({ + req, + org_pk: organisation.pk, + }); return res.status(200).send(); } catch (error) { @@ -49,7 +49,8 @@ const farmAddonController = { const { addon_partner_id } = req.query; const rows = await FarmAddonModel.query() .where({ farm_id, addon_partner_id }) - .skipUndefined(); + .skipUndefined() + .whereNotDeleted(); if (!rows.length) { return res.sendStatus(404); } diff --git a/packages/api/src/models/farmAddonModel.js b/packages/api/src/models/farmAddonModel.js index fc236a4f51..6c483da635 100644 --- a/packages/api/src/models/farmAddonModel.js +++ b/packages/api/src/models/farmAddonModel.js @@ -99,6 +99,7 @@ class FarmAddon extends baseModel { .select('org_uuid', 'org_pk') .where('farm_id', farmId) .where('addon_partner_id', addonPartnerId) + .whereNotDeleted() .first(); } @@ -106,6 +107,7 @@ class FarmAddon extends baseModel { const { farm_id } = req.headers; const { addon_partner_id, org_uuid } = req.body; + // With unique composite index only one can exist const existingAddon = await this.query() .findOne({ farm_id, addon_partner_id }) .whereNotDeleted(); diff --git a/packages/api/src/util/ensemble.js b/packages/api/src/util/ensemble.js index cc70c9db54..3021c0c4a8 100644 --- a/packages/api/src/util/ensemble.js +++ b/packages/api/src/util/ensemble.js @@ -314,6 +314,7 @@ async function registerOrganisationWebhook(farmId, organisationId) { const authHeader = `${farmId}${process.env.SENSOR_SECRET}`; const existingIntegration = await FarmAddonModel.query() .where({ farm_id: farmId, addon_partner_id: 1 }) + .whereNotDeleted() .first(); if (existingIntegration?.webhook_id) { return; @@ -399,6 +400,7 @@ async function createOrganisation(farmId) { const data = await FarmModel.getFarmById(farmId); const existingIntegration = await FarmAddonModel.query() .where({ farm_id: farmId, addon_partner_id: 1 }) + .whereNotDeleted() .first(); if (!existingIntegration) { const axiosObject = { @@ -544,6 +546,7 @@ async function authenticateToGetTokens() { ); return response.data; } catch (error) { + console.error(error); const err = new Error('Failed to authenticate with Ensemble.'); err.status = 500; throw err;