From 91d649d8c1c31c9dbb0ed8337c516cdfa75d5ec2 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Sun, 26 Oct 2025 21:38:11 +0100 Subject: [PATCH 01/38] #41 installed express-validator and did basic test with it --- backend/package-lock.json | 29 +++++++++++++++++++++++++++++ backend/package.json | 1 + backend/services/activities.js | 15 ++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 4d622c8..ee0ef26 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,6 +14,7 @@ "cors": "^2.8.5", "express": "^4.19.2", "express-fileupload": "^1.5.0", + "express-validator": "^7.3.0", "luxon": "^3.4.4", "morgan": "^1.10.0" }, @@ -1176,6 +1177,19 @@ "node": ">=12.0.0" } }, + "node_modules/express-validator": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.0.tgz", + "integrity": "sha512-ujK2BX5JUun5NR4JuBo83YSXoDDIpoGz3QxgHTzQcHFevkKnwV1in4K7YNuuXQ1W3a2ObXB/P4OTnTZpUyGWiw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.15.15" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1688,6 +1702,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2748,6 +2768,15 @@ "node": ">= 0.4.0" } }, + "node_modules/validator": { + "version": "13.15.20", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", + "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/backend/package.json b/backend/package.json index e2fa306..f338ce7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -16,6 +16,7 @@ "cors": "^2.8.5", "express": "^4.19.2", "express-fileupload": "^1.5.0", + "express-validator": "^7.3.0", "luxon": "^3.4.4", "morgan": "^1.10.0" }, diff --git a/backend/services/activities.js b/backend/services/activities.js index c6562f3..3a0d1df 100644 --- a/backend/services/activities.js +++ b/backend/services/activities.js @@ -3,11 +3,24 @@ const activitiesDao = require('../dao/activitiesDao.js'); const plantsDao = require('../dao/plantsDao.js'); const express = require('express'); var serviceRouter = express.Router(); +const { body, validationResult } = require('express-validator'); console.log('- Service Activities'); // Neue Activity erstellen -serviceRouter.post('/activities', function(request, response) { +// TODO Funktion weiter ausbauen +serviceRouter.post('/activities', +body("plant_id").isInt({min:0}), +function(request, response) { + const result = validationResult(request); + if (!result.isEmpty()) { + return response.send({ errors: result.array() }); + } + response.send(`Hello, ${request.body.plant_id}!`); +}); + +// Neue Activity erstellen (old) +serviceRouter.post('/activities_old', function(request, response) { console.log('Activities plants: Client requested creation of new record'); var errorMsgs=[]; From c11187a56833179c132ec78009427eedc041d842 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Mon, 27 Oct 2025 21:13:39 +0100 Subject: [PATCH 02/38] #41 tested custom validator and created a helper for it. --- backend/services/activities.js | 6 ++++-- backend/services/validationHelper.js | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 backend/services/validationHelper.js diff --git a/backend/services/activities.js b/backend/services/activities.js index 3a0d1df..5ab2d48 100644 --- a/backend/services/activities.js +++ b/backend/services/activities.js @@ -1,4 +1,5 @@ const helper = require('../helper.js'); +const validationHelper = require('./validationHelper.js'); const activitiesDao = require('../dao/activitiesDao.js'); const plantsDao = require('../dao/plantsDao.js'); const express = require('express'); @@ -10,8 +11,9 @@ console.log('- Service Activities'); // Neue Activity erstellen // TODO Funktion weiter ausbauen serviceRouter.post('/activities', -body("plant_id").isInt({min:0}), +body("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), function(request, response) { + console.log('Activities plants: Client requested creation of new activity'); const result = validationResult(request); if (!result.isEmpty()) { return response.send({ errors: result.array() }); @@ -36,7 +38,7 @@ serviceRouter.post('/activities_old', function(request, response) { } if (helper.isNull(request.body.plant_id)) { errorMsgs.push('plant_id is null'); - } + } if (helper.isUndefined(request.body.type)) { errorMsgs.push('type missing'); } diff --git a/backend/services/validationHelper.js b/backend/services/validationHelper.js new file mode 100644 index 0000000..7dd79a9 --- /dev/null +++ b/backend/services/validationHelper.js @@ -0,0 +1,11 @@ +const plantsDao = require('../dao/plantsDao.js'); + +// plant_id exists validation function +module.exports.validatePlantIDExists = (value,{req}) => { + const plantsDaoInstance = new plantsDao(req.app.locals.dbConnection); + console.log('Validating plant_id existence: ' + value); + if (!plantsDaoInstance.exists(value)) { + throw new Error('Plant with the given ID does not exist'); + } + return true; +}; \ No newline at end of file From 9f02275d6f8c2ef599a0325e3cfe203cbab8374a Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:52:43 +0100 Subject: [PATCH 03/38] #41 added more validators to activities post --- backend/services/activities.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/services/activities.js b/backend/services/activities.js index 5ab2d48..19776eb 100644 --- a/backend/services/activities.js +++ b/backend/services/activities.js @@ -12,11 +12,13 @@ console.log('- Service Activities'); // TODO Funktion weiter ausbauen serviceRouter.post('/activities', body("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), +body("type").isInt({min:0, max:1}), +body("date").optional().isISO8601().toDate(), function(request, response) { console.log('Activities plants: Client requested creation of new activity'); const result = validationResult(request); if (!result.isEmpty()) { - return response.send({ errors: result.array() }); + return response.status(400).json({ errors: result.array() }); } response.send(`Hello, ${request.body.plant_id}!`); }); From ce7f23ac498bf2d29fa56aaf3bf068ecdcfc53da Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:37:23 +0100 Subject: [PATCH 04/38] #41 added validation helpers for activityID and exception to JSON --- backend/services/validationHelper.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/backend/services/validationHelper.js b/backend/services/validationHelper.js index 7dd79a9..ecdc7e5 100644 --- a/backend/services/validationHelper.js +++ b/backend/services/validationHelper.js @@ -1,6 +1,7 @@ const plantsDao = require('../dao/plantsDao.js'); +const activitiesDao = require('../dao/activitiesDao.js'); -// plant_id exists validation function +// plant exists validation function module.exports.validatePlantIDExists = (value,{req}) => { const plantsDaoInstance = new plantsDao(req.app.locals.dbConnection); console.log('Validating plant_id existence: ' + value); @@ -8,4 +9,26 @@ module.exports.validatePlantIDExists = (value,{req}) => { throw new Error('Plant with the given ID does not exist'); } return true; -}; \ No newline at end of file +}; + +// activity exists validation function +module.exports.validateActivityIDExists = (value,{req}) => { + const activitiesDaoInstance = new activitiesDao(req.app.locals.dbConnection); + console.log('Validating activity_id existence: ' + value); + if (!activitiesDaoInstance.exists(value)) { + throw new Error('Activity with the given ID does not exist'); + } + return true; +} + +/** + * Converts an exception to a JSON object. + * @param {Error} ex The exception to convert + * @returns {Object} JSON object representing the exception + */ +module.exports.exceptionToJson = (ex) => { + return { + 'msg': "An error occurred", + 'errorMsg': ex.message + }; +} \ No newline at end of file From 531c1c560d34e7690bf9e1dfd2f506850a291f29 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:39:49 +0100 Subject: [PATCH 05/38] #41 reimplemented activity creation and exists --- backend/server.js | 6 +- backend/services/activities.js | 126 ++++++++++----------------------- docs/API.md | 1 + 3 files changed, 41 insertions(+), 92 deletions(-) diff --git a/backend/server.js b/backend/server.js index 165965e..f43ebb0 100644 --- a/backend/server.js +++ b/backend/server.js @@ -97,9 +97,9 @@ try console.log('Listening at localhost, port ' + HTTP_PORT); console.log('\nUsage: http://localhost:' + HTTP_PORT + TOPLEVELPATH + "/SERVICENAME/SERVICEMETHOD/...."); console.log('\nPlant Manager Backend \nDeveloped by: QuadcoreDevelopment'); - console.log('\n\n-----------------------------------------'); - console.log('exit / stop Server by pressing 2 x CTRL-C'); - console.log('-----------------------------------------\n\n'); + console.log('\n\n-------------------------------------'); + console.log('exit / stop Server by pressing CTRL-C'); + console.log('-------------------------------------\n\n'); }); } catch (ex) diff --git a/backend/services/activities.js b/backend/services/activities.js index 19776eb..f101796 100644 --- a/backend/services/activities.js +++ b/backend/services/activities.js @@ -1,120 +1,68 @@ const helper = require('../helper.js'); const validationHelper = require('./validationHelper.js'); const activitiesDao = require('../dao/activitiesDao.js'); -const plantsDao = require('../dao/plantsDao.js'); const express = require('express'); var serviceRouter = express.Router(); -const { body, validationResult } = require('express-validator'); +const { body, param, matchedData, validationResult } = require('express-validator'); console.log('- Service Activities'); // Neue Activity erstellen // TODO Funktion weiter ausbauen serviceRouter.post('/activities', -body("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), -body("type").isInt({min:0, max:1}), -body("date").optional().isISO8601().toDate(), -function(request, response) { + body("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), + body("type").isInt({min:0, max:1}).toInt(), + body("date").optional().isISO8601(), + function(req, resp) { + console.log('Activities plants: Client requested creation of new activity'); - const result = validationResult(request); + const result = validationResult(req); if (!result.isEmpty()) { - return response.status(400).json({ errors: result.array() }); + console.warn('Service activities: Creation not possible, validation errors'); + return resp.status(400).json({ errors: result.array() }); } - response.send(`Hello, ${request.body.plant_id}!`); -}); -// Neue Activity erstellen (old) -serviceRouter.post('/activities_old', function(request, response) { - console.log('Activities plants: Client requested creation of new record'); - - var errorMsgs=[]; - if (helper.isUndefined(request.body.plant_id)) { - errorMsgs.push('plant_id missing'); - } - if (request.body.plant_id < 0) { - errorMsgs.push('plant_id cannot be negative'); - } - // checks if plant_id starts with a number but is a string - if(helper.strHasNumericValue(request.body.plant_id)) { - request.body.plant_id = parseInt(request.body.plant_id, 10); - } - if (helper.isNull(request.body.plant_id)) { - errorMsgs.push('plant_id is null'); - } - if (helper.isUndefined(request.body.type)) { - errorMsgs.push('type missing'); - } - if (request.body.type < 0 || request.body.type > 1) { - errorMsgs.push('type cannot be negative or greater than 1'); - } - if(helper.strHasNumericValue(request.body.type)) { - request.body.type = parseInt(request.body.type, 10); - } - if (helper.isNull(request.body.type)) { - errorMsgs.push('type is null'); - } - // Aktuelles Datum für date nehmen - if (helper.isUndefined(request.body.date)) { - request.body.date = helper.getNow(); - } else { - // Date wenn es ein String ist in ein valides date Object umwandeln - try { - if(helper.isString(request.body.date)) { - request.body.date = helper.parseDateTimeString(request.body.date); - } - } catch (ex) { - errorMsgs.push('DateString could not be transformed into Date Object' + ex); - } - } - - // Typüberprüfung der Werte - if(!helper.isNumeric(request.body.plant_id)) { - errorMsgs.push('plant_id is not a numeric value.'); - } - if(!helper.isNumeric(request.body.type)) { - errorMsgs.push('type is not a numeric value.'); + const data = matchedData(req); + // use current date if date is not provided + if (helper.isUndefined(data.date)) { + data.date = helper.getNow(); } - if(!helper.isDateTime(request.body.date)) { - errorMsgs.push('date is not a valid dateTime format.'); + else { + data.date = helper.parseDateTimeString(data.date); } - - if (errorMsgs.length > 0) { - console.log('Service activities: Creation not possible, data missing'); - response.status(400).json({ 'fehler': true, 'nachricht': 'Function not possible. Missing Data: ' + helper.concatArray(errorMsgs) }); - return; - } - - const activitiesDaoInstance = new activitiesDao(request.app.locals.dbConnection); - const plantsDaoInstance = new plantsDao(request.app.locals.dbConnection); + const activitiesDaoInstance = new activitiesDao(req.app.locals.dbConnection); try { - // Check if plant with given plant_id actually exists - if (plantsDaoInstance.exists(request.body.plant_id)) { - var obj = activitiesDaoInstance.create(request.body.plant_id, request.body.type, request.body.date); - console.log('Service activities: Record inserted'); - response.status(200).json(obj); - } else { - console.error('Service activities: Plant with given ID does not exist.'); - response.status(404).json({ 'fehler': true, 'nachricht': 'Plant with the given ID does not exist.' }); - } + let obj = activitiesDaoInstance.create(data.plant_id, data.type, data.date); + console.log('Service activities: Record inserted'); + resp.status(200).json(obj); } catch (ex) { console.error('Service activities: Error creating new record. Exception occurred: ' + ex.message); - response.status(400).json({ 'fehler': true, 'nachricht': ex.message }); + resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); } }); // Activity nach ID holen -serviceRouter.get('/activities/exists/:id', function(request, response) { - console.log('Service activities: Client requested check, if activity exists, id=' + request.params.id); - - const activitiesDaoInstance = new activitiesDao(request.app.locals.dbConnection); +serviceRouter.get('/activities/exists/:id', + param("id").isInt({min:0}).toInt(), + function(req, resp) { + + console.log('Service activities: Client requested check, if activity exists'); + const result = validationResult(req); + if (!result.isEmpty()) { + console.warn('Service activities: Check not possible, validation errors'); + return resp.status(400).json({ errors: result.array() }); + } + + const data = matchedData(req); + const activitiesDaoInstance = new activitiesDao(req.app.locals.dbConnection); try { - var exists = activitiesDaoInstance.exists(request.params.id); - console.log('Service activities: Check if activity exists by id=' + request.params.id +', exists= ' + exists); - response.status(200).json({'id': parseInt(request.params.id), 'existiert': exists}); + let exists = activitiesDaoInstance.exists(data.id); + console.log('Service activities: Check if activity exists by id=' + data.id +', exists= ' + exists); + resp.status(200).json({'id': data.id, 'exists': exists}); } catch (ex) { console.error('Service activities: Error checking if record exists. Exception occurred: ' + ex.message); - response.status(400).json({ 'fehler': true, 'nachricht': ex.message }); + resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); } }); diff --git a/docs/API.md b/docs/API.md index 792abc5..628ee2a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -63,6 +63,7 @@ Alle HTTP Aufrufe sind englisch und kleingeschrieben! ``` ## Beispiel im Fehlerfall +TODO Rework this after Validator implementation ```JSON { "nachricht": "Fehler: name fehlt", From c7913f4787095724a74db50a3e4c6a15bf922df4 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:54:53 +0100 Subject: [PATCH 06/38] #41 WIP activities by plant_id --- backend/services/activities.js | 59 +++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/backend/services/activities.js b/backend/services/activities.js index f101796..f0fd157 100644 --- a/backend/services/activities.js +++ b/backend/services/activities.js @@ -8,7 +8,6 @@ const { body, param, matchedData, validationResult } = require('express-validato console.log('- Service Activities'); // Neue Activity erstellen -// TODO Funktion weiter ausbauen serviceRouter.post('/activities', body("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), body("type").isInt({min:0, max:1}).toInt(), @@ -42,7 +41,7 @@ serviceRouter.post('/activities', } }); -// Activity nach ID holen +// Activity exists check serviceRouter.get('/activities/exists/:id', param("id").isInt({min:0}).toInt(), function(req, resp) { @@ -66,8 +65,60 @@ serviceRouter.get('/activities/exists/:id', } }); -// Alle Activities für Plant_ID holen -serviceRouter.get('/activities/all/:plant_id', function(request, response) { +// Get all activities for a plants +serviceRouter.get('/activities/all/:plant_id', + param("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), + function(req, resp) { + + console.log('Service activities: Client requested all records for a plant'); + const vResult = validationResult(req); + if (!vResult.isEmpty()) { + console.warn('Service activities: Fetch not possible, validation errors'); + return resp.status(400).json({ errors: vResult.array() }); + } + + const data = matchedData(req); + const activitiesDaoInstance = new activitiesDao(req.app.locals.dbConnection); + + data.days_since = data.date - helper.getNow(); //TODO What is this for? + try { + var result = activitiesDaoInstance.loadByPlantId(data.plant_id); + console.log('Service activities: Records loaded, result= ', result); + + var activities = []; + + // Check if result is an array or a single object + if (Array.isArray(result)) { + activities = result; + } else if (result && typeof result === 'object') { + activities = [result]; + } else { + return resp.status(404).json({ errors: [{msg: 'No activities found for the given plant ID.'}] }); + } + + //TODO Hier weiter.. + + // Process each activity + const currentDate = new Date(); + activities.forEach(activity => { + if (activity && activity.date) { + const activityDate = new Date(activity.date); + const timeDifference = currentDate - activityDate; + const daysSince = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); + activity.days_since = daysSince; + } + }); + + response.status(200).json(activities); + } catch (ex) { + console.error('Service activities: Error loading all records. Exception occurred: ' + ex.message); + response.status(400).json({ 'fehler': true, 'nachricht': ex.message }); + } +}); + +// Get all activities for a plants +// TODO Remove this old version later +serviceRouter.get('/activities/all_old/:plant_id', function(request, response) { console.log('Service activities: Client requested all records'); From 1d2c89069baa3394441384be4fcbe938e12e626c Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Wed, 29 Oct 2025 23:39:13 +0100 Subject: [PATCH 07/38] #41 updated 404 response of the backend --- backend/server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/server.js b/backend/server.js index f43ebb0..fed9b65 100644 --- a/backend/server.js +++ b/backend/server.js @@ -85,8 +85,8 @@ try // send default error message if no matching endpoint found app.use(function (request, response) { - console.log('Error occured, 404, resource not found'); - response.status(404).json({'fehler': true, 'nachricht': 'Resource nicht gefunden'}); + console.log('Error occurred, 404, resource not found'); + response.status(404).json({ errors: [{ 'msg': "API Endpoint not found"}] }); }); // =============================== From 0ef0c0286e52041ceb91787bb378a3778eff9bf6 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Wed, 29 Oct 2025 23:43:34 +0100 Subject: [PATCH 08/38] #41 Added helper to calculate days between two days to comply with DRY --- backend/helper.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/helper.js b/backend/helper.js index 8ce68e3..fc56d4f 100644 --- a/backend/helper.js +++ b/backend/helper.js @@ -202,6 +202,18 @@ module.exports.compareDateTimes = function(leftdatetime, rightdatetime) { return 0; } +/** + * Probably not correctly implemented, see issue #39 + * Calculates the number of days between two datetime objects + * @param {*} leftdatetime The left datetime object + * @param {*} rightdatetime The right datetime object + * @returns The number of days between the two datetime objects + */ +module.exports.calculateDaysBetween = function(leftdatetime, rightdatetime) { + const timeDifference = Math.abs(leftdatetime - rightdatetime); + return Math.floor(timeDifference / (1000 * 60 * 60 * 24)); +} + // modifies a given datetime object // adds or subs values to years, months, days, hours, minutes, seconds // positive values are added, negative ones subbed. 0 values are ignored From d518a1e43275113defd781ccfbe1599fc9dc0831 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Wed, 29 Oct 2025 23:48:04 +0100 Subject: [PATCH 09/38] #41 reimplemented all activities by plant_id --- backend/services/activities.js | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/backend/services/activities.js b/backend/services/activities.js index f0fd157..68cd185 100644 --- a/backend/services/activities.js +++ b/backend/services/activities.js @@ -7,6 +7,21 @@ const { body, param, matchedData, validationResult } = require('express-validato console.log('- Service Activities'); +/** + * Adds the days_since field to each activity in the provided array. + * @param {*} activities The array of activity objects to process + */ +function addDaysSinceToActivities(activities) { + const currentDate = new Date(); + activities.forEach(activity => { + if (activity && activity.date) { + const activityDate = new Date(activity.date); + const daysSince = helper.calculateDaysBetween(activityDate, currentDate); + activity.days_since = daysSince; + } + }); +} + // Neue Activity erstellen serviceRouter.post('/activities', body("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), @@ -80,7 +95,6 @@ serviceRouter.get('/activities/all/:plant_id', const data = matchedData(req); const activitiesDaoInstance = new activitiesDao(req.app.locals.dbConnection); - data.days_since = data.date - helper.getNow(); //TODO What is this for? try { var result = activitiesDaoInstance.loadByPlantId(data.plant_id); console.log('Service activities: Records loaded, result= ', result); @@ -95,24 +109,14 @@ serviceRouter.get('/activities/all/:plant_id', } else { return resp.status(404).json({ errors: [{msg: 'No activities found for the given plant ID.'}] }); } - - //TODO Hier weiter.. // Process each activity - const currentDate = new Date(); - activities.forEach(activity => { - if (activity && activity.date) { - const activityDate = new Date(activity.date); - const timeDifference = currentDate - activityDate; - const daysSince = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); - activity.days_since = daysSince; - } - }); + addDaysSinceToActivities(activities); - response.status(200).json(activities); + resp.status(200).json(activities); } catch (ex) { - console.error('Service activities: Error loading all records. Exception occurred: ' + ex.message); - response.status(400).json({ 'fehler': true, 'nachricht': ex.message }); + console.error('Service activities: Error loading all records based on plant_id. Exception occurred: ' + ex.message); + resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); } }); From 687d2e6e3560579cf8ef39c41ac2e366cb833fdf Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Thu, 30 Oct 2025 00:17:54 +0100 Subject: [PATCH 10/38] #41 reimplemented delete activity --- backend/services/activities.js | 70 ++++++++-------------------------- 1 file changed, 16 insertions(+), 54 deletions(-) diff --git a/backend/services/activities.js b/backend/services/activities.js index 68cd185..39dfa9d 100644 --- a/backend/services/activities.js +++ b/backend/services/activities.js @@ -22,7 +22,7 @@ function addDaysSinceToActivities(activities) { }); } -// Neue Activity erstellen +// Create new activity serviceRouter.post('/activities', body("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), body("type").isInt({min:0, max:1}).toInt(), @@ -120,66 +120,28 @@ serviceRouter.get('/activities/all/:plant_id', } }); -// Get all activities for a plants -// TODO Remove this old version later -serviceRouter.get('/activities/all_old/:plant_id', function(request, response) { - - console.log('Service activities: Client requested all records'); - - const activitiesDaoInstance = new activitiesDao(request.app.locals.dbConnection); - - request.body.days_since = request.body.date - helper.getNow(); - try { - var result = activitiesDaoInstance.loadByPlantId(request.params.plant_id); - console.log('Service activities: Records loaded, result= ', result); +// delete activity by id +serviceRouter.delete('/activities/:id', + param("id").isInt({min:0}).bail().custom(validationHelper.validateActivityIDExists), + function(req, resp) { - var activities = []; - - // Check if result is an array or a single object - if (Array.isArray(result)) { - activities = result; - } else if (result && typeof result === 'object') { - activities = [result]; - } else { - return response.status(404).json({ 'fehler': true, 'nachricht': 'No activities found for the given plant ID.' }); - } - - // Process each activity - const currentDate = new Date(); - activities.forEach(activity => { - if (activity && activity.date) { - const activityDate = new Date(activity.date); - const timeDifference = currentDate - activityDate; - const daysSince = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); - activity.days_since = daysSince; - } - }); - - response.status(200).json(activities); - } catch (ex) { - console.error('Service activities: Error loading all records. Exception occurred: ' + ex.message); - response.status(400).json({ 'fehler': true, 'nachricht': ex.message }); + console.log('Service activities: Client requested deletion of activity'); + const vResult = validationResult(req); + if (!vResult.isEmpty()) { + console.warn('Service activities: Deletion not possible, validation errors'); + return resp.status(400).json({ errors: vResult.array() }); } -}); - -// Activity löschen -serviceRouter.delete('/activities/:id', function(request, response) { - console.log('Service activities: Client requested deletion of activity, id=' + request.params.id); - const activitiesDaoInstance = new activitiesDao(request.app.locals.dbConnection); + const data = matchedData(req); + const activitiesDaoInstance = new activitiesDao(req.app.locals.dbConnection); try { - if (activitiesDaoInstance.exists(request.params.id)) { - activitiesDaoInstance.delete(request.params.id); - console.log('Service activities: Deletion of activity successfull, id=' + request.params.id); - response.status(200).json({ 'fehler': false, 'nachricht': 'Activity deleted' }); - } else { - console.error('Service activities: Activity with given ID does not exist.'); - response.status(404).json({ 'fehler': true, 'nachricht': 'Activity with the given ID does not exist.' }); - } + activitiesDaoInstance.delete(data.id); + console.log('Service activities: Deletion of activity successful, id=' + data.id); + resp.status(200).json({'id': data.id, 'deleted': true}); } catch (ex) { console.error('Service activities: Error deleting record. Exception occurred: ' + ex.message); - response.status(400).json({ 'fehler': true, 'nachricht': ex.message }); + resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); } }); From dca78bffd1dcb96b9c6fe55bf4a18f9e1eb45a9c Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Thu, 30 Oct 2025 00:21:16 +0100 Subject: [PATCH 11/38] replaced var with let in activities service --- backend/services/activities.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/services/activities.js b/backend/services/activities.js index 39dfa9d..6f06baf 100644 --- a/backend/services/activities.js +++ b/backend/services/activities.js @@ -2,7 +2,7 @@ const helper = require('../helper.js'); const validationHelper = require('./validationHelper.js'); const activitiesDao = require('../dao/activitiesDao.js'); const express = require('express'); -var serviceRouter = express.Router(); +let serviceRouter = express.Router(); const { body, param, matchedData, validationResult } = require('express-validator'); console.log('- Service Activities'); @@ -96,10 +96,10 @@ serviceRouter.get('/activities/all/:plant_id', const activitiesDaoInstance = new activitiesDao(req.app.locals.dbConnection); try { - var result = activitiesDaoInstance.loadByPlantId(data.plant_id); + let result = activitiesDaoInstance.loadByPlantId(data.plant_id); console.log('Service activities: Records loaded, result= ', result); - var activities = []; + let activities = []; // Check if result is an array or a single object if (Array.isArray(result)) { From 8ed1a039405d84fc4c04fe276137e86d753d495e Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:45:47 +0100 Subject: [PATCH 12/38] #41 reimplemented get plant by id and fixed some spelling --- .vscode/settings.json | 5 ++++- backend/services/plants.js | 37 ++++++++++++++++++++++++------------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e75b1e7..159f91b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "cSpell.language": "en,de-DE" + "cSpell.language": "en,de-DE", + "cSpell.words": [ + "repotted" + ] } \ No newline at end of file diff --git a/backend/services/plants.js b/backend/services/plants.js index 5bf0686..ff78492 100644 --- a/backend/services/plants.js +++ b/backend/services/plants.js @@ -1,8 +1,10 @@ const helper = require('../helper.js'); +const validationHelper = require('./validationHelper.js'); const express = require('express'); const plantsDao = require('../dao/plantsDao.js'); const activitiesDao = require('../dao/activitiesDao.js'); var serviceRouter = express.Router(); +const { body, param, matchedData, validationResult } = require('express-validator'); console.log('- Service Plants'); @@ -13,8 +15,8 @@ function extendPlantJSON(json,activitiesDaoInstance) { // Berechnung watering_interval_calculated let watering_interval_calculated = json.watering_interval + json.watering_interval_offset; - // Datum letestes Bewässern ermitteln - var arrWat = activitiesDaoInstance.loadByPlantIdAndType(json.plant_id,0); + // Datum letztes Bewässern ermitteln + let arrWat = activitiesDaoInstance.loadByPlantIdAndType(json.plant_id,0); let last_watered = null; if(helper.isArray(arrWat)) { @@ -25,7 +27,7 @@ function extendPlantJSON(json,activitiesDaoInstance) { } else { - // 2 or more elemts = array + // 2 or more elements = array last_watered = new Date(arrWat[0].date); } } @@ -53,7 +55,7 @@ function extendPlantJSON(json,activitiesDaoInstance) { let days_until_watering = Math.floor(ms_until_watering / (1000 * 60 * 60 * 24)) +1; //Bestimmung repotted - var arrPot = activitiesDaoInstance.loadByPlantIdAndType(json.plant_id,1); + let arrPot = activitiesDaoInstance.loadByPlantIdAndType(json.plant_id,1); let repotted = null; if(helper.isArray(arrPot)) { @@ -64,7 +66,7 @@ function extendPlantJSON(json,activitiesDaoInstance) { } else { - // 2 or more elemts = array + // 2 or more elements = array repotted = arrPot[0].date; } } @@ -85,22 +87,31 @@ function extendPlantJSON(json,activitiesDaoInstance) { json.repotted = repotted; } -serviceRouter.get('/plants/get/:id', function(request, response) { - console.log('Service plants: Client requested one record, id=' + request.params.id); +serviceRouter.get('/plants/get/:id', + param("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), + function(req, resp) { - const plantDaoInstance = new plantsDao(request.app.locals.dbConnection); - const activitiesDaoInstance = new activitiesDao(request.app.locals.dbConnection); + console.log('Service plants: Client requested one record'); + const vResult = validationResult(req); + if (!vResult.isEmpty()) { + console.warn('Service plants: Error loading record by id, validation errors'); + return resp.status(400).json({ errors: vResult.array() }); + } + + const data = matchedData(req); + const plantDaoInstance = new plantsDao(req.app.locals.dbConnection); + const activitiesDaoInstance = new activitiesDao(req.app.locals.dbConnection); try { // JSON Objekt aus DB holen - var obj = plantDaoInstance.loadById(request.params.id); + var obj = plantDaoInstance.loadById(data.id); extendPlantJSON(obj,activitiesDaoInstance); console.log('Service plants: Record loaded'); - response.status(200).json(obj); + resp.status(200).json(obj); } catch (ex) { - console.error('Service plants: Error loading record by id. Exception occured: ' + ex.message); - response.status(400).json({ 'fehler': true, 'nachricht': ex.message }); + console.error('Service plants: Error loading record by id. Exception occurred: ' + ex.message); + resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); } }); From ed00b014bbfd56504f10fdadc7ab253975806d81 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:50:02 +0100 Subject: [PATCH 13/38] #41 fixed wrong variable name in last commit --- backend/services/plants.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/services/plants.js b/backend/services/plants.js index ff78492..e1499de 100644 --- a/backend/services/plants.js +++ b/backend/services/plants.js @@ -87,7 +87,7 @@ function extendPlantJSON(json,activitiesDaoInstance) { json.repotted = repotted; } -serviceRouter.get('/plants/get/:id', +serviceRouter.get('/plants/get/:plant_id', param("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), function(req, resp) { @@ -103,7 +103,7 @@ serviceRouter.get('/plants/get/:id', const activitiesDaoInstance = new activitiesDao(req.app.locals.dbConnection); try { // JSON Objekt aus DB holen - var obj = plantDaoInstance.loadById(data.id); + var obj = plantDaoInstance.loadById(data.plant_id); extendPlantJSON(obj,activitiesDaoInstance); From 2d5cde5ff1d11d0108b1bd83214cc26a84500c3e Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:57:26 +0100 Subject: [PATCH 14/38] #41 reimplemented get composted plants --- backend/services/plants.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/services/plants.js b/backend/services/plants.js index e1499de..ae2c57a 100644 --- a/backend/services/plants.js +++ b/backend/services/plants.js @@ -115,11 +115,11 @@ serviceRouter.get('/plants/get/:plant_id', } }); -serviceRouter.get('/plants/composted', function(request, response) { - console.log('Service plants: Client requested all records'); +serviceRouter.get('/plants/composted', function(req, resp) { + console.log('Service plants: Client requested all composted records'); - const plantDaoInstance = new plantsDao(request.app.locals.dbConnection); - const activitiesDaoInstance = new activitiesDao(request.app.locals.dbConnection); + const plantDaoInstance = new plantsDao(req.app.locals.dbConnection); + const activitiesDaoInstance = new activitiesDao(req.app.locals.dbConnection); try { var plantArr = plantDaoInstance.loadAllComposted(); // foreach Schleife über alle plant JSON, diese werden dabei erweitert @@ -127,11 +127,11 @@ serviceRouter.get('/plants/composted', function(request, response) { extendPlantJSON(plant,activitiesDaoInstance); }); - console.log('Service plants: Records loaded, count= ' + plantArr.length); - response.status(200).json(plantArr); + console.log('Service plants: Composted records loaded, count= ' + plantArr.length); + throw Error('Test error handling'); } catch (ex) { - console.error('Service plants: Error loading all records. Exception occured: ' + ex.message); - response.status(400).json({ 'fehler': true, 'nachricht': ex.message }); + console.error('Service plants: Error loading all composted records. Exception occurred: ' + ex.message); + resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); } }); From 6071cc4d5818e21103aaa83f39a85b4dc4522b58 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:59:23 +0100 Subject: [PATCH 15/38] #41 reimplemented get all plants --- backend/services/plants.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/services/plants.js b/backend/services/plants.js index ae2c57a..b87322b 100644 --- a/backend/services/plants.js +++ b/backend/services/plants.js @@ -135,11 +135,11 @@ serviceRouter.get('/plants/composted', function(req, resp) { } }); -serviceRouter.get('/plants/all', function(request, response) { +serviceRouter.get('/plants/all', function(req, resp) { console.log('Service plants: Client requested all records'); - const plantDaoInstance = new plantsDao(request.app.locals.dbConnection); - const activitiesDaoInstance = new activitiesDao(request.app.locals.dbConnection); + const plantDaoInstance = new plantsDao(req.app.locals.dbConnection); + const activitiesDaoInstance = new activitiesDao(req.app.locals.dbConnection); try { var plantArr = plantDaoInstance.loadAll(); // foreach Schleife über alle plant JSON, diese werden dabei erweitert @@ -148,10 +148,10 @@ serviceRouter.get('/plants/all', function(request, response) { }); console.log('Service plants: Records loaded, count= ' + plantArr.length); - response.status(200).json(plantArr); + resp.status(200).json(plantArr); } catch (ex) { - console.error('Service plants: Error loading all records. Exception occured: ' + ex.message); - response.status(400).json({ 'fehler': true, 'nachricht': ex.message }); + console.error('Service plants: Error loading all records. Exception occurred: ' + ex.message); + resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); } }); From a03a8e8a7918a25076c9f03440e532b8e0b7fd5b Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Thu, 6 Nov 2025 23:10:18 +0100 Subject: [PATCH 16/38] #41 reimplemented plant exists --- backend/services/plants.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/backend/services/plants.js b/backend/services/plants.js index b87322b..e3b4cf8 100644 --- a/backend/services/plants.js +++ b/backend/services/plants.js @@ -155,17 +155,26 @@ serviceRouter.get('/plants/all', function(req, resp) { } }); -serviceRouter.get('/plants/exists/:id', function(request, response) { - console.log('Service plants: Client requested check, if record exists, id=' + request.params.id); +serviceRouter.get('/plants/exists/:plant_id', + param("plant_id").isInt({min:0}).toInt(), + function(req, resp) { - const plantDaoInstance = new plantsDao(request.app.locals.dbConnection); + console.log('Service plants: Client requested check, if record exists'); + const vResult = validationResult(req); + if (!vResult.isEmpty()) { + console.warn('Service plants: Error checking if plant exists, validation errors'); + return resp.status(400).json({ errors: vResult.array() }); + } + + const data = matchedData(req); + const plantDaoInstance = new plantsDao(req.app.locals.dbConnection); try { - var exists = plantDaoInstance.exists(request.params.id); - console.log('Service plants: Check if record exists by id=' + request.params.id +', exists= ' + exists); - response.status(200).json({'plant_id': request.params.id, 'existiert': exists}); + var exists = plantDaoInstance.exists(data.plant_id); + console.log('Service plants: Check if record exists by id=' + data.plant_id +', exists= ' + exists); + resp.status(200).json({'plant_id': data.plant_id, 'exists': exists}); } catch (ex) { - console.error('Service plants: Error checking if record exists. Exception occured: ' + ex.message); - response.status(400).json({ 'fehler': true, 'nachricht': ex.message }); + console.error('Service plants: Error checking if record exists. Exception occurred: ' + ex.message); + resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); } }); From da2da048ab85d0e77d93eed2985f694851c077d0 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Sat, 8 Nov 2025 23:24:19 +0100 Subject: [PATCH 17/38] #41 reimplemented new plant post --- backend/services/plants.js | 58 ++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/backend/services/plants.js b/backend/services/plants.js index e3b4cf8..f612a33 100644 --- a/backend/services/plants.js +++ b/backend/services/plants.js @@ -178,48 +178,40 @@ serviceRouter.get('/plants/exists/:plant_id', } }); -serviceRouter.post('/plants', function(request, response) { - console.log('Service plants: Client requested creation of new record'); +serviceRouter.post('/plants', + body("name").default("New Plant").isString().notEmpty().trim().escape(), + body("species_name").default("Unknown").isString().notEmpty().trim().escape(), + body("watering_interval").isInt({min:1,max:100}).toInt(), + body("watering_interval_offset").isInt({min:-25,max:25}).toInt(), + body("added").optional().isISO8601(), + function(req, resp) { - var errorMsgs=[]; - if (helper.isUndefined(request.body.name)) { - errorMsgs.push('name missing'); - } - if (helper.isUndefined(request.body.species_name)) { - errorMsgs.push('species_name missing'); - } - // Nimmt aktuelles Datum für added, kann noch anders strukturiert werden - if (helper.isUndefined(request.body.added)) { - request.body.added = helper.getNow(); - } - if (helper.isUndefined(request.body.watering_interval)) { - errorMsgs.push('watering_interval missing'); - } else if (!helper.isNumeric(request.body.watering_interval)) { - errorMsgs.push('watering_interval has to be a number'); - } else if (request.body.watering_interval <= 0) { - errorMsgs.push('watering_interval has to be a bigger number than 0'); + console.log('Service plants: Client requested creation of new record'); + const vResult = validationResult(req); + if (!vResult.isEmpty()) { + console.warn('Service plants: Error creating new record, validation errors'); + return resp.status(400).json({ errors: vResult.array() }); } - if (helper.isUndefined(request.body.watering_interval_offset)) { - errorMsgs.push('watering_interval_offset missing'); - } else if (!helper.isNumeric(request.body.watering_interval_offset)) { - errorMsgs.push('watering_interval has to be a number'); + const data = matchedData(req); + + // use current date if date is not provided + if (helper.isUndefined(data.added)) { + data.added = helper.getNow(); } - if (errorMsgs.length > 0) { - console.log('Service plants: Creation not possible, data missing'); - response.status(400).json({ 'fehler': true, 'nachricht': 'Function not possible. Missing Data: ' + helper.concatArray(errorMsgs) }); - return; + else { + data.added = helper.parseDateTimeString(data.added); } - const plantDaoInstance = new plantsDao(request.app.locals.dbConnection); + const plantDaoInstance = new plantsDao(req.app.locals.dbConnection); try { // #37 image is set to null - var obj = plantDaoInstance.create(request.body.name, request.body.species_name,null,request.body.added,request.body.watering_interval,request.body.watering_interval_offset); + var obj = plantDaoInstance.create(data.name, data.species_name,null,data.added,data.watering_interval,data.watering_interval_offset); console.log('Service plants: Record inserted'); - response.status(200).json(obj); + resp.status(200).json(obj); } catch (ex) { - console.error('Service plants: Error creating new record. Exception occured: ' + ex.message); - response.status(400).json({ 'fehler': true, 'nachricht': ex.message }); - } + console.error('Service plants: Error creating new record. Exception occurred: ' + ex.message); + resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); + } }); serviceRouter.put('/plants', function(request, response) { From 5949d2b47d21ed7a124ed5a6788050888aa38eb2 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Sat, 8 Nov 2025 23:28:14 +0100 Subject: [PATCH 18/38] #41 added TODOs in backend --- backend/services/plants.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/services/plants.js b/backend/services/plants.js index f612a33..2bebec9 100644 --- a/backend/services/plants.js +++ b/backend/services/plants.js @@ -214,10 +214,12 @@ serviceRouter.post('/plants', } }); +// TODO Add validation with express-validator serviceRouter.put('/plants', function(request, response) { console.log('Service plants: Client requested update of existing plant'); // TODO Replace this madness with a validation Framework like express validator + // TODO Add validation for composted Date const plantDaoInstance = new plantsDao(request.app.locals.dbConnection); var errorMsgs=[]; if (helper.isUndefined(request.body.plant_id)) { @@ -278,6 +280,7 @@ serviceRouter.put('/plants', function(request, response) { } }); +// TODO Add validation with express-validator serviceRouter.delete('/plants/:id', function(request, response) { console.log('Service plants: Client requested deletion of plant, plant_id=' + request.params.id); From e2b46d890f4ed4e6edeef3b3f9c790505abd1058 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Sun, 9 Nov 2025 15:18:16 +0100 Subject: [PATCH 19/38] #41 WIP reimplementation of put plant - can't set composted back to null - untested change - noticed that plantDAO is inconsistent - noticed that plantDAO uses var --- backend/dao/plantsDao.js | 5 ++ backend/services/plants.js | 101 ++++++++++++++++++------------------- 2 files changed, 55 insertions(+), 51 deletions(-) diff --git a/backend/dao/plantsDao.js b/backend/dao/plantsDao.js index 6e7692a..420ceae 100644 --- a/backend/dao/plantsDao.js +++ b/backend/dao/plantsDao.js @@ -2,6 +2,11 @@ const helper = require('../helper.js'); const daoHelper = require('./daoHelper.js'); +// TODO Add JSDoc comments to all methods +// TODO Make date handling consistent (either use strings or DateTime objects throughout the DAO) +// Post uses DateTime objects, Put uses strings +// Strings is probably better because the validation middleware uses strings +// TODO change var to const/let /** * Data Access Object for plants diff --git a/backend/services/plants.js b/backend/services/plants.js index 2bebec9..b3b23c0 100644 --- a/backend/services/plants.js +++ b/backend/services/plants.js @@ -214,69 +214,68 @@ serviceRouter.post('/plants', } }); -// TODO Add validation with express-validator -serviceRouter.put('/plants', function(request, response) { +// TODO It musst be possible to set composted to null again +serviceRouter.put('/plants', + body("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), + body("name").optional().isString().notEmpty().trim().escape(), + body("species_name").optional().isString().notEmpty().trim().escape(), + body("watering_interval").optional().isInt({min:1,max:100}).toInt(), + body("watering_interval_offset").optional().isInt({min:-25,max:25}).toInt(), + body("composted").optional({values: ['undefined', 'null']}).isISO8601(), + function(req, resp) { + + // Evaluate request console.log('Service plants: Client requested update of existing plant'); - - // TODO Replace this madness with a validation Framework like express validator - // TODO Add validation for composted Date - const plantDaoInstance = new plantsDao(request.app.locals.dbConnection); - var errorMsgs=[]; - if (helper.isUndefined(request.body.plant_id)) { - errorMsgs.push('plant_id missing'); - } else if (!helper.isNumeric(request.body.plant_id)) { - errorMsgs.push('plant_id has to be a number'); - } else if (request.body.plant_id <= 0) { - errorMsgs.push('plant_id has to be a bigger number than 0'); + const vResult = validationResult(req); + if (!vResult.isEmpty()) { + console.warn('Service plants: Error updating record, validation errors'); + return resp.status(400).json({ errors: vResult.array() }); } + const data = matchedData(req); + const plantDaoInstance = new plantsDao(req.app.locals.dbConnection); - if (helper.isUndefined(request.body.name)) { - errorMsgs.push('name missing'); - } + // Load existing plant data + let oldPlantData; + try { + oldPlantData = plantDaoInstance.loadById(data.plant_id) + console.log('Service plants: Loaded existing plant data for update, plant_id=' + data.plant_id); + } catch (ex) { + console.error('Service plants: Error loading old plant data. Exception occurred: ' + ex.message); + resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); + } - if (helper.isUndefined(request.body.species_name)) { - errorMsgs.push('species_name missing'); + // Check which fields need to be updated + if (helper.isUndefined(data.name)) { + // use current name from DB + data.name = oldPlantData.name; } - - if (helper.isUndefined(request.body.watering_interval)) { - errorMsgs.push('watering_interval missing'); - } else if (!helper.isNumeric(request.body.watering_interval)) { - errorMsgs.push('watering_interval has to be a number'); - } else if (request.body.watering_interval <= 0) { - errorMsgs.push('watering_interval has to be a number bigger than 0'); + if (helper.isUndefined(data.species_name)) { + // use current species_name from DB + data.species_name = oldPlantData.species_name; } - - if (helper.isUndefined(request.body.watering_interval_offset)) { - errorMsgs.push('watering_interval_offset missing'); - } else if (!helper.isNumeric(request.body.watering_interval_offset)) { - errorMsgs.push('watering_interval has to be a number'); + if (helper.isUndefined(data.watering_interval)) { + // use current watering_interval from DB + data.watering_interval = oldPlantData.watering_interval; } - - if (errorMsgs.length > 0) { - console.log('Service plants: Update not possible, data missing or invalid'); - response.status(400).json({ 'fehler': true, 'nachricht': 'Function not possible. Missing or invalid data: ' + helper.concatArray(errorMsgs) }); - return; + if (helper.isUndefined(data.watering_interval_offset)) { + // use current watering_interval_offset from DB + data.watering_interval_offset = oldPlantData.watering_interval_offset; + } + if (helper.isUndefined(data.composted)) { + // use current composted from DB + data.composted = oldPlantData.composted; } + // get the current image of the plant as it should not be changed here. See issue #37 + data.image = oldPlantData.image; try { - // check if the plant even exists - if(!plantDaoInstance.exists(request.body.plant_id)) - { - console.error('Service plants: Error updating record by plant_id. No plant with this plant_id found'); - response.status(400).json({ 'fehler': true, 'nachricht': 'No plant with this plant_id found' }); - return; - } - - // get the current image of the plant as it should not be changed here. See issue #37 - let image = plantDaoInstance.loadById(request.body.plant_id).image; - // update the plant - var obj = plantDaoInstance.update(request.body.plant_id,request.body.name, request.body.species_name,image,request.body.watering_interval,request.body.watering_interval_offset,request.body.composted); - console.log('Service plants: Record updated, plant_id=' + request.body.plant_id); - response.status(200).json(obj); + let obj = plantDaoInstance.update(data.plant_id, data.name, data.species_name, data.image, data.watering_interval, data.watering_interval_offset, data.composted); + console.log('Service plants: Record updated, plant_id=' + data.plant_id); + resp.status(200).json(obj); } catch (ex) { - console.error('Service plants: Error updating record by plant_id. Exception occured: ' + ex.message); - response.status(400).json({ 'fehler': true, 'nachricht': ex.message }); + console.error('Service plants: Error updating record by plant_id. Exception occurred: ' + ex.message); + resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); } }); From c898c07172cd6fb8d52de10d61abd6e28862f2c6 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Sun, 9 Nov 2025 19:54:29 +0100 Subject: [PATCH 20/38] #41 removed test error --- backend/services/plants.js | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/services/plants.js b/backend/services/plants.js index b3b23c0..49d2434 100644 --- a/backend/services/plants.js +++ b/backend/services/plants.js @@ -128,7 +128,6 @@ serviceRouter.get('/plants/composted', function(req, resp) { }); console.log('Service plants: Composted records loaded, count= ' + plantArr.length); - throw Error('Test error handling'); } catch (ex) { console.error('Service plants: Error loading all composted records. Exception occurred: ' + ex.message); resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); From 0bbbb2313d12b4dfd02bfdda925054a9f4d2bd7f Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Sun, 9 Nov 2025 20:04:02 +0100 Subject: [PATCH 21/38] #41 fixed get all composted plants --- backend/services/plants.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/services/plants.js b/backend/services/plants.js index 49d2434..2e38714 100644 --- a/backend/services/plants.js +++ b/backend/services/plants.js @@ -128,6 +128,7 @@ serviceRouter.get('/plants/composted', function(req, resp) { }); console.log('Service plants: Composted records loaded, count= ' + plantArr.length); + resp.status(200).json(plantArr); } catch (ex) { console.error('Service plants: Error loading all composted records. Exception occurred: ' + ex.message); resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); From 679e5881cdddccaa6c62cc9a2907a7c72a9396cf Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Sun, 9 Nov 2025 20:21:55 +0100 Subject: [PATCH 22/38] #41 reimplemented put plant update --- backend/services/plants.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/backend/services/plants.js b/backend/services/plants.js index 2e38714..c5c0010 100644 --- a/backend/services/plants.js +++ b/backend/services/plants.js @@ -4,7 +4,7 @@ const express = require('express'); const plantsDao = require('../dao/plantsDao.js'); const activitiesDao = require('../dao/activitiesDao.js'); var serviceRouter = express.Router(); -const { body, param, matchedData, validationResult } = require('express-validator'); +const { body, param, matchedData, validationResult, oneOf } = require('express-validator'); console.log('- Service Plants'); @@ -214,14 +214,13 @@ serviceRouter.post('/plants', } }); -// TODO It musst be possible to set composted to null again serviceRouter.put('/plants', body("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), body("name").optional().isString().notEmpty().trim().escape(), body("species_name").optional().isString().notEmpty().trim().escape(), body("watering_interval").optional().isInt({min:1,max:100}).toInt(), body("watering_interval_offset").optional().isInt({min:-25,max:25}).toInt(), - body("composted").optional({values: ['undefined', 'null']}).isISO8601(), + body("composted").optional({values: "null"}).isISO8601(), function(req, resp) { // Evaluate request @@ -261,10 +260,17 @@ serviceRouter.put('/plants', // use current watering_interval_offset from DB data.watering_interval_offset = oldPlantData.watering_interval_offset; } - if (helper.isUndefined(data.composted)) { + + // null is not passed by express-validator, so we need to check the original req.body + if (helper.isNull(req.body.composted)) { + // set composted to null + data.composted = null; + } + else if (helper.isUndefined(data.composted)) { // use current composted from DB data.composted = oldPlantData.composted; } + // get the current image of the plant as it should not be changed here. See issue #37 data.image = oldPlantData.image; From 6bdd50e55d5e6a8dd3cd6050560f5e3d394dccfe Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Mon, 10 Nov 2025 19:55:05 +0100 Subject: [PATCH 23/38] #41 reimplemented delete plant --- backend/services/plants.js | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/backend/services/plants.js b/backend/services/plants.js index c5c0010..b9456f3 100644 --- a/backend/services/plants.js +++ b/backend/services/plants.js @@ -4,7 +4,7 @@ const express = require('express'); const plantsDao = require('../dao/plantsDao.js'); const activitiesDao = require('../dao/activitiesDao.js'); var serviceRouter = express.Router(); -const { body, param, matchedData, validationResult, oneOf } = require('express-validator'); +const { body, param, matchedData, validationResult } = require('express-validator'); console.log('- Service Plants'); @@ -285,18 +285,26 @@ serviceRouter.put('/plants', } }); -// TODO Add validation with express-validator -serviceRouter.delete('/plants/:id', function(request, response) { - console.log('Service plants: Client requested deletion of plant, plant_id=' + request.params.id); +serviceRouter.delete('/plants/:plant_id', + param("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), + function(req, resp) { + + console.log('Service plants: Client requested deletion of plant'); + const vResult = validationResult(req); + if (!vResult.isEmpty()) { + console.warn('Service plants: Error deleting plant, validation errors'); + return resp.status(400).json({ errors: vResult.array() }); + } + const data = matchedData(req); - const plantDaoInstance = new plantsDao(request.app.locals.dbConnection); + const plantDaoInstance = new plantsDao(req.app.locals.dbConnection); try { - plantDaoInstance.delete(request.params.id); - console.log('Service plants: Deletion of plant successfull, plant_id=' + request.params.id); - response.status(200).json({ 'fehler': false, 'nachricht': 'Plant deleted' }); + plantDaoInstance.delete(data.plant_id); + console.log('Service plants: Deletion of plant successful, plant_id=' + data.plant_id); + resp.status(200).json({'id': data.plant_id, 'deleted': true}); } catch (ex) { - console.error('Service plants: Error deleting record. Exception occured: ' + ex.message); - response.status(400).json({ 'fehler': true, 'nachricht': ex.message }); + console.error('Service plants: Error deleting record. Exception occurred: ' + ex.message); + resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); } }); From 828d8a41e83a9efff45df0e1e2cb127b21144e20 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Mon, 10 Nov 2025 20:19:19 +0100 Subject: [PATCH 24/38] #41 added `.toInt()` after all `bail()` to keep it consistent. Also renamed id to plant_id in the plant deletion response --- backend/services/activities.js | 6 +++--- backend/services/plants.js | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/services/activities.js b/backend/services/activities.js index 6f06baf..3fd142c 100644 --- a/backend/services/activities.js +++ b/backend/services/activities.js @@ -24,7 +24,7 @@ function addDaysSinceToActivities(activities) { // Create new activity serviceRouter.post('/activities', - body("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), + body("plant_id").isInt({min:0}).bail().toInt().custom(validationHelper.validatePlantIDExists), body("type").isInt({min:0, max:1}).toInt(), body("date").optional().isISO8601(), function(req, resp) { @@ -82,7 +82,7 @@ serviceRouter.get('/activities/exists/:id', // Get all activities for a plants serviceRouter.get('/activities/all/:plant_id', - param("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), + param("plant_id").isInt({min:0}).bail().toInt().custom(validationHelper.validatePlantIDExists), function(req, resp) { console.log('Service activities: Client requested all records for a plant'); @@ -122,7 +122,7 @@ serviceRouter.get('/activities/all/:plant_id', // delete activity by id serviceRouter.delete('/activities/:id', - param("id").isInt({min:0}).bail().custom(validationHelper.validateActivityIDExists), + param("id").isInt({min:0}).bail().toInt().custom(validationHelper.validateActivityIDExists), function(req, resp) { console.log('Service activities: Client requested deletion of activity'); diff --git a/backend/services/plants.js b/backend/services/plants.js index b9456f3..2eed831 100644 --- a/backend/services/plants.js +++ b/backend/services/plants.js @@ -88,7 +88,7 @@ function extendPlantJSON(json,activitiesDaoInstance) { } serviceRouter.get('/plants/get/:plant_id', - param("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), + param("plant_id").isInt({min:0}).bail().toInt().custom(validationHelper.validatePlantIDExists), function(req, resp) { console.log('Service plants: Client requested one record'); @@ -215,7 +215,7 @@ serviceRouter.post('/plants', }); serviceRouter.put('/plants', - body("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), + body("plant_id").isInt({min:0}).bail().toInt().custom(validationHelper.validatePlantIDExists), body("name").optional().isString().notEmpty().trim().escape(), body("species_name").optional().isString().notEmpty().trim().escape(), body("watering_interval").optional().isInt({min:1,max:100}).toInt(), @@ -286,7 +286,7 @@ serviceRouter.put('/plants', }); serviceRouter.delete('/plants/:plant_id', - param("plant_id").isInt({min:0}).bail().custom(validationHelper.validatePlantIDExists), + param("plant_id").isInt({min:0}).bail().toInt().custom(validationHelper.validatePlantIDExists), function(req, resp) { console.log('Service plants: Client requested deletion of plant'); @@ -301,7 +301,7 @@ serviceRouter.delete('/plants/:plant_id', try { plantDaoInstance.delete(data.plant_id); console.log('Service plants: Deletion of plant successful, plant_id=' + data.plant_id); - resp.status(200).json({'id': data.plant_id, 'deleted': true}); + resp.status(200).json({'plant_id': data.plant_id, 'deleted': true}); } catch (ex) { console.error('Service plants: Error deleting record. Exception occurred: ' + ex.message); resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); From 600cfa4c88368c81e5f6b36e2698ca488cebc62d Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Mon, 10 Nov 2025 20:23:29 +0100 Subject: [PATCH 25/38] =?UTF-8?q?#41=20updated=20API=20-=20Updated=20the?= =?UTF-8?q?=20Port=20-=20Beispiele=20f=C3=BCr=20Fehlerfall=20und=20Erfolgs?= =?UTF-8?q?fall=20aktualisiert=20-=20Added=20TODO=20to=20upload=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/services/upload.js | 1 + docs/API.md | 76 ++++++++++++++++++++++++++++++-------- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/backend/services/upload.js b/backend/services/upload.js index 7d229e1..457154b 100644 --- a/backend/services/upload.js +++ b/backend/services/upload.js @@ -18,6 +18,7 @@ function deletePublicImage(filenameAndPath){ }); } +// TODO: add validation with express-validator like in other services serviceRouter.put('/upload/image', (request, response) => { console.log('Service Upload: Client uploaded an image'); diff --git a/docs/API.md b/docs/API.md index 628ee2a..89f75da 100644 --- a/docs/API.md +++ b/docs/API.md @@ -2,7 +2,7 @@ - Die jeweiligen Services werden mit einem HTTP Request aufgerufen. Dieser ist wie folgt aufgebaut - `http://[serveradresse]:[port]/[backendname]/[servicename]/[servicemethode]/[opt.Werte]` - Wenn Sie also z.B. den Service „Plants“ aufrufen wollen um Objekte aller Pflanzen zu erhalten verwenden Sie - - `http://localhost:8000/api/plants/all` + - `http://localhost:8001/api/plants/all` # Übersicht über die einzelnen Serviceklassen Folgende Serviceklassen sind implementiert und werden als sogenannte Endpoints eingebunden: @@ -23,24 +23,24 @@ Bestehenden Eintrag in Datenbank löschen Alle HTTP Aufrufe sind englisch und kleingeschrieben! ### Daten abrufen vom Backend (GET) - Objekt vom Typ Plant mit der ID 3 holen -- http://localhost:8000/api/plants/get/3 +- http://localhost:8001/api/plants/get/3 ### Alle Plant – Objekte holen -- http://localhost:8000/api/plants/all +- http://localhost:8001/api/plants/all ### Prüfen ob ein Eintrag mit der ID 7 existiert -- http://localhost:8000/api/plants/exsists/7 +- http://localhost:8001/api/plants/exsists/7 ### Neuen Eintrag erstellen (POST) -- http://localhost:8000/api/plants +- http://localhost:8001/api/plants - Wobei die Daten hier als JSON Objekt vom jeweiligen Typ geliefert werden müssen, ohne ID ### Bestehenden Eintrag ändern (PUT) -- http://localhost:8000/api/plants +- http://localhost:8001/api/plants - Wobei die Daten hier als JSON Objekt vom jeweiligen Typ geliefert werden müssen, mit ID ### Bestehenden Eintrag löschen (DELETE) -- http://localhost:8000/api/plants/4 +- http://localhost:8001/api/plants/4 # Daten an Servicemethoden senden - Entweder als Teil des RESTFul Aufrufs (z.b. in Form einer ID) - - http://localhost:8000/api/plants/get/2 + - http://localhost:8001/api/plants/get/2 - oder als - JSON Objekt,welches im HTTP Body übertragen wird - Bei den JSON Objekten sind alle Eigentschaftsnamen ebenfalls kleingeschrieben. @@ -55,20 +55,66 @@ Alle HTTP Aufrufe sind englisch und kleingeschrieben! - Im Erfolgsfall, wenn die Verarebeitung geklappt hat, ein Objekt mit den angeforderten Daten. - Oder im Fehlerfall ein Objekt mit einer Fehlermeldung -## Beispiel im Erfolgsfall +## Beispiele im Erfolgsfall +### Put oder Get einzelner Daten ```JSON { } ``` - -## Beispiel im Fehlerfall -TODO Rework this after Validator implementation +### Get aller Daten +```JSON +[ + { }, + { }, + { } +] +``` +### Existenzprüfung +```JSON +{ + "plant_id": 1, + "exists": false +} +``` +### Löschen einzelnen Daten +```JSON +{ + "plant_id": 2, + "deleted": true +} +``` +## Beispiele im Fehlerfall +### Validierungsfehler (Error 400) +```JSON +{ + "errors": [ + { + "type": "field", + "value": 0, + "msg": "Plant with the given ID does not exist", + "path": "plant_id", + "location": "body" + }, + { + "type": "field", + "value": "2025-13-32", + "msg": "Invalid value", + "path": "composted", + "location": "body" + } + ] +} +``` +### Server Fehler (Error 500) ```JSON { - "nachricht": "Fehler: name fehlt", - "fehler": true, - "daten": null + "errors": [ + { + "msg": "An error occurred", + "errorMsg": "Test error" + } + ] } ``` From cd2ab119412cd3e302ac8c0047630874477fd064 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Sat, 15 Nov 2025 17:53:22 +0100 Subject: [PATCH 26/38] #41 reimplemented image upload --- backend/services/upload.js | 95 ++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/backend/services/upload.js b/backend/services/upload.js index 457154b..2c3d76a 100644 --- a/backend/services/upload.js +++ b/backend/services/upload.js @@ -1,8 +1,10 @@ const helper = require('../helper.js'); +const validationHelper = require('./validationHelper.js'); const express = require('express'); const plantsDao = require('../dao/plantsDao.js'); const fs = require('fs'); var serviceRouter = express.Router(); +const { body, matchedData, validationResult } = require('express-validator'); console.log('- Service Upload'); @@ -18,68 +20,73 @@ function deletePublicImage(filenameAndPath){ }); } -// TODO: add validation with express-validator like in other services -serviceRouter.put('/upload/image', (request, response) => { +function isValidImageExtension(extension){ + const validExtensions = ['jpg','jpeg','png','gif','bmp','webp','heic']; + return validExtensions.includes(extension.toLowerCase()); +} + +function isValidImageMimeType(mimeType){ + // Based on tests I saw: 'image/jpeg','application/pdf' and 'application/x-msdownload' + const validMimeTypes = ['image/jpg','image/jpeg', 'image/heic','image/png','image/gif','image/bmp','image/webp']; + return validMimeTypes.includes(mimeType); +} + +// Files can't be validated with express-validator directly, so we only validate the plant_id here +serviceRouter.put('/upload/image', + body("plant_id").isInt({min:0}).bail().toInt().custom(validationHelper.validatePlantIDExists), + (req, resp) => { + console.log('Service Upload: Client uploaded an image'); + const vResult = validationResult(req); + if (!vResult.isEmpty()) { + console.warn('Service plants: Error uploading an image, validation errors'); + return resp.status(400).json({ errors: vResult.array() }); + } - const plantDaoInstance = new plantsDao(request.app.locals.dbConnection); - let errorMsgs=[]; - let plant_id = request.body.plant_id; + const plant_id = matchedData(req).plant_id; + const plantDaoInstance = new plantsDao(req.app.locals.dbConnection); + let picture = null; + let extension = "error"; - // Validations + // File Validations try { - if (helper.isUndefined(plant_id)) { - errorMsgs.push('plant_id missing'); - } else if (!helper.isNumeric(plant_id)) { - errorMsgs.push('plant_id has to be a number'); - } else if (plant_id <= 0) { - errorMsgs.push('plant_id has to be a bigger number than 0'); - } else if(!plantDaoInstance.exists(plant_id)) { - errorMsgs.push('no plant with this plant_id'); + if (!req.files || !req.files.picture) { + console.warn('Service Upload: No file was uploaded by the client'); + return resp.status(400).json({ errors: [{msg: 'No picture was uploaded'}] }); + } + picture = req.files.picture; + if(!isValidImageMimeType(picture.mimetype)) + { + console.warn('Service Upload: Client uploaded file with MimeType "' + picture.mimetype + '" but was supposed to upload image'); + return resp.status(400).json({ errors: [{msg: 'The uploaded file had the wrong mimetype'}] }); } - if (!request.files) { - errorMsgs.push('no files provided'); + extension = picture.name.split(".").pop(); + if(!isValidImageExtension(extension)) + { + console.warn('Service Upload: Client uploaded file with extension "' + extension + '" but was supposed to upload image'); + return resp.status(400).json({ errors: [{msg: 'The uploaded file had the wrong extension'}] }); } } catch(ex) { - response.status(400).json({'fehler': true, 'nachricht': 'Error in Validation: ' + ex}); + console.error('Service Upload: Exception during file validation: ' + ex.message); + return resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); } - // Send back result of validation - if (errorMsgs.length > 0) { - console.log('Service Upload: Upload not possible, data missing or invalid'); - response.status(400).json({ 'fehler': true, 'nachricht': 'Function not possible. Missing or invalid Data: ' + helper.concatArray(errorMsgs) }); - return; - } //Get image, save it and update plant try { - // get handle on file info, in this case 'picture' is the HTML Field Name - var picture = request.files.picture; - - // validate mimetype type - // Based on tests iI saw: 'image/jpeg','application/pdf' and 'application/x-msdownload' - let type = picture.mimetype.split("/")[0]; - if(type != "image") - { - console.log('Service Upload: Client uploaded file of type "' + type + '" but was supposed to upload image'); - response.status(400).json({'fehler': true, 'nachricht': 'the file sent was not of type image but "' + type + '"'}); - return; - } - // save file on server - // if target directory is not existent, it is created automatically - // exsisting files will be overwritten! + // if target directory does not exist, it will be created automatically + // existing files will be overwritten! console.log('Service Upload: saving file to target directory on server'); - let extension = picture.name.split(".").pop(); let filename = plant_id + "." + extension; picture.mv('./public/images/plants/' + filename); // about Path Traversal attacks on the .mv: - // - plant_id is validated as number and should thus be safe - // - extension can not contain . because of the split and pop + // - plant_id is validated and sanitized by the express validator and should thus be safe + // - extension is checked against a whitelist and should thus be safe // Update plant to use the uploaded picture let plant = plantDaoInstance.loadById(plant_id); @@ -96,13 +103,13 @@ serviceRouter.put('/upload/image', (request, response) => { // - as long as the filename in the DB is clean this is not an issue } - response.status(200).json({'fehler': false}); + resp.status(200).json({'success': true, 'filename': filename, 'plant_id': plant_id}); } catch(ex) { - response.status(400).json({'fehler': true, 'nachricht': 'Error in Service: ' + ex}); + console.error('Service Upload: Exception while saving file: ' + ex.message); + return resp.status(500).json({ errors: [validationHelper.exceptionToJson(ex)] }); } - }); module.exports = serviceRouter; \ No newline at end of file From cb09a2abd33b6feffc5e34ae6010b87f003bc05e Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Sun, 16 Nov 2025 20:30:42 +0100 Subject: [PATCH 27/38] #41 created customError to batter handle error responses from the backend --- frontend/public/mjs/backend_api.mjs | 42 +++++++++++++--------------- frontend/public/mjs/customErrors.mjs | 24 ++++++++++++++++ 2 files changed, 43 insertions(+), 23 deletions(-) create mode 100644 frontend/public/mjs/customErrors.mjs diff --git a/frontend/public/mjs/backend_api.mjs b/frontend/public/mjs/backend_api.mjs index 183d7ff..97624d1 100644 --- a/frontend/public/mjs/backend_api.mjs +++ b/frontend/public/mjs/backend_api.mjs @@ -1,5 +1,6 @@ import { backendUrl_api } from "./config.mjs"; import * as utils from "./utils.mjs"; +import { BackendError } from "./customErrors.mjs"; /** * async function to create a new plant on the backend. @@ -27,8 +28,8 @@ export async function createPlant(){ // check if it was successful if(res.status !== 200) { - const errorResponse = await res.text(); - throw new Error(`Failed to create plant - Error ${res.status}: ${errorResponse}`); + const errorResponse = await res.json(); + throw new BackendError(`Failed to create plant`,res.status, errorResponse); } console.log("created new plant"); let createdId = JSON.parse(JSON.stringify(await res.json())).plant_id; @@ -68,8 +69,8 @@ export async function createActivity(plant_id, type) // check if it was successful if (res.status !== 200) { - const errorResponse = await res.text(); - throw new Error(`Failed to create activity with type ${type} - Error ${res.status}: ${errorResponse}`); + const errorResponse = await res.json(); + throw new BackendError(`Failed to create activity with type ${type}`,res.status, errorResponse); } else { @@ -119,8 +120,8 @@ export async function deleteActivity(id) { // check if it was successful if(res.status !== 200) { - const errorResponse = await res.text(); - throw new Error(`Failed to delete activity - Error ${res.status}: ${errorResponse}`); + const errorResponse = await res.json(); + throw new BackendError(`Failed to delete activity`,res.status, errorResponse); } else console.log("Activity deleted successfully"); @@ -148,8 +149,8 @@ export async function fetchPlants(onlyCompsted=false) { const res = await fetch(backendUrl_api + endpoint); // check if it was successful if(res.status !== 200) { - const errorResponse = await res.text(); - throw new Error(`Failed to fetch plants - Error ${res.status}: ${errorResponse}`); + const errorResponse = await res.json(); + throw new BackendError(`Failed to fetch plants`,res.status, errorResponse); } else { @@ -176,13 +177,8 @@ export async function fetchPlant(plant_id) { const res = await fetch(backendUrl_api + '/plants/get/' + plant_id); // check if it was successful if(res.status !== 200) { - const errorResponse = await res.text(); - const prefix = "Failed to fetch plant"; - if(errorResponse.includes("No record found")) - { - throw new Error(prefix + ` - plant does not exist`); - } - throw new Error(prefix + ` - Error ${res.status}: ${errorResponse}`); + const errorResponse = await res.json(); + throw new BackendError(`Failed to fetch plant`,res.status, errorResponse); } else { @@ -210,8 +206,8 @@ export async function fetchActivities(plant_id) { const res = await fetch(backendUrl_api + '/activities/all/' + plant_id); // check if it was successful if(res.status !== 200) { - const errorResponse = await res.text(); - throw new Error(`Failed to fetch activities - Error ${res.status}: ${errorResponse}`); + const errorResponse = await res.json(); + throw new BackendError(`Failed to fetch activities`,res.status, errorResponse); } else { @@ -242,8 +238,8 @@ export async function uploadImageForPlant(formData) { // check if it was successful if(res.status !== 200) { - const errorResponse = await res.text(); - throw new Error(`Failed to upload image - Error ${res.status}: ${errorResponse}`); + const errorResponse = await res.json(); + throw new BackendError(`Failed to upload image`,res.status, errorResponse); } else { @@ -272,8 +268,8 @@ export async function deletePlant(plant_id) { // check if it was successful if(res.status !== 200) { - const errorResponse = await res.text(); - throw new Error(`Failed to delete plant - Error ${res.status}: ${errorResponse}`); + const errorResponse = await res.json(); + throw new BackendError(`Failed to delete plant`,res.status, errorResponse); } else { @@ -345,8 +341,8 @@ export async function updatePlant(plant) { // check if it was successful if (res.status !== 200) { - const errorResponse = await res.text(); - throw new Error(`Failed to update plant - Error ${res.status}: ${errorResponse}`); + const errorResponse = await res.json(); + throw new BackendError(`Failed to update plant`,res.status, errorResponse); } else { diff --git a/frontend/public/mjs/customErrors.mjs b/frontend/public/mjs/customErrors.mjs new file mode 100644 index 0000000..6b3ab90 --- /dev/null +++ b/frontend/public/mjs/customErrors.mjs @@ -0,0 +1,24 @@ +/** + * Custom error class to represent backend-related errors. + */ +export class BackendError extends Error { + + /** + * Creates an instance of BackendError. + * @param {string} message A descriptive error message. + * @param {number} httpStatusCode The HTTP status code associated with the error. + * @param {Array} errorArray An array of error details from the backend. + */ + constructor(message, httpStatusCode, errorArray) { + super(message); + + // Maintains proper stack trace for where our error was thrown (non-standard) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, BackendError); + } + + this.name = typeof this; + this.httpStatusCode = httpStatusCode; + this.errorArray = errorArray; + } +} \ No newline at end of file From b5b3ae0ea181143999909e2fece6af4fcd6e4308 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Sun, 16 Nov 2025 21:07:53 +0100 Subject: [PATCH 28/38] added multiline support to alerts display --- frontend/public/mjs/alerts.mjs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/public/mjs/alerts.mjs b/frontend/public/mjs/alerts.mjs index 76f2cfa..410b6dd 100644 --- a/frontend/public/mjs/alerts.mjs +++ b/frontend/public/mjs/alerts.mjs @@ -46,9 +46,14 @@ export function displayAlert(mainMessage, type, secondaryMessage, icon=null) { div.append(strong); if(secondaryMessage != null) { + let parts = secondaryMessage.split('\n'); let secondDiv = $('
'); div.append(secondDiv); - secondDiv.text(secondaryMessage); + for (let i = 0; i < parts.length; i++) { + let p = $('

