Skip to content

Commit 4e1bd2f

Browse files
authored
Merge pull request #3854 from LiteFarmOrg/integration
Release 3.8.0
2 parents d65e9d9 + d6a2bcb commit 4e1bd2f

File tree

120 files changed

+4042
-857
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

120 files changed

+4042
-857
lines changed

packages/api/.env.default

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,15 @@ API_HOST=localhost
9797
OOO_MESSAGE_ENABLED=
9898
# Format YYYY/MM/DD
9999
OOO_END_DATE=
100+
101+
102+
# Debugging variables for working on specific features
103+
104+
# Speed up the notifications timer (cycle through 1 hour every minute)
105+
# MOCK_TIMER=true
106+
107+
# Specify which mock implementation to use for addon partners in development (GET irrigation prescription list)
108+
# MOCK_ADDON_PARTNER=ESCI
109+
110+
# Mock Ensemble irrigation prescription details
111+
# USE_IP_MOCK_DETAILS=true
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2025 LiteFarm.org
3+
* This file is part of LiteFarm.
4+
*
5+
* LiteFarm is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* LiteFarm is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
14+
*/
15+
16+
/**
17+
* @param { import("knex").Knex } knex
18+
* @returns { Promise<void> }
19+
*/
20+
export const up = async (knex) => {
21+
// Rows to be inserted to 'migration_deletion_logs' table
22+
// When rolling back, rows will be restored backwards
23+
const rowsToLog = [];
24+
const addToLogs = (rows, tableName) => {
25+
rows.forEach((row) => {
26+
rowsToLog.push({ table_name: tableName, data: row });
27+
});
28+
};
29+
30+
const legacyEnsembleFarmAddons = await knex('farm_addon')
31+
.where('addon_partner_id', 1)
32+
.whereNull('org_pk');
33+
34+
addToLogs(legacyEnsembleFarmAddons, 'farm_addon');
35+
36+
// Delete logged records
37+
await knex('farm_addon').where('addon_partner_id', 1).whereNull('org_pk').del();
38+
39+
if (rowsToLog.length) {
40+
await knex('migration_deletion_logs').insert(
41+
rowsToLog.map((row) => ({
42+
migration_name: 'cleanup_legacy_ensemble_farm_addons',
43+
...row,
44+
})),
45+
);
46+
}
47+
};
48+
49+
/**
50+
* @param { import("knex").Knex } knex
51+
* @returns { Promise<void> }
52+
*/
53+
export const down = async (knex) => {
54+
// Restore deleted rows from migration_deletion_logs
55+
const rows = await knex('migration_deletion_logs')
56+
.where('migration_name', 'cleanup_legacy_ensemble_farm_addons')
57+
.orderBy('id', 'desc');
58+
59+
for (const { table_name, data } of rows) {
60+
try {
61+
await knex(table_name).insert(data);
62+
} catch (err) {
63+
console.error(`Failed to restore row in ${table_name}:`, err);
64+
throw err;
65+
}
66+
}
67+
68+
await knex('migration_deletion_logs')
69+
.where('migration_name', 'cleanup_legacy_ensemble_farm_addons')
70+
.del();
71+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2025 LiteFarm.org
3+
* This file is part of LiteFarm.
4+
*
5+
* LiteFarm is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* LiteFarm is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
14+
*/
15+
16+
/**
17+
* @param { import("knex").Knex } knex
18+
* @returns { Promise<void> }
19+
*/
20+
export const up = async (knex) => {
21+
await knex.schema.alterTable('task', (table) => {
22+
table.dateTime('revision_date');
23+
table.string('revised_by_user_id').references('user_id').inTable('users');
24+
});
25+
};
26+
27+
/**
28+
* @param { import("knex").Knex } knex
29+
* @returns { Promise<void> }
30+
*/
31+
export const down = async (knex) => {
32+
await knex.schema.table('task', (table) => {
33+
table.dropColumn('revision_date');
34+
table.dropColumn('revised_by_user_id');
35+
});
36+
};
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
/*
2+
* Copyright 2025 LiteFarm.org
3+
* This file is part of LiteFarm.
4+
*
5+
* LiteFarm is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* LiteFarm is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
14+
*/
15+
16+
-- @description: Return new JSONB = base + (key→payload) if payload IS NOT NULL. A helper function to build the export data object
17+
CREATE OR REPLACE FUNCTION jsonb_merge(
18+
base JSONB,
19+
key TEXT,
20+
payload JSONB
21+
) RETURNS JSONB
22+
LANGUAGE sql AS $$
23+
SELECT CASE
24+
WHEN payload IS NULL THEN base
25+
ELSE base || jsonb_build_object(key, payload)
26+
END;
27+
$$;
28+
29+
-- @description: Build a JSONB payload containing every user- and farm-scoped record for export
30+
CREATE OR REPLACE FUNCTION export_user_data()
31+
RETURNS JSONB
32+
LANGUAGE plpgsql
33+
AS $$
34+
DECLARE
35+
target_user_id VARCHAR := 'user_id_here';
36+
user_data user_data_collections;
37+
farm_data farm_data_collections;
38+
farm_count INTEGER;
39+
40+
task_tables RECORD;
41+
task_prod_tables RECORD;
42+
pmp_tables RECORD;
43+
mp_tables RECORD;
44+
plan_repetition_tables RECORD;
45+
figure_tables RECORD;
46+
animal_tables RECORD;
47+
animal_batch_tables RECORD;
48+
location_tables RECORD;
49+
secondary_tables RECORD;
50+
farm_tables RECORD;
51+
created_by_tables RECORD;
52+
user_tables RECORD;
53+
54+
farm_json JSONB;
55+
table_data JSONB;
56+
full_export JSONB := '{}'::jsonb;
57+
BEGIN
58+
user_data := get_user_data(target_user_id);
59+
60+
FOREACH farm_data IN ARRAY user_data.farms LOOP
61+
farm_json := '{}'::jsonb;
62+
63+
-- ==== TASK PRODUCT-SCOPED TABLES ====
64+
FOR task_prod_tables IN SELECT * FROM get_task_product_tables() LOOP
65+
EXECUTE format(
66+
'SELECT json_agg(to_jsonb(t)) FROM %I t WHERE t.task_products_id = ANY($1)',
67+
task_prod_tables.table_name
68+
)
69+
INTO table_data
70+
USING farm_data.task_products_ids;
71+
72+
farm_json := jsonb_merge(farm_json, task_prod_tables.table_name, table_data);
73+
END LOOP;
74+
75+
-- ==== TASK‐SCOPED TABLES ====
76+
FOR task_tables IN SELECT * FROM get_task_tables() LOOP
77+
EXECUTE format(
78+
'SELECT json_agg(to_jsonb(t)) FROM %I t WHERE t.task_id = ANY($1)',
79+
task_tables.table_name
80+
)
81+
INTO table_data
82+
USING farm_data.task_ids;
83+
84+
farm_json := jsonb_merge(farm_json, task_tables.table_name, table_data);
85+
END LOOP;
86+
87+
-- ==== PLANTING MANAGEMENT PLAN TABLES ====
88+
FOR pmp_tables IN SELECT * FROM get_pmp_tables() LOOP
89+
EXECUTE format(
90+
'SELECT json_agg(to_jsonb(p)) FROM %I p ' ||
91+
'WHERE p.planting_management_plan_id = ANY($1)',
92+
pmp_tables.table_name
93+
)
94+
INTO table_data
95+
USING farm_data.pmp_ids;
96+
97+
farm_json := jsonb_merge(farm_json, pmp_tables.table_name, table_data);
98+
END LOOP;
99+
100+
-- ==== MANAGEMENT PLAN TABLES ====
101+
FOR mp_tables IN SELECT * FROM get_management_plan_tables() LOOP
102+
EXECUTE format(
103+
'SELECT json_agg(to_jsonb(m)) FROM %I m ' ||
104+
'WHERE m.management_plan_id = ANY($1)',
105+
mp_tables.table_name
106+
)
107+
INTO table_data
108+
USING farm_data.management_plan_ids;
109+
110+
farm_json := jsonb_merge(farm_json, mp_tables.table_name, table_data);
111+
END LOOP;
112+
113+
-- ==== MANAGEMENT PLAN GROUP TABLES ====
114+
FOR plan_repetition_tables IN SELECT * FROM get_mp_repetition_tables() LOOP
115+
EXECUTE format(
116+
'SELECT json_agg(to_jsonb(r)) FROM %I r ' ||
117+
'WHERE r.management_plan_group_id = ANY($1)',
118+
plan_repetition_tables.table_name
119+
)
120+
INTO table_data
121+
USING farm_data.management_plan_group_ids;
122+
123+
farm_json := jsonb_merge(farm_json, plan_repetition_tables.table_name, table_data);
124+
END LOOP;
125+
126+
-- ==== ANIMALS ====
127+
FOR animal_tables IN SELECT * FROM get_animal_tables() LOOP
128+
EXECUTE format(
129+
'SELECT json_agg(to_jsonb(a)) FROM %I a ' ||
130+
'WHERE a.animal_id = ANY($1)',
131+
animal_tables.table_name
132+
)
133+
INTO table_data
134+
USING farm_data.animal_ids;
135+
136+
farm_json := jsonb_merge(farm_json, animal_tables.table_name, table_data);
137+
END LOOP;
138+
139+
FOR animal_batch_tables IN SELECT * FROM get_animal_batch_tables() LOOP
140+
EXECUTE format(
141+
'SELECT json_agg(to_jsonb(ab)) FROM %I ab ' ||
142+
'WHERE ab.animal_batch_id = ANY($1)',
143+
animal_batch_tables.table_name
144+
)
145+
INTO table_data
146+
USING farm_data.animal_batch_ids;
147+
148+
farm_json := jsonb_merge(farm_json, animal_batch_tables.table_name, table_data);
149+
END LOOP;
150+
151+
-- ==== LOCATIONS ====
152+
FOR figure_tables IN SELECT * FROM get_figure_tables() LOOP
153+
EXECUTE format(
154+
'SELECT json_agg(to_jsonb(f)) FROM %I f ' ||
155+
'WHERE f.figure_id = ANY($1)',
156+
figure_tables.table_name
157+
)
158+
INTO table_data
159+
USING farm_data.figure_ids;
160+
161+
farm_json := jsonb_merge(farm_json, figure_tables.table_name, table_data);
162+
END LOOP;
163+
164+
FOR location_tables IN SELECT * FROM get_location_export_tables() LOOP
165+
EXECUTE format(
166+
'SELECT json_agg(to_jsonb(l)) FROM %I l ' ||
167+
'WHERE l.location_id = ANY($1)',
168+
location_tables.table_name
169+
)
170+
INTO table_data
171+
USING farm_data.location_ids;
172+
173+
farm_json := jsonb_merge(farm_json, location_tables.table_name, table_data);
174+
END LOOP;
175+
176+
-- ==== TABLES REQUIRING SECONDARY JOIN FOR FARM_ID ====
177+
FOR secondary_tables IN SELECT * FROM get_secondary_tables() LOOP
178+
EXECUTE format(
179+
'SELECT json_agg(to_jsonb(c)) ' ||
180+
'FROM %I c JOIN %I p ON c.%I = p.%I ' ||
181+
'WHERE p.farm_id = $1',
182+
secondary_tables.child_table,
183+
secondary_tables.join_table,
184+
secondary_tables.join_key,
185+
secondary_tables.join_key
186+
)
187+
INTO table_data
188+
USING farm_data.farm_id;
189+
190+
farm_json := jsonb_merge(farm_json, secondary_tables.child_table, table_data);
191+
END LOOP;
192+
193+
-- ==== REMAINING FARM-SCOPED TABLES ====
194+
FOR farm_tables IN SELECT * FROM get_farm_export_tables() LOOP
195+
EXECUTE format(
196+
'SELECT json_agg(to_jsonb(fm)) ' ||
197+
'FROM %I fm WHERE fm.farm_id = $1',
198+
farm_tables.table_name
199+
)
200+
INTO table_data
201+
USING farm_data.farm_id;
202+
203+
farm_json := jsonb_merge(farm_json, farm_tables.table_name, table_data);
204+
END LOOP;
205+
206+
-- append this farm into the "farms" array
207+
full_export := jsonb_merge(
208+
full_export,
209+
'farms',
210+
COALESCE(full_export->'farms','[]'::jsonb) || farm_json
211+
);
212+
213+
END LOOP;
214+
215+
-- ==== USER DATA NOT SPECIFIC TO FARM ====
216+
FOR created_by_tables IN SELECT * FROM get_created_by_user_tables() LOOP
217+
EXECUTE format(
218+
'SELECT json_agg(to_jsonb(cb)) ' ||
219+
'FROM %I cb WHERE cb.created_by_user_id = $1',
220+
created_by_tables.table_name
221+
)
222+
INTO table_data
223+
USING target_user_id;
224+
225+
full_export := jsonb_merge(full_export, created_by_tables.table_name, table_data);
226+
END LOOP;
227+
228+
FOR user_tables IN SELECT * FROM get_user_scoped_tables() LOOP
229+
EXECUTE format(
230+
'SELECT json_agg(to_jsonb(u)) ' ||
231+
'FROM %I u WHERE u.user_id = $1',
232+
user_tables.table_name
233+
)
234+
INTO table_data
235+
USING target_user_id;
236+
237+
full_export := jsonb_merge(full_export, user_tables.table_name, table_data);
238+
END LOOP;
239+
240+
-- Format export object
241+
RETURN jsonb_build_object(
242+
'user_id', target_user_id,
243+
'exported_at', to_char(now(), 'YYYY-MM-DD"T"HH24:MI:SS"Z"'),
244+
'export_type', 'all_user_data',
245+
'data', full_export
246+
);
247+
248+
END $$;
249+
250+
SELECT export_user_data() as export;

0 commit comments

Comments
 (0)