Enrich your models

Declaring a new model

Whenever you have a new table/collection in your database, you will have to create file to declare it. Here is a template example for a companies table:

SQL
Mongodb
/forest/companies.js
module.exports = (sequelize, DataTypes) => {
const { Sequelize } = sequelize;
const Company = sequelize.define('companies', {
name: {
type: DataTypes.STRING,
},
createdAt: {
type: DataTypes.DATE,
},
...
}, {
tableName: 'companies',
underscored: true,
schema: process.env.DATABASE_SCHEMA,
});
Company.associate = (models) => {
};
return Company;
};

Fields within that model should match your table's fields as shown in next section.

New relationships may be added there:

Company.associate = (models) => {
};

You can learn more about relationships on this dedicated page.

/forest/companies.js
const mongoose = require('mongoose');
const schema = mongoose.Schema({
'name': String,
'createdAt': Date,
...
}, {
timestamps: false,
});
module.exports = mongoose.model('companies', schema, 'companies');

Fields within that model should match your collection's fields as shown in next section.

New relationships are to be added as properties:

'orders': [{ type: mongoose.Schema.Types.ObjectId, ref: 'orders' }],
'customer_id': { type: mongoose.Schema.Types.ObjectId, ref: 'customers' },

You can learn more about relationships on this dedicated page.

Declaring a new field in a model

Any new field must be added manually within the corresponding model of your /forest folder.

SQL
Mongodb

Fields are declared as follows:

createdAt: {
type: DataTypes.DATE,
},

An exhaustive list of DataTypes can be found in Sequelize documentation.

You can see how that snippet fits into your code in the model example above.

Fields are declared as follows:

'createdAt': Date,

An exhaustive list of SchemaTypes can be found in Mongoose documentation.

You can see how that snippet fits into your code in the model example above.

Adding validation to your models

Validation allows you to keep control over your data's quality and integrity.

If your existing app already has validation conditions, you may - or may not - want to reproduce the same validation conditions in your admin backend's models.

If so, you'll have to do it manually, using the below examples.

Depending on your database type, your models will have been generated in Sequelize (for SQL databases) or Mongoose (for Mongo databases).

SQL
Mongodb

In Sequelize, you add validation using the validate property:

/models/customers.js
module.exports = (sequelize, DataTypes) => {
const Customer = sequelize.define('customers', {
...
'email': {
type: DataTypes.STRING,
validate: {
isEmail: true,
len: [10,25]
}
},
...
},
...
return Customer;
};

The 2 validators above will have the following effect on your email field:

Invalid email
Email too short (not within 10-25 range)

For an exhaustive list of available validators, check out the Sequelize documentation.

In Mongoose, you add validators alongside the type property:

/models/customers.js
...
const schema = mongoose.Schema({
'createdAt': Date,
'email': {
'type': String,
'minlength': 10,
'maxlength': 25
},
'firstname': String,
...
}

This is the effect on your field:

Email is too short (not within 10-25 range)

Mongoose has no build-in validators to check whether a string is an email. Should you want to validate that a content is an email, you have several solutions:

/models/customers.js
...
const schema = mongoose.Schema({
'createdAt': Date,
'email': {
'type': String,
'match': [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 'Invalid email']
},
'firstname': String,
...
}

A better yet solution would be to rely on an external library called validator.js which provides many build-in validators:

/models/customers.js
import { isEmail } from 'validator';
...
const schema = mongoose.Schema({
'createdAt': Date,
'email': {
'type': String,
'validate': [isEmail, 'Invalid email']
},
'firstname': String,
...
}

You then that any invalid email is refused:

For further details on validators in Mongoose, check out the Mongoose documentation.

Adding a default value to your models

You can choose to add a default value for some fields in your models. As a result, the corresponding fields will be prefilled with their default value in the creation form:

SQL
Mongodb
/models/customers.js
module.exports = (sequelize, DataTypes) => {
const Customer = sequelize.define('customers', {
...
'firstname': {
'type': DataTypes.STRING,
'defaultValue': 'Marc'
},
...
},
...
return Customer;
};
/models/customers.js
...
const schema = mongoose.Schema({
'createdAt': Date,
'email': {
'type': String,
'default': 'Marc'
},
'firstname': String,
...
}

Adding a hook

Hooks are a powerful mechanism which allow you to automatically trigger an event at specific moments in your records lifecycle.

In our case, let's pretend we want to update a update_count field every time a record is updated:

SQL
Mongodb

To add a beforeSave hook in Sequelize, use the following syntax:

/models/orders.js
module.exports = (sequelize, DataTypes) => {
var Order = sequelize.define('orders', {
...
'update_count': {
'type': DataTypes.INTEGER,
'defaultValue': 0
},
...
},
...
Order.beforeSave((order, options) => {
order.update_count += 1;
}
);
return Order;
};

Every time the order is updated, the updateCount field will be incremented by 1:

The exhaustive list of available hooks in Sequelize are available here.

To add a hook in Mongoose on save event, you may use the following snippet:

/models/customers.js
schema.pre('save', async function() {
var newCount = this.update_count+1;
const incrementCount = () => {
this.set('update_count', newCount);
};
await incrementCount();
});

As mentioned in their documentation

Pre and post save() hooks are not executed on update(), findOneAndUpdate(), etc.

This would only work if you specifically call save in your update method.

Adding nested documents (level 2 and 3) in Mongoose

Lumber only detects Level 1 document fields in your collections. However Forest Admin is able to display Level 2 and Level 3 sub-documents if properly declared in your model.

The following model...

const mongoose = require('mongoose');
const schema = mongoose.Schema({
// Level 1
'age': Number,
'id': Number,
'name': String,
// Level 2
'address':{
'addressDetail': String,
'area': String,
'city': String,
'pincode': Number,
},
// Level 3
'contactDetails':{
'phone':{
'homePhone': String,
'mobilePhone': String,
},
'email': String,
},
// Related data
'booksRead':[{
'name': String,
'authorName': String,
'publishedBy': String,
}],
}, {
timestamps: false,
});
module.exports = mongoose.model('testCollection', schema, 'testCollection');

...will result in the following interface:

Removing a model

By default all tables/collections in your database are analyzed by Lumber to generate your models. If you want to exclude some of them to prevent them from appearing in your Forest, check out this how-to.