Adding a new field to schema, form and display

This walk through shows how to add a new field to a data entity. In the example we will add a simple text field to an organisation.

The goal here is to show where edits should go.

Instructions

When we need a new field we have to do work in multiple places

At a minimum

  1. The Schema file - so that it can be stored in the database
  2. The Detail file - so that the value can be displayed
  3. The DetailForm file - so that users can enter new values
  4. Related test files and text fixtures.

In addition if the entity has restricted access then

  1. CASL Ability file
  2. Entity Constants


The example we will use is adding a new string field for domain to an organisation record.

Here we add a new string field to the organisation that will hold the domain for the organisation e.g datacom.co.nz. This can then be used to validate email addresses from that domain. We could use the existing web address but that might point to a subdomain or hosting service. We want something we can do a DNS look up on perhaps for Single Sign On integration.


Server Side - database and API

Entity Schema File - organisations.js

Each entity in the system (organisation, person, activity, opportunity) has a matching javascript file in /server/api/{entity}/{entity}.js

organisation.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema

const organisationSchema = new Schema({
  name: { type: 'String', required: true },
  slug: { type: 'String', required: true },


All we have to do here is add a new value e.g domain: String. 

Either just define the field type : String,

or define other properties in an object such as required, default, enum etc 

  category: {
    type: [String],
    required: true,
    default: ['vp'],
    enum: ['admin', 'vp', 'op', 'ap', 'other']
  },
  dateAdded: { type: 'Date', default: Date.now, required: true }

There are a range of type validation options such as min/max, and automatic modifications such as trim, lowercase.

See the full list of types here: https://mongoosejs.com/docs/schematypes.html

Entity Constants File - e.g person.constants.js.

When an entity API has server side security (they all should have) a middleware library (CASL) protects the database by taking each API entry point (LIST,READ, UPDATE, CREATE, DELETE) and allowing or denying access to the endpoint based on criteria such as whether the person is signed in or not, whether the person has admin status or whether the person 'owns' the record e.g. their own profile.  In addition CASL can control which fields are returned in requests based on that status. This is enabled by defining string constants for each field that can be returned. 

const { Action } = require('../../services/abilities/ability.constants')

const SchemaName = 'Person'

const PersonRoutes = {
  [Action.LIST]: '/api/people',
  [Action.READ]: '/api/people/:id',
  [Action.UPDATE]: '/api/people/:id',
  [Action.CREATE]: '/api/people'
}
const PersonFields = {
  ID: '_id',
  EMAIL: 'email',
  NICKNAME: 'nickname',
  NAME: 'name',

Hence if you add a new field you must update the matching constants file if it exists.

Entity Ability file e.g person.ability.js

If the entity has an ability file you will need to update it with the new field essentially adding the field to each rule depending on who is allowed to see the field. For example a person's phone number is restricted and not shown to the public.

Again this is only required if the ability file exists for the entity. But if it does and you forget to update it your new field probably won't show up in API responses.

Entity Controller file: opportunity.controller.js

Changes here may not be needed but you should review the file to see if anything special is required.  For example in Opportunity.controller. we handle search parameters and determine which fields get searched.

     const searchParams = {
        $or: [
          { 'name': searchExpression },
          { 'subtitle': searchExpression },
          { 'description': searchExpression }
        ]
      }

So if your new field should be included in the search you would need to add it here.

No change required for our example.

Entity Test file: opportunity.spec.js, and fixtures. opportunity.fixture.js

Finally on the server side you should review and update the entity test and fixture files. These are in entity/__tests__

The test fixture is simply a JSON file containing a set of examples of the data type. Update this so that some of the records have values for the new field and if its optional so that some don't so that both cases are represented.

For testing - If you simply added a new field with no validation or required flags then you probably don't need to make any changes. The test will load the new fixture and that will be it. However if the field is required - then you should show validation fails if it is omitted, and if the field is required but has a default you can create a record with no field and verify that it gets set.

If there are changes to the CASL abilities or the controller you will need to add tests that exercise the new lines of code.

Commit

If this test passes then commit and PR the server side changes before moving onto the Client side. This will allow client side work to be delegated or shared across several people.

Client Side

On the client side we tend to use abbreviated names for the entities: organisation → org, opportunity → op, activity → act.  Although some like person and interest we keep the same. 

All the components for the entity are in the folder /components/{entity}. e.g. /components/org.

Entity Detail e.g OrgDetail.js

The main display page for each entity is called the Detail page (OrgDetail.js). The role of this component is to display to the reader all the relevant bits of information in the record, either directly or using child components. This component should be dumb - taking entity as a prop and not having internal behaviour except for visual changes such as show/hide. Some information may be dependent on the person's status and some may have been removed from the record by the server side security so do not assume that all fields have values.


Update PropTypes

The PropTypes function defines the expected shape of the props used by the component. As this component takes an instance of the entity e.g org it will define the shape of an organisation. This will look a lot like the entity schema definition but it is not the same and this function should be used to define only what is expected and required to make the display page work correctly. If the entity has fields that point at other objects e.g. personID or ActivityID etc then the API may be setup to fill in (product) these fields so that the ID is replaced with a copy of the referenced record or at least some of its fields. In this case the PropTypes should show that these fields are expected to be present.

PropTypes will generate warnings in the console.log if the values passed in when the component is rendered do not match up

OrgDetail.propTypes = {
  org: PropTypes.shape({
    name: PropTypes.string.isRequired,

Update Render

Now add the field {org.domain} to the rendered output.  

If the field needs a label then it would look like

<FormattedMessage
	id='orgdetail.social.label'
    default='Social:'
    description='Label for social media links on organisation details'
/>{org.domain} 

Usually however the label and fields are wrapped in some type of layout and styling elements.

<TitleContainer>
  <H3Bold>{org.name}</H3Bold>
</TitleContainer>

If the field is optional and likely to be absent then the render should not print lines unnecessarily so we wrap the block in a conditional test.

{org.twitter && (
  <SocialButton
 	type='link'
    href={`https://www.twitter.com/${org.twitter}`}
    icon='twitter'
    target='_blank'
    rel='noopener noreferrer'
/>
)}


Here we use the logical AND (&&). This works thanks to a mix of javascript rules.

  1. AND is true only if both sides of the && are true. e.g. 1 && 1 === True.
  2. All expressions can evaluate to Truthy or Falsy e.g 1 is true, 0 is false,  'hello' is true, '' is false, null is false etc.
  3. If the first item of the AND evaluates to false the second item is not evaluated - so we don't have to worry about using an undefined variable
  4. the result of any expression is the last value evaluated in that expression.

So if org.twitter is null or empty the expression evaluates to null or empty and when this is rendered nothing is output. If org.twitter has a value then the SocialButton is rendered with org.twitter in the href.

You'll see this a lot and its preferred to the alternative  org.twitter ? {org.twitter} : null



Finally add any styling required.

If the field itself is complex - e.g an Enum or Array then consider creating a small child component to render the field on its own.  For example we use OrgCategory component to render the org.category as an Icon rather than a text string.  Note also that Enum String values should not be displayed directly but passed through a child component that can render the value of each enum item as a translated string. e.g PersonRole.js


Updating the Entity Form

see Part 2. Adding a new field to a Form