diff --git a/packages/api/db/migration/20250106233711_sensors_refactor_destroy_data.js b/packages/api/db/migration/20250106233711_sensors_refactor_destroy_data.js new file mode 100644 index 0000000000..0df1ac3bfb --- /dev/null +++ b/packages/api/db/migration/20250106233711_sensors_refactor_destroy_data.js @@ -0,0 +1,260 @@ +/* + * 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 . + */ + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export const up = async function (knex) { + // WARNING: Will delete all existing sensors data except for 'integrating_partner' + await knex.schema.dropTable('sensor_reading'); + await knex.schema.dropTable('sensor_reading_type'); + await knex.schema.dropTable('sensor'); + await knex.schema.dropTable('partner_reading_type'); + await knex.schema.dropTable('farm_external_integration'); + + // Add inbound integration type + await knex.schema.createTable('inbound_integration_type', (table) => { + table.increments('id').primary(); + table.string('type').notNullable(); + }); + + await knex('inbound_integration_type').insert([ + { type: 'SENSOR_READING' }, + { type: 'IRRIGATION_PRESCRIPTION' }, + ]); + + // Create farm inbound integration + await knex.schema.createTable('farm_inbound_integration', (table) => { + table.primary(['farm_id', 'partner_id']); + // New notNullable constraints on farm_id and partner_id since null != null violating our original intention for primary key + table.uuid('farm_id').references('farm_id').inTable('farm').notNullable(); + table + .integer('partner_id') + .references('partner_id') + .inTable('integrating_partner') + .notNullable(); + table.uuid('organization_uuid').nullable(); + table.integer('webhook_id').nullable(); + table.integer('type_id').references('id').inTable('inbound_integration_type').notNullable(); + }); + + // Create sensor manufacturer table + await knex.schema.createTable('sensor_manufacturer', (table) => { + table.increments('id').primary(); + // references name on integrating_partner if integrating_partner_id exists + table.string('name').notNullable(); + table.uuid('farm_id').references('farm_id').inTable('farm').nullable(); + table + .integer('integrating_partner_id') + .references('partner_id') + .inTable('integrating_partner') + .notNullable(); + }); + + // Create sensor_array table + await knex.schema.createTable('sensor_array', (table) => { + // Other locations seem to use .onDelete('CASCADE') but I don't think it is appropriate + table.uuid('location_id').primary().references('location_id').inTable('location').notNullable(); + table.uuid('field_location_id').references('location_id').inTable('field').notNullable(); + }); + + // Add inbound integration type + await knex.schema.createTable('sensor_status', (table) => { + table.increments('id').primary(); + table.string('key').notNullable(); + }); + + await knex('sensor_status').insert([{ key: 'ONLINE' }, { key: 'OFFLINE' }]); + + // Create sensor table + await knex.schema.createTable('sensor', (table) => { + table.increments('id').primary(); + table.string('name').nullable(); + // Contrains sensor to arrays instead of all locations + table.uuid('location_id').references('location_id').inTable('sensor_array').notNullable(); + // changed from notNullable + table.string('external_id').nullable(); + table + .integer('sensor_manufacturer_id') + .references('id') + .inTable('sensor_manufacturer') + .nullable(); + table.check( + '(?? IS NULL AND ?? IS NULL) OR (?? IS NOT NULL AND ?? IS NOT NULL)', + ['external_id', 'sensor_manufacturer_id', 'external_id', 'sensor_manufacturer_id'], + 'external_data_check', + ); + table.string('model'); + // combining depth and elevation + table.float('depth_elevation').nullable(); + // removing defaultTo('cm') + table.enu('depth_elevation_unit', ['cm', 'm', 'in', 'ft']).nullable(); + table.check( + '(?? IS NULL AND ?? IS NULL) OR (?? IS NOT NULL AND ?? IS NOT NULL)', + ['depth_elevation', 'depth_elevation_unit', 'depth_elevation', 'depth_elevation_unit'], + 'depth_elevation_unit_check', + ); + table.boolean('retired').notNullable().defaultTo('false'); + table.boolean('deleted').notNullable().defaultTo('false'); + }); + + // Refactor sensor_reading_type - references partner_reading_type, sensor_reading_type + await knex.schema.createTable('sensor_reading_type', (table) => { + table.increments('id').primary(); + table.string('type').notNullable(); + table.string('unit').notNullable(); + }); + + await knex('sensor_reading_type').insert([ + { type: 'temperature', unit: 'C' }, + { type: 'soil_water_potential', unit: 'kPa' }, + { type: 'soil_water_content', unit: 'mm' }, + ]); + + // Create relationship table + await knex.schema.createTable('sensor_reading_mode', (table) => { + table.integer('sensor_id').references('id').inTable('sensor').notNullable(); + table + .integer('sensor_reading_type_id') + .references('id') + .inTable('sensor_reading_type') + .notNullable(); + }); + + // Create sensor reading table + await knex.schema.createTable('sensor_reading', (table) => { + table.increments('id').primary(); + table.integer('sensor_id').references('id').inTable('sensor').notNullable(); + table + .integer('sensor_reading_type_id') + .references('id') + .inTable('sensor_reading_type') + .notNullable(); + table.timestamp('created_at').notNullable(); + table.float('value').notNullable(); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export const down = async function (knex) { + // WARNING: Will delete all existing sensors data except for 'integrating_partner' + await knex.schema.dropTable('sensor_reading'); + await knex.schema.dropTable('sensor_reading_mode'); + await knex.schema.dropTable('sensor_reading_type'); + await knex.schema.dropTable('sensor'); + await knex.schema.dropTable('sensor_status'); + await knex.schema.dropTable('sensor_array'); + await knex.schema.dropTable('sensor_manufacturer'); + await knex.schema.dropTable('farm_inbound_integration'); + await knex.schema.dropTable('inbound_integration_type'); + + // Recreate farm_external_integration - from /20220610171713_create-farm-external-integrations-table.js + // Including alteration from webhook address to webhook id + await knex.schema.createTable('farm_external_integration', (table) => { + table.primary(['farm_id', 'partner_id']); + table.uuid('farm_id').references('farm_id').inTable('farm'); + table.integer('partner_id').references('partner_id').inTable('integrating_partner'); + table.uuid('organization_uuid'); + table.integer('webhook_id'); + }); + + // Recreate partner reading type - from /20220614195741_sensor_reading_types.js + await knex.schema.createTable('partner_reading_type', (table) => { + table + .uuid('partner_reading_type_id') + .primary() + .notNullable() + .defaultTo(knex.raw('uuid_generate_v1()')); + table + .integer('partner_id') + .references('partner_id') + .inTable('integrating_partner') + .notNullable(); + table.integer('raw_value'); + table.string('readable_value'); + }); + + await knex('partner_reading_type').insert([ + { + partner_id: 1, + readable_value: 'soil_water_content', + }, + { + partner_id: 1, + readable_value: 'soil_water_potential', + }, + { + partner_id: 1, + readable_value: 'temperature', + }, + ]); + + // Recreate sensor table - from /20220715214935_sensor_as_standard_location.js + // Including alteration to depth_unit + await knex.schema.createTable('sensor', function (table) { + table + .uuid('location_id') + .primary() + .notNullable() + .references('location_id') + .inTable('location') + .unique() + .onDelete('CASCADE'); + table + .integer('partner_id') + .references('partner_id') + .inTable('integrating_partner') + .notNullable(); + table.string('external_id').notNullable(); + table.float('depth'); + table.enu('depth_unit', ['cm', 'm', 'in', 'ft']).defaultTo('cm'); + table.float('elevation'); + table.string('model'); + }), + // Rebuild sensor reading type - from /20220715214935_sensor_as_standard_location.js + await knex.schema.createTable('sensor_reading_type', function (table) { + table + .uuid('sensor_reading_type_id') + .primary() + .notNullable() + .defaultTo(knex.raw('uuid_generate_v1()')); + table + .uuid('partner_reading_type_id') + .references('partner_reading_type_id') + .inTable('partner_reading_type'); + table.uuid('location_id').references('location_id').inTable('sensor').onDelete('CASCADE'); + }); + + // Rebuild sensor reading type - from /20220715214935_sensor_as_standard_location.js + await knex.schema.createTable('sensor_reading', function (table) { + table.uuid('reading_id').primary().notNullable().defaultTo(knex.raw('uuid_generate_v1()')); + table + .uuid('location_id') + .notNullable() + .references('location_id') + .inTable('sensor') + .onDelete('CASCADE'); + table.timestamp('read_time').notNullable(); + table.timestamp('created_at').notNullable().defaultTo(knex.fn.now()); + table.string('reading_type').notNullable(); + table.float('value').notNullable(); + table.string('unit').notNullable(); + table.boolean('valid').notNullable().defaultTo(true); + }); +};