'); + p.text(parts[i]); + secondDiv.append(p); + } } alert.append($('')); @@ -64,5 +69,5 @@ export function displayAlert(mainMessage, type, secondaryMessage, icon=null) { */ export function displayError(mainMessage, secondaryMessage) { - displayAlert(mainMessage, "danger", secondaryMessage, "bi-x-circle"); + displayAlert(mainMessage, "danger", secondaryMessage, "bi-x-circle-fill"); } \ No newline at end of file From fd9d12619b24c141e64821dd373e12d0a3003492 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Sun, 16 Nov 2025 21:55:24 +0100 Subject: [PATCH 29/38] #41 corrected use of backendError and correctly set the name of the error in the constructor of backendError --- frontend/public/mjs/backend_api.mjs | 18 +++++++++--------- frontend/public/mjs/customErrors.mjs | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/public/mjs/backend_api.mjs b/frontend/public/mjs/backend_api.mjs index 97624d1..a60d7ca 100644 --- a/frontend/public/mjs/backend_api.mjs +++ b/frontend/public/mjs/backend_api.mjs @@ -29,7 +29,7 @@ export async function createPlant(){ // check if it was successful if(res.status !== 200) { const errorResponse = await res.json(); - throw new BackendError(`Failed to create plant`,res.status, errorResponse); + throw new BackendError(`Failed to create plant`,res.status, errorResponse.errors); } console.log("created new plant"); let createdId = JSON.parse(JSON.stringify(await res.json())).plant_id; @@ -70,7 +70,7 @@ export async function createActivity(plant_id, type) // check if it was successful if (res.status !== 200) { const errorResponse = await res.json(); - throw new BackendError(`Failed to create activity with type ${type}`,res.status, errorResponse); + throw new BackendError(`Failed to create activity with type ${type}`,res.status, errorResponse.errors); } else { @@ -121,7 +121,7 @@ export async function deleteActivity(id) { // check if it was successful if(res.status !== 200) { const errorResponse = await res.json(); - throw new BackendError(`Failed to delete activity`,res.status, errorResponse); + throw new BackendError(`Failed to delete activity`,res.status, errorResponse.errors); } else console.log("Activity deleted successfully"); @@ -150,7 +150,7 @@ export async function fetchPlants(onlyCompsted=false) { // check if it was successful if(res.status !== 200) { const errorResponse = await res.json(); - throw new BackendError(`Failed to fetch plants`,res.status, errorResponse); + throw new BackendError(`Failed to fetch plants`,res.status, errorResponse.errors); } else { @@ -178,7 +178,7 @@ export async function fetchPlant(plant_id) { // check if it was successful if(res.status !== 200) { const errorResponse = await res.json(); - throw new BackendError(`Failed to fetch plant`,res.status, errorResponse); + throw new BackendError(`Failed to fetch plant`,res.status, errorResponse.errors); } else { @@ -207,7 +207,7 @@ export async function fetchActivities(plant_id) { // check if it was successful if(res.status !== 200) { const errorResponse = await res.json(); - throw new BackendError(`Failed to fetch activities`,res.status, errorResponse); + throw new BackendError(`Failed to fetch activities`,res.status, errorResponse.errors); } else { @@ -239,7 +239,7 @@ export async function uploadImageForPlant(formData) { // check if it was successful if(res.status !== 200) { const errorResponse = await res.json(); - throw new BackendError(`Failed to upload image`,res.status, errorResponse); + throw new BackendError(`Failed to upload image`,res.status, errorResponse.errors); } else { @@ -269,7 +269,7 @@ export async function deletePlant(plant_id) { // check if it was successful if(res.status !== 200) { const errorResponse = await res.json(); - throw new BackendError(`Failed to delete plant`,res.status, errorResponse); + throw new BackendError(`Failed to delete plant`,res.status, errorResponse.errors); } else { @@ -342,7 +342,7 @@ export async function updatePlant(plant) { // check if it was successful if (res.status !== 200) { const errorResponse = await res.json(); - throw new BackendError(`Failed to update plant`,res.status, errorResponse); + throw new BackendError(`Failed to update plant`,res.status, errorResponse.errors); } else { diff --git a/frontend/public/mjs/customErrors.mjs b/frontend/public/mjs/customErrors.mjs index 6b3ab90..8c2ad13 100644 --- a/frontend/public/mjs/customErrors.mjs +++ b/frontend/public/mjs/customErrors.mjs @@ -17,7 +17,7 @@ export class BackendError extends Error { Error.captureStackTrace(this, BackendError); } - this.name = typeof this; + this.name = "BackendError"; this.httpStatusCode = httpStatusCode; this.errorArray = errorArray; } From af866038e8ab02a55cb81b0c9a95d5948f09e404 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Sun, 16 Nov 2025 22:05:31 +0100 Subject: [PATCH 30/38] #41 completely reworked the error handler to work with the new validation framework and --- frontend/public/mjs/error_handler.mjs | 120 +++++++++++++++++++------- 1 file changed, 88 insertions(+), 32 deletions(-) diff --git a/frontend/public/mjs/error_handler.mjs b/frontend/public/mjs/error_handler.mjs index 8a8aa56..05f843a 100644 --- a/frontend/public/mjs/error_handler.mjs +++ b/frontend/public/mjs/error_handler.mjs @@ -2,72 +2,128 @@ import * as alerts from "./alerts.mjs"; /** * Takes an error aka. exception, generates a user-friendly message for it and displays it. - * Requires a initialized alerts display. + * Requires an initialized alerts display. * @param {Error} error The Error for which a message should be displayed. */ export function handleError(error) { console.error("Handling error:", error); // generate main message - let mainMessage = "Ein unbekannter Fehler ist aufgetreten"; + let mainMessage = generatePrimaryErrorMessage(error); + + // generate secondary message + let secondaryMessage = generateSecondaryErrorMessage(error); + + // Display the error message + alerts.displayError(mainMessage, secondaryMessage); +} + +function generatePrimaryErrorMessage(error) { switch (error.name) { case "TypeError": if (error.message.includes("NetworkError")) { - mainMessage = "Fehler bei der Kommunikation mit dem Backend"; + return "Fehler bei der Kommunikation mit dem Backend"; } break; - default: + case "BackendError": if (error.message.includes("Failed to fetch plants")) { - mainMessage = "Fehler beim Abrufen der Pflanzen"; + return "Fehler beim Abrufen der Pflanzen"; } else if (error.message.includes("Failed to fetch plant")) { - mainMessage = "Fehler beim Abrufen der Pflanze"; + return "Fehler beim Abrufen der Pflanze"; } else if (error.message.includes("Failed to create plant")) { - mainMessage = "Konnte keine neue Pflanze hinzufügen"; + return "Konnte keine neue Pflanze hinzufügen"; } else if (error.message.includes("Failed to fetch activities")) { - mainMessage = "Konnte keine Aktivitäten zu dieser Pflanze abrufen"; + return "Konnte keine Aktivitäten zu dieser Pflanze abrufen"; } else if (error.message.includes("Failed to upload image")) { - mainMessage = "Fehler beim Hochladen des Bildes"; + return "Fehler beim Hochladen des Bildes"; } else if (error.message.includes("Failed to delete plant")) { - mainMessage = "Fehler beim Löschen der Pflanze"; + return "Fehler beim Löschen der Pflanze"; } else if (error.message.includes("Failed to delete activity")) { - mainMessage = "Fehler beim Löschen der Aktivität"; + return "Fehler beim Löschen der Aktivität"; } else if (error.message.includes("Failed to update plant")) { - mainMessage = "Fehler beim aktualisieren der Pflanze"; + return "Fehler beim aktualisieren der Pflanze"; } else if (error.message.includes("Failed to create activity")) { if(error.message.includes("type 0")) { - mainMessage = "Konnte die Pflanze nicht bewässern"; + return "Konnte die Pflanze nicht bewässern"; } else if(error.message.includes("type 1")) { - mainMessage = "Konnte die Pflanze nicht umtopfen"; + return "Konnte die Pflanze nicht umtopfen"; } else{ - mainMessage = "Konnte keine neue Aktivität anlegen"; + return "Konnte keine neue Aktivität anlegen"; } } break; } - // generate secondary message - let secondaryMessage = null; - - if (error.message.includes("Error 404")) { - secondaryMessage = "Das Backend unterstützt die benötigte API nicht. Möglicherweise wird ein Update benötigt."; - } else if (error.message.includes("Error 400")) { - if (error.message.includes("was not of type image")) { - secondaryMessage = "Es können nur Bild Dateien hochgeladen werden. Bitte wählen Sie eine andere Datei."; - } else if (error.message.includes("Missing or invalid data")) { - secondaryMessage = "Die eingegebenen Daten sind ungültig oder unvollständig"; - } else { - secondaryMessage = "Das Backend konnte die Anfrage nicht verarbeiten. Möglicherweise benötigt das Backend weitere Informationen oder das Frontend ist nicht kompatibel."; - } - } else if (error.message.includes("plant does not exist")) { - secondaryMessage = "Die gewünschte Pflanze existiert nicht. Möglicherweise wurde sie gelöscht."; + return "Ein unbekannter Fehler ist aufgetreten"; +} + +function generateSecondaryErrorMessage(error) { + + switch (error.name) { + case "TypeError": + if (error.message.includes("NetworkError")) { + return "Es konnte keine Verbindung zum Backend hergestellt werden. Bitte überprüfen Sie die Netzwerkkonfiguration und ob das Backend erreichbar ist."; + } + break; + + case "BackendError": + return generateSecondaryErrorMessage_BackendError(error); } - // Display the error message - alerts.displayError(mainMessage, secondaryMessage); + return "Es ist ein unbekannter Fehler aufgetreten. \nFehlertyp: " + error.name + " \nFehlerdetails: " + error.message; +} + +/** + * Generates a secondary, more detailed error message for BackendErrors. + * @param {BackendError} error The BackendError for which a message should be generated. + * @returns {string} The generated secondary error message. + */ +function generateSecondaryErrorMessage_BackendError(error) { + + switch (error.httpStatusCode) { + case 404: + return "Das Backend unterstützt die benötigte API nicht. Möglicherweise wird ein Update des Frontends oder Backends benötigt." + + case 400:{ + let errorMsg = ""; + for (let i = 0; i < error.errorArray.length; i++) { + let err = error.errorArray[i]; + + switch (err.msg) { + case "Invalid value": + errorMsg += `Der Wert für das Feld "${err.path}" ist ungültig.`; + break; + case "plant does not exist": + errorMsg += `Die Pflanze mit der ID "${err.value}" existiert nicht. Möglicherweise wurde sie gelöscht.`; + break; + case "No picture was uploaded": + errorMsg += `Es wurde kein Bild zum Hochladen ausgewählt. Bitte wählen Sie eine Bilddatei aus.`; + break; + case "The uploaded file had the wrong mimetype": + errorMsg += `Die hochgeladene Datei hat einen ungültigen Dateityp. Bitte laden Sie eine Bilddatei hoch.`; + break; + case "The uploaded file had the wrong extension": + errorMsg += `Die hochgeladene Datei hat eine ungültige Dateiendung. Bitte laden Sie eine Bilddatei hoch.`; + break; + default: + errorMsg += `Es ist ein unbekannter Fehler aufgetreten: ${err.msg}`; + } + + if( i < error.errorArray.length - 1 ){ + errorMsg += err.errorMsg + "\n"; + } + } + return errorMsg } + + case 500: + return `Im Backend ist ein interner Fehler aufgetreten. Fehlerdetails: ${error.errorArray[0].errorMsg}`; + } + + return `Es ist ein unbekannter Fehler im Backend aufgetreten. HTTP-Statuscode: ${error.httpStatusCode}`; } \ No newline at end of file From 86c990b9a733b42d9bcb489a626672207d126005 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:53:57 +0100 Subject: [PATCH 31/38] #41 fixed a wrong case in the error handler for the frontend --- frontend/public/mjs/error_handler.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/public/mjs/error_handler.mjs b/frontend/public/mjs/error_handler.mjs index 05f843a..c24a1d8 100644 --- a/frontend/public/mjs/error_handler.mjs +++ b/frontend/public/mjs/error_handler.mjs @@ -99,7 +99,7 @@ function generateSecondaryErrorMessage_BackendError(error) { case "Invalid value": errorMsg += `Der Wert für das Feld "${err.path}" ist ungültig.`; break; - case "plant does not exist": + case "Plant with the given ID does not exist": errorMsg += `Die Pflanze mit der ID "${err.value}" existiert nicht. Möglicherweise wurde sie gelöscht.`; break; case "No picture was uploaded": From efc603d08126e3dcc4733be898cd7921865935c3 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:54:40 +0100 Subject: [PATCH 32/38] Fixed a incorrect log message in the backend --- backend/services/activities.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/services/activities.js b/backend/services/activities.js index 3fd142c..ff6c154 100644 --- a/backend/services/activities.js +++ b/backend/services/activities.js @@ -29,7 +29,7 @@ serviceRouter.post('/activities', body("date").optional().isISO8601(), function(req, resp) { - console.log('Activities plants: Client requested creation of new activity'); + console.log('Service activities: Client requested creation of new activity'); const result = validationResult(req); if (!result.isEmpty()) { console.warn('Service activities: Creation not possible, validation errors'); @@ -97,7 +97,7 @@ serviceRouter.get('/activities/all/:plant_id', try { let result = activitiesDaoInstance.loadByPlantId(data.plant_id); - console.log('Service activities: Records loaded, result= ', result); + console.log('Service activities: Records loaded for plant_id = ' + data.plant_id); let activities = []; From 59621bcec4ace10e62a8851b049ff683e28c8326 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:56:18 +0100 Subject: [PATCH 33/38] Added two new environment variables to disable sql and http logging on the backend --- backend/server.js | 12 ++++++++++-- docker-compose-dev.yml | 3 +++ docs/docker-compose-example.yaml | 3 +++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/backend/server.js b/backend/server.js index fed9b65..f6c95f5 100644 --- a/backend/server.js +++ b/backend/server.js @@ -24,7 +24,11 @@ try // connect database console.log('Connect database...'); const Database = require('better-sqlite3'); - const dbOptions = { verbose: console.log }; + let dbOptions = {} + if (process.env.LOG_SQLITE_QUERIES === 'true') { + console.log('Database debugging enabled'); + dbOptions = { verbose: console.log }; + } const dbFile = './db/plantmanagerDB.sqlite'; const dbConnection = new Database(dbFile, dbOptions); @@ -67,7 +71,11 @@ try response.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); next(); }); - app.use(morgan('dev')); + + // log all requests to the console + if (process.env.LOG_HTTP_QUERIES === 'true') { + app.use(morgan('dev')); + } // ====== BINDING ENDPOINTS ====== const TOPLEVELPATH = '/api'; diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 0031040..06a1456 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -14,6 +14,9 @@ services: image: plantmanager-backend:test ports: - 8001:8001 + environment: + - LOG_SQLITE_QUERIES=true + - LOG_HTTP_QUERIES=true volumes: - db_vol:/home/node/app/db - images_vol:/home/node/app/public/images/plants diff --git a/docs/docker-compose-example.yaml b/docs/docker-compose-example.yaml index a505269..1f6a9f8 100644 --- a/docs/docker-compose-example.yaml +++ b/docs/docker-compose-example.yaml @@ -11,6 +11,9 @@ services: image: ghcr.io/quadcoredevelopment/plantmanager_backend:latest ports: - 8001:8001 + environment: + - LOG_SQLITE_QUERIES=false # set to "true" (lowercase) to enable query logging + - LOG_HTTP_QUERIES=false # set to "true" (lowercase) to enable HTTP request logging volumes: - db_vol:/home/node/app/db - images_vol:/home/node/app/public/images/plants From 6cd686c31e9a0bec6089c7d6848ae20b689d64e0 Mon Sep 17 00:00:00 2001 From: CoderTobi <77673526+CoderTobi@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:09:40 +0100 Subject: [PATCH 34/38] updated template to also mostly work on the dev dir --- frontend/templates/commonHead.html | 2 +- frontend/templates/nav.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/templates/commonHead.html b/frontend/templates/commonHead.html index 0171232..38839f1 100644 --- a/frontend/templates/commonHead.html +++ b/frontend/templates/commonHead.html @@ -10,4 +10,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/templates/nav.html b/frontend/templates/nav.html index a5efbae..c87975e 100644 --- a/frontend/templates/nav.html +++ b/frontend/templates/nav.html @@ -2,7 +2,7 @@

- Logo + Logo