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..006985bc8b --- /dev/null +++ b/packages/api/db/migration/20250203144322_add_base_properties_to_farm_addon.js @@ -0,0 +1,69 @@ +/* + * 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) => { + // Knex apparently does not rename indexes along 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') + .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 + 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('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(); + }); + 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', + 'created_at', + 'updated_at', + ]); + }); +}; diff --git a/packages/api/src/controllers/farmAddonController.js b/packages/api/src/controllers/farmAddonController.js index 6615f3535a..bae569d302 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); @@ -30,9 +29,7 @@ const farmAddonController = { } await FarmAddonModel.upsertFarmAddon({ - farm_id, - addon_partner_id, - org_uuid, + req, org_pk: organisation.pk, }); @@ -52,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 3299ee10e2..6c483da635 100644 --- a/packages/api/src/models/farmAddonModel.js +++ b/packages/api/src/models/farmAddonModel.js @@ -14,11 +14,12 @@ */ 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 Model { +class FarmAddon extends baseModel { /** * Identifies the database table for this Model. * @static @@ -31,10 +32,10 @@ class FarmAddon extends Model { /** * 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,10 +47,12 @@ class FarmAddon extends Model { return { type: 'object', properties: { + id: { type: 'integer' }, farm_id: { type: 'string' }, addon_partner_id: { type: 'integer' }, org_uuid: { type: 'string' }, org_pk: { type: 'integer' }, + ...this.baseProperties, }, additionalProperties: false, }; @@ -96,21 +99,32 @@ class FarmAddon extends Model { .select('org_uuid', 'org_pk') .where('farm_id', farmId) .where('addon_partner_id', addonPartnerId) + .whereNotDeleted() .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; + + // With unique composite index only one can exist + 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, + ); } } } 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;