Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

Version 1 Current »

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

{
    "location_id": "b03b3932-aabb-11eb-92dc-0242ac120002",
    "farm_id": "d5edd27c-61bc-11eb-b50d-22000ab3e50b",
    "figure": {
      "figure_id": "b03d3a3e-aabb-11eb-92dc-0242ac120002",
      "type": "barn",
      "location_id": "b03b3932-aabb-11eb-92dc-0242ac120002",
      "area": {
        "figure_id": "b03d3a3e-aabb-11eb-92dc-0242ac120002"
      },
      "line": null,
      "point": null
    },
    "gate": null,
    "water_valve": null,
    "field": null,
    "garden": null,
    "buffer_zone": null,
    "watercourse": null,
    "fence": null,
    "ceremonial_area": null,
    "residence": null,
    "surface_water": null,
    "natural_area": null,
    "greenhouse": null,
    "barn": {
      "location_id": "b03b3932-aabb-11eb-92dc-0242ac120002"
    },
    "farm_site_boundary": null
  }

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

const Model = require('objection').Model;
const baseModel = require('./baseModel');

class Location extends baseModel {
  static get relationMappings() {
    return {
      figure: {
        modelClass: require('./figureModel'),
        relation: Model.HasOneRelation,
        join: {
          from: 'location.location_id',
          to: 'figure.location_id',
        },
      },
      barn: {
        modelClass: require('./barnModel'),
        relation: Model.HasOneRelation,
        join: {
          from: 'location.location_id',
          to: 'barn.location_id',
        },
      },
    };
  }
}
module.exports = Location;

Copy

const Model = require('objection').Model;

class Figure extends Model {
  static get relationMappings() {
    return {
      area: {
        relation: Model.HasOneRelation,
        modelClass: require('./areaModel'),
        join: {
          from: 'figure.figure_id',
          to: 'area.figure_id',
        },
      },
    };
  }
}

module.exports = Figure;

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

//should avoid
const userFarmModel = require('../models/userFarmModel');
const userFarmController = {

  getFarmInfo() {
    return async (req, res) => {
      try {
        const user_id = req.params.user_id;
        const farm_id = req.params.farm_id;
        const rows = await userFarmModel.query().context({ user_id: req.auth.user_id }).select('*')
        .where('userFarm.user_id', user_id).andWhere('userFarm.farm_id', farm_id)
          .leftJoin('role', 'userFarm.role_id', 'role.role_id')
          .leftJoin('users', 'userFarm.user_id', 'users.user_id')
          .leftJoin('farm', 'userFarm.farm_id', 'farm.farm_id');
        return res.status(200).send(rows[0]);
      } catch (error) {
        return res.status(400).send(error);
      }
    };
  },
};

Copy

2.Post endpointsGet endpoints

Add a single entity

const NewEntityModel = require('../models/newEntityModel');

const newEntityController = {
  addNewEntity() {
    return async (req, res, next) => {
      try{
        const result = await NewEntityModel.query().context(req.auth).insert(req.body);
        return res.status(201).send(result);
      }catch(error){
        return res.status(400).json({ error });
      }
    }
  },
}

module.exports = newEntityController;

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

const LocationModel = require('../models/locationModel');

const LocationController = {
  createLocation(asset) {
    const nonModifiable = getNonModifiable(asset);
    return async (req, res, next) => {
      try {
        const result = await LocationModel.transaction(async trx => {
          return await LocationModel.query(trx).context(req.auth).upsertGraph(
            req.body, { noUpdate: true, noDelete: true, noInsert: nonModifiable });
        });
        return res.status(200).send(result);
      } catch (error) {
        console.log(error);
        return res.status(400).send({ error });
      }
    }
  },
}

module.exports = LocationController;

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

  • No labels