5. Controller
This file was copied from the packages/webapp/src/stories/docs
folder. Its accuracy as of August 8, 2024 has not been verified
| Resources | | --------- | | Express routing | | Objection query builder | | Knex query builder | | Objection withGraphFetched | | Objection transactions | | Objection hooks |
1.Get endpointsGet endpoints
Get endpoints return
objects from a single table
{...crop}
nested objects
{...location, figure: {...figure, area }, field}
objects from joined tables
{...userFarm, ...user, ...farm}
(mostly legacy code)
Please refer to LiteFarm REST API styleguide for status status code convention
In short, post
success should return status code 201
.
get
delete
patch
put
success should return status code 200
.
post
and put
should return inserted/edited object.
If no entity is found, return status code 404
.
Validation errors should return status code 400
.
getNewEntityByEntityId controller
const NewEntityModel = require('../models/newEntityModel');
const newEntityController = {
getNewEntityByNewEntityId() {
return async (req, res, next) => {
const { new_entity_id } = req.params;
try{
const result = await NewEntityModel.query().whereNotDeleted().findById(new_entity_id);
return result ? res.status(200).send(result): res.status(404).send('New entity not found');
}catch(error){
return res.status(400).json({ error });
}
}
},
}
module.exports = newEntityController;
Copy
If newEntityModel supports soft delete, or NewEntityModel extends baseModel/softDelete, method whereNotDeleted()
is required to filter out deleted entities
baseModel.created_atbaseModel.updated_atbaseModel.created_by_user_idbaseModel.updated_by_user_idbaseModel.deleted
are hidden by default from baseModel queries. If you need to access hidden fields, pass in context showHidden
as such await NewEntityModel.query().context({showHidden: true}).whereNotDeleted().findById(new_entity_id)
getNewEntityByFarmId controller
const NewEntityModel = require('../models/newEntityModel');
const newEntityController = {
getNewEntitiesByFarmId() {
return async (req, res, next) => {
const { farm_id } = req.params;
try{
const result = await NewEntityModel.query().whereNotDeleted().where({farm_id});
return result?.length ? res.status(200).send(result): res.status(404).send('New entities not found');
}catch(error){
return res.status(400).json({ error });
}
}
},
}
module.exports = newEntityController;
Copy
Nested object example: getLocationsById
const LocationModel = require('../models/locationModel');
const LocationController = {
getLocationsById() {
return async (req, res, next) => {
const { location_id } = req.params;
const location = await LocationModel.query()
.findById(location_id).andWhere({ deleted: false })
.withGraphJoined(`[
figure.[area, line, point],
gate, water_valve, field, garden, buffer_zone, watercourse, fence,
ceremonial_area, residence, surface_water, natural_area,
greenhouse, barn, farm_site_boundary
]`)
return location?res.status(200).send(location):res.sendStatus(404);
}
},
}
module.exports = LocationController;
Copy
If location_id refers to a barn, response would look like
Copy
withGraphJoined checks LocationModel.relationMappings()
to find how tables should be joined
[ figure.[area, line, point], gate, water_valve, field, garden, buffer_zone, watercourse, fence, ceremonial_area, residence, surface_water, natural_area, greenhouse, barn, farm_site_boundary ]
means that figure
table joins area/line/point
tables, location
table joins figure
, gate
, water_valve
, field
, garden
, buffer_zone
, watercourse
, fence
, ceremonial_area
, residence
, surface_water
, natural_area
, greenhouse
, barn
, farm_site_boundary
tables
The relationship betweenarea
and figure
, barn
and location
need to be defined in relationMappings
for withGraphJoined
to get a barn
by location_id
Copy
Copy
Joined table example (should avoid)
Sending joined table directly as response should be avoided since objection model hooks (such as beforeFind
) do not run when tables are joined
For example, user has a hashed password column which should be hidden from user. We add user.password
to user.hidden
prop and use user.beforeFind
to remove password field on every userModel.query()
. If we userFarm.query().join('user').join('farm')
. userFarm.beforeFind
would run but user.beforeFind
and farm.beforeFind
would not. As a result, user.password
would be returned by the getUserFarm endpoints
Copy
2.Post endpointsGet endpoints
Add a single entity
Copy
NewEntityModel.query().context(req.auth)
is required when NewEntityModel
extends baseModel so that updated_by_user_id
created_by_user_id
can be populated
Post request should return inserted object.
Add a single entity that uses multiple tables
Copy
Always use transactions when an endpoint performs more than 1 insert/update/delete
Steps for a backend - frontend story from a data flow perspective
Create knex migration to create/modify tables in database
Create objection models
Endpoints and empty controllers
Backend jest unit testing
Implement controllers
Create authorization/validation middlewares
Run tests and make sure everything passes
Create pure frontend components in storybook
res.data normalizer, redux slice, redux selectors to hold and access the data
Redux saga for get/post/put/patch request
Container component to connect saga actions/selector with pure components
Connect Route component with container components with react router
Manual testing