The shared package
This package contains code which is shared between both the api
and webapp
packages. This was originally implemented to make the validation of data more consistent between the frontend and the backend.
The shared code
The code in the shared
package is normally more general purpose than that in the api
or webapp
packages. It doesn’t handle any React or Express.js specific logic, rather it handles smaller shared pieces of logic which can fit into either React or Express. A good example of this is the first code that ever went into the shared
package: the validation for the sensor upload csv file. This code can be found in packages/shared/validation/sensorCSV.js
. A portion of this code can be seen below:
const sensorCsvValidators = [
{
key: 'name',
parse: (val) => val.trim(),
validate: (val) => 1 <= val.length && val.length <= 100,
required: true,
errorTranslationKey: sensorErrors.SENSOR_NAME,
useParsedValForError: true
},
{
key: 'external_id',
parse: (val) => val.trim(),
validate: (val) => val.length <= 40,
required: false,
errorTranslationKey: sensorErrors.EXTERNAL_ID,
useParsedValForError: true
},
{
key: 'latitude',
parse: (val) => parseFloat(val),
validate: (val) => -90 <= val && val <= 90,
required: true,
errorTranslationKey: sensorErrors.SENSOR_LATITUDE,
useParsedValForError: true
},
{
key: 'longitude',
parse: (val) => parseFloat(val),
validate: (val) => -180 <= val && val <= 180,
required: true,
errorTranslationKey: sensorErrors.SENSOR_LONGITUDE,
useParsedValForError: true
},
{
key: 'reading_types',
parse: (val, lang) => {
const rawReadingTypes = val.replaceAll(' ', '').split(',');
return getReadableValuesForReadingTypes(lang, rawReadingTypes);
},
validate: (val) => {
if (!val.length || (val.length === 1 && val[0] === '')) {
return false;
}
const allowedReadingTypes = ['soil_water_potential', 'soil_water_content', 'temperature'];
return val.every((readingType) => allowedReadingTypes.includes(readingType));
},
required: true,
errorTranslationKey: sensorErrors.SENSOR_READING_TYPES,
useParsedValForError: false
},
{
key: 'depth',
parse: (val) => parseFloat(val) / 100, // val is in cm, so divide by 100 to get val in m,
validate: (val) => 0 <= val && val <= 10,
required: false,
errorTranslationKey: sensorErrors.SENSOR_DEPTH,
useParsedValForError: true
},
{
key: 'brand',
parse: (val) => val.trim(),
validate: (val) => val.length <= 100,
required: false,
errorTranslationKey: sensorErrors.SENSOR_BRAND,
useParsedValForError: true
},
{
key: 'model',
parse: (val) => val.trim(),
validate: (val) => val.length <= 100,
required: false,
errorTranslationKey: sensorErrors.SENSOR_MODEL,
useParsedValForError: true
},
];
// Returns the readable values to save in the database based on the given translated reading types
const getReadableValuesForReadingTypes = (lang, readingTypes) => {
const translationEntries = Object.entries(readingTypeTranslations[lang]);
return readingTypes.map((rt) => {
const entryWithReadableValue = translationEntries.find((e) => e[1] === rt);
return entryWithReadableValue ? entryWithReadableValue[0] : null;
});
};
const generateSensorKey = (sensor) => {
return `${sensor.brand ?? ''}:${sensor.external_id ?? ''}`;
};
export const parseSensorCsv = (csvString, lang) => {
return parseCsv(
csvString,
lang,
sensorCsvValidators,
sensorCsvHeaderTranslations,
sensorErrors.MISSING_COLUMNS,
true,
generateSensorKey,
100
);
};
This code describes how the data in the csv file should be both parsed, and validated. The exported function parseSensorCsv
returns an object with two keys: data
, and errors
. This allows the webapp
to display any errors found in the file to the users, and allows the api
to parse the file to get the data. On top of this, as we always need to validate data on the backend anyways, this provides any errors found in the data to the api
, allowing us to properly handle those there as well. The key thing to note with this code is that while it does return data and errors, it doesn’t make any assumptions about how those will be used. This is crucial, as the data and errors may need to be handled differently between the frontend and the backend.
Using code from the shared package
To use code from the shared
package, all you need to do is import it into whichever file you want to use it in. All of the hot reload functionality for the api
and webapp
packages will continue to work, and all of the Docker images and deployments will work as well.