{{title}}

Getting Started

Introduction

Forest instantly provides all common admin tasks such as CRUD operations, simple chart rendering, user group management, and WYSIWYG interface editor. That’s what makes Forest a quick and easy solution to get your admin interface started.

This guide assists developers in having the admin completely tailored to cater to their specific needs. We’ve developed Forest so that your admin is 100% customizable. It means that you have full control over your data, your back-end business logic and the UI.

Forest provides a very simple API-based framework to handle all parts of your admin back-end configuration. You should be good to go in no time, with virtually no learning curve.

How it works

Before you start writing a single line of code, it’s a good idea to get an overview of how Forest works.

1

The Initialization phase

Install the Forest Liana on your application

Initialization

There are three steps on the initialization phase:

2

Forest's architecture

Where the magic happens

archi

The magic of Forest lies in its architecture. Forest is divided into two main components:

  • The Forest Liana that allows Forest to communicate seamlessly with your application’s database.
  • The Forest UI (web application), accessible from any browser, that handles communication between the user and the database through your admin API.

Data Privacy

The main advantage of Forest’s architecture is that absolutely no data transits over our servers. The user accesses application data directly from the client and Forest is just deployed as a service to display and interact with the data. Read more about hosting.

With Forest, your data are transferred directly from your application to your browser while remaining invisible to our servers. See how it works.

Privacy

Security

We use a two-step authentication to connect you to both Forest’s server and your Admin API.

1

Log into your Forest account

To retrieve your UI configuration

When logging into your account, your credentials are sent to the Forest’s server which returns the UI token to authenticate your session.

2

Log into your admin API

To manage your data

Your password is sent to your Admin API which returns the data token signed by the FOREST_AUTH_SECRET you chose. Each of your requests to your Admin API are authenticated with the data token.

security

Your admin uses the UI token to make request about the UI configuration. And the data token is used to make queries on your admin API to manage your data. All our tokens are generated using the JWT standard.

Installation

If you haven’t yet, you should have Forest installed on your application. The relevant information is below. You should be set in a few minutes, no more. If you encounter any issue, feel free to drop us a line at support@forestadmin.com or using the live chat available on our homepage.

Tip: You’re using a framework based off Rails? Express? Head over to our Github account. If support is just a matter of a couple tweaks, it’ll be listed there along with specific instructions.

# Add the liana to your application's Gemfile
gem 'forest_liana'

# Bundle it
bundle install

# Install it with the provided environment secret
rails g forest_liana:install YOUR-SUPER-SECRET-SECRET-KEY
// Install the liana
npm install forest-express-mongoose --save

// Add the following code to your app.js file:
app.use(require('forest-express-mongoose').init({
  modelsDir: __dirname + '/models',  // Your models directory.
  envSecret: process.env.FOREST_ENV_SECRET,
  authSecret: process.env.FOREST_AUTH_SECRET,
  mongoose: require('mongoose') // The database connection.
}));

// Setup Forest environment variables and do not version them
FOREST_ENV_SECRET=FOREST-ENV-SECRET
FOREST_AUTH_SECRET=FOREST-AUTH-SECRET // Choose a secure auth secret
// Install the liana
npm install forest-express-sequelize --save

// Add the following code to your app.js file:
app.use(require('forest-express-sequelize').init({
  modelsDir: __dirname + '/models',  // Your models directory.
  envSecret: process.env.FOREST_ENV_SECRET,
  authSecret: process.env.FOREST_AUTH_SECRET,
  sequelize: require('./models').sequelize // The database connection.
}));

// Setup Forest environment variables and do not version them
FOREST_ENV_SECRET=FOREST-ENV-SECRET
FOREST_AUTH_SECRET=FOREST-AUTH-SECRET // Choose a secure auth secret


Get Started

Glossary

  • Admin API is generated by the Forest Liana to manage all your application data and business operations.
  • Collection is a group of records gathered from a unique table of your database.
  • Data token is used to authenticate your requests on your admin API.
  • Forest liana is a locally-installed add-on that allows us to communicate seamlessly with your application’s database.
  • Forest UI is the web application of Forest, accessible from any browser at https://app.forestadmin.com.
  • Forest UI Schema is a schema of your data model generated by the Forest Liana.
  • FOREST_AUTH_SECRET - chosen by yourself - is an environment variable used to sign the data token.
  • FOREST_ENV_SECRET is an environment variable used to identify your project environment in Forest.
  • Segment is a subgroup of a collection gathering filtered records.
  • Smart Action is a specific action related to your business. Read more about action.
  • Smart chart is a complex chart computed based on your business logic. Read more about smart chart.
  • Smart collection is a group of records gathered from different sources implemented following on your business logic. Read more about smart collection.
  • Smart field is a new field of an existing collection implemented based on your business logic. Read more about smart field.

Analytics

As an admin user, KPIs are one of the most important things to follow day by day. Your customers’ growth, Monthly Recurring Revenue (MRR), Paid VS Free accounts are some common examples.

Forest can render three types of charts:

  • Single value (Number of users, MRR, …)
  • Repartition (Number of users by countries, Paid VS Free, …)
  • Time-based (Number of signups per month, …)

Ensure you’ve enabled the Edit Layout mode to add, edit or delete a chart.

Creating a Chart

Forest provides a straightforward UI to configure step-by-step the charts you want. The only information the UI needs to handle such charts is:

  • 1 collection
  • 1 aggregate function (count, sum, …)
  • 1 group by field
  • 1 time frame (day, week, month, year) option.

Analytics 1`

Creating a Smart Chart

Sometimes, charts data are complicated and closely tied to your business. Forest allows you to code how the chart is computed. Choose “URL” as the data source when configuring your chart. Forest will make the HTTP call to this address when retrieving the chart values for the rendering.

Analytics 2`

Value chart

The value format passed to the serializer for a Value chart must be:

<value>

1

Declare the route

config/routes.rb (add the route before the Forest engine)

namespace :forest do
  post '/stats/mrr' => 'stats#mrr'
end

# ...
# mount ForestLiana::Engine => '/forest'

2

Configure CORS

config/application.rb

We use the gem rack-cors for the CORS configuration.

class Application < Rails::Application
  # ...

  config.middleware.insert_before 0, 'Rack::Cors' do
    allow do
      origins 'app.forestadmin.com'
      resource '*', headers: :any, methods: :any
    end
  end
end

3

Create the controller

app/controllers/stats_controller.rb

# ForestLiana::ApplicationController takes care of the authentication for you.
class StatsController < ForestLiana::ApplicationController
  def mrr
    stat = ForestLiana::Model::Stat.new({
      value: 500000 # Your business logic here.
    })

    # The serializer_model function serializes the ForestLiana::Model::Stat
    # model to a valid JSONAPI payload.
    render json: serialize_model(stat)
  end
end

1

Handle the route

Declare the route in the Express Router

var liana = require('forest-express-mongoose');

function mrr(req, res) {
  var result = 500000; // Your business logic here.

  // The liana.StatSerializer function serializes the result to a valid
  // JSONAPI payload.
  var json = new liana.StatSerializer({ value: result }).perform();
  res.send(json);
}

// liana.ensureAuthenticated middleware takes care of the authentication for you.
router.post('/api/stats/mrr', liana.ensureAuthenticated, mrr);

1

Handle the route

Declare the route in the Express Router

var liana = require('forest-express-sequelize');

function mrr(req, res) {
  var result = 500000; // Your business logic here.

  // The liana.StatSerializer function serializes the result to a valid
  // JSONAPI payload.
  var json = new liana.StatSerializer({ value: result }).perform();
  res.send(json);
}

// liana.ensureAuthenticated middleware takes care of the authentication for you.
router.post('/api/stats/mrr', liana.ensureAuthenticated, mrr);

Repartition chart

The value format passed to the serializer for a Repartition chart must be:

[
  { key: <key>, value: <value> },
  { key: <key>, value: <value> },
  ...
]

1

Declare the route

config/routes.rb (add the route before the Forest engine)

namespace :forest do
  post '/stats/avg_price_per_supplier' => 'stats#avg_price_per_supplier'
end

# ...
# mount ForestLiana::Engine => '/forest'

2

Configure CORS

config/application.rb

We use the gem rack-cors for the CORS configuration.

class Application < Rails::Application
  # ...

  config.middleware.insert_before 0, 'Rack::Cors' do
    allow do
      origins 'app.forestadmin.com'
      resource '*', headers: :any, methods: :any
    end
  end
end

3

Create the controller

app/controllers/stats_controller.rb

# ForestLiana::ApplicationController takes care of the authentication for you.
class StatsController < ForestLiana::ApplicationController
  def avg_price_per_supplier
    # Your business logic here.
    value = Item
      .all
      .group(:supplier)
      .average(:price)
      .map {|supplier, avg| { key: supplier.last_name, value: avg }}

    stat = ForestLiana::Model::Stat.new({
      value: value
    })

    # The serializer_model function serializes the ForestLiana::Model::Stat
    # model to a valid JSONAPI payload.
    render json: serialize_model(stat)
  end
end

1

Handle the route

Declare the route in the Express Router

var liana = require('forest-express-mongoose');

function avgPricePerSupplier(req, res) {
  // Your business logic here.
  Item
    .aggregate()
    .group({
      _id: '$supplier',
      count: { $avg: '$price' }
    })
    .project({
      key: '$_id',
      value: '$count',
      _id: false
    })
    .exec(function (err, value) {
      // The liana.StatSerializer function serializes the result to a valid
      // JSONAPI payload.
      var json = new liana.StatSerializer({ value: value }).perform();
      res.send(json);
    });
}

// liana.ensureAuthenticated middleware takes care of the authentication for you.
router.post('/api/stats/avg_price_per_supplier', liana.ensureAuthenticated,
  avgPricePerSupplier);

1

Handle the route

Declare the route in the Express Router

var liana = require('forest-express-sequelize');

function avgPricePerSupplier(req, res) {
  models.item
    .findAll({
      attributes: [
        'supplier',
        [ db.sequelize.fn('avg', db.sequelize.col('price')), 'value' ]
      ],
      group: ['supplier'],
      order: ['value']
    })
    .then((result) => {
      return result.map((r) => {
        r = r.toJSON();

        var ret = {
          key: r.supplier,
          value: r.value
        };

        return ret;
      });
    })
    .then((result) => {
      var json = new liana.StatSerializer({ value: result }).perform();
      res.send(json);
    });
}

// liana.ensureAuthenticated middleware takes care of the authentication for you.
router.post('/api/stats/avg_price_per_supplier', liana.ensureAuthenticated,
  avgPricePerSupplier);

Time-based chart

The value format passed to the serializer for a Line chart must be:

[
  { label: <date key>, values: { value: <value> } },
  { label: <date key>, values: { value: <value> } },
  ...
]

1

Declare the route

config/routes.rb (add the route before the Forest engine)

namespace :forest do
  post '/stats/avg_price_per_month' => 'stats#avg_price_per_month'
end

# ...
# mount ForestLiana::Engine => '/forest'

2

Configure CORS

config/application.rb

We use the gem rack-cors for the CORS configuration.

class Application < Rails::Application
  # ...

  config.middleware.insert_before 0, 'Rack::Cors' do
    allow do
      origins 'app.forestadmin.com'
      resource '*', headers: :any, methods: :any
    end
  end
end

3

Create the controller

app/controllers/stats_controller.rb

# ForestLiana::ApplicationController takes care of the authentication for you.
class StatsController < ForestLiana::ApplicationController
  def avg_price_per_month
    # Your business logic here.
    value = Item
      .all
      .group("DATE_TRUNC('month', created_at)")
      .average(:price)
      .map do |date, avg|
        {
          label: date,
          values: { value: avg }
        }
      end
      .sort_by {|x| x[:label]}

    stat = ForestLiana::Model::Stat.new({
      value: value
    })

    # The serializer_model function serializes the ForestLiana::Model::Stat
    # model to a valid JSONAPI payload.
    render json: serialize_model(stat)
  end
end

1

Handle the route

Declare the route in the Express Router

var liana = require('forest-express-mongoose');

function avgPricePerMonth(req, res) {
  // Your business logic here.
  Item
    .aggregate()
    .group({
      _id: {
        month: { $month: '$createdAt' },
        year: { $year: '$createdAt' }
      },
      createdAt: { '$last': '$createdAt' },
      count: { $avg: '$price' }
    })
    .sort({
      createdAt: 1
    })
    .project({
      label: '$createdAt',
      values: { value: '$count' },
      _id: false
    })
    .exec(function (err, value) {
      // The liana.StatSerializer function serializes the result to a valid
      // JSONAPI payload.
      var json = new liana.StatSerializer({ value: value }).perform();
      res.send(json);
    });
}

// liana.ensureAuthenticated middleware takes care of the authentication for you.
router.post('/api/stats/avg_price_per_month', liana.ensureAuthenticated,
  avgPricePerMonth);

1

Handle the route

Declare the route in the Express Router

var liana = require('forest-express-sequelize');

function avgPricePerMonth(req, res) {
  models.item
    .findAll({
      attributes: [
        [ db.sequelize.fn('avg', db.sequelize.col('price')), 'value' ],
        [ db.sequelize.fn('to_char', db.sequelize.fn('date_trunc', 'month',
            db.sequelize.col('createdAt')), 'YYYY-MM-DD 00:00:00'), 'key' ]
      ],
      group: ['key'],
      order: ['key']
    })
    .then((result) => {
      return result.map((r) => {
        r = r.toJSON();

        var ret = {
          label: r.key,
          values: {
            value: r.value
          }
        };

        return ret;
      });
    })
    .then((result) => {
      var json = new liana.StatSerializer({ value: result }).perform();
      res.send(json);
    });

}

// liana.ensureAuthenticated middleware takes care of the authentication for you.
router.post('/api/stats/avg_price_per_month', liana.ensureAuthenticated,
  avgPricePerMonth);

Integrations

Forest is able to leverage data from third party services by reconciliating it with your application’s data, providing it directly to your admin. All your admin actions can be performed at the same place, bringing additional intelligence to your admin and ensuring consistency.

Intercom

Configuring the Intercom integration for Forest allows you to have your user’s session data (location, browser type, …) and support conversations directly alongside the corresponding user from your application.

1

Add the Intercom integration

config/initializers/forest_liana.rb

# ...
ForestLiana.integrations = {
  intercom: {
    app_id: 'YOUR_INTERCOM_APP_ID',
    api_key: 'YOUR_INTERCOM_API_KEY',
    mapping: 'User'
  }
}

2

Restart your Rails server

Intercom is now plugged to Forest

Intercom 1

1

Add the Intercom integration

Pass the Intercom options to the Forest init function.

app.use(Liana.init({
  // ...
  integrations: {
    intercom: {
      appId: 'YOUR_INTERCOM_APP_ID',
      apiKey: 'YOUR_INTERCOM_API_KEY',
      intercom: require('intercom-client'),
      mapping: 'user'
    }
  }
}));

2

Restart your Express server

Intercom is now plugged to Forest

Intercom 1

1

Add the Intercom integration

Pass the Intercom options to the Forest init function.

app.use(Liana.init({
  // ...
  integrations: {
    intercom: {
      appId: 'YOUR_INTERCOM_APP_ID',
      apiKey: 'YOUR_INTERCOM_API_KEY',
      intercom: require('intercom-client'),
      mapping: 'user'
    }
  }
}));

2

Restart your Express server

Intercom is now plugged to Forest

Intercom 1

Stripe

Configuring the Stripe integration for Forest allows you to have your user’s payments, invoices and cards alongside the corresponding user from your application. A refund action is also available out-of-the-box on the user_collection configured.

1

Add the Stripe integration

config/initializers/forest_liana.rb

# ...
ForestLiana.integrations = {
  stripe: {
    api_key: 'YOUR_STRIPE_SECRET_KEY',
    mapping: 'User.stripe_id'
  }
}

2

Restart your Rails server

Stripe is now plugged to Forest

Stripe 1

1

Add the Stripe integration

Pass the Stripe options to the Forest init function.

app.use(Liana.init({
  // ...
  integrations: {
    stripe: {
      apiKey: 'YOUR_STRIPE_SECRET_KEY',
      mapping: 'user.stripeId',
      stripe: require('stripe')
    }
  }
}));

2

Restart your Express server

Stripe is now plugged to Forest

Stripe 1

1

Add the Stripe integration

Pass the Stripe options to the Forest init function.

app.use(Liana.init({
  // ...
  integrations: {
    stripe: {
      apiKey: 'YOUR_STRIPE_SECRET_KEY',
      mapping: 'user.stripeId',
      stripe: require('stripe')
    }
  }
}));

2

Restart your Express server

Stripe is now plugged to Forest

Stripe 1

Smart Actions

Forest provides instantly all common actions such as CRUD, sort, search, etc. Sooner or later, you will need to perform actions on your data that are specific to your business. Moderating comments, logging into a customer’s account (a.k.a impersonate) or banning a user are exactly the kind of important tasks you need to make available in order to manage your day-to-day operations.

Creating an action

In the following example, we add the Ban user action to your Forest admin on a collection customers.

1

Declare the action in the collection schema

lib/forest_liana/collections/customer.rb

class Forest::Customer
  include ForestLiana::Collection

  collection :customers
  action 'Ban user'
end

2

Restart your Rails server

Your action is now visible on Forest

Action 1

3

Configure CORS

config/application.rb

We use the gem rack-cors for the CORS configuration.

class Application < Rails::Application
  # ...

  config.middleware.insert_before 0, 'Rack::Cors' do
    allow do
      origins 'app.forestadmin.com'
      resource '*', headers: :any, methods: :any
    end
  end
end

4

Declare the route

config/routes.rb (add the route before the Forest engine)

namespace :forest do
  post '/actions/ban-user' => 'customers#ban_user'
end

# ...
# mount ForestLiana::Engine => '/forest'

5

Create the controller

app/controllers/forest/customers_controller.rb

# ForestLiana::ApplicationController takes care of the authentication for you.
class Forest::CustomersController < ForestLiana::ApplicationController
  def ban_user
    # Your business logic to send an email here.
    render nothing: true, status: 204
  end
end

1

Declare the action in the collection schema

/forest/customers.js

var Liana = require('forest-express-mongoose');

Liana.collection('customers', {
  actions: [{ name: 'Ban user' }]
});


⚠ Notice, that Forest requires automatically the files inside the /forest directory. It’s nor necessary nor advised to require theses files manually in your code.

2

Restart your Express server

Your action is now visible on Forest

Action 1

3

Handle the route

Declare the route to the Express Router

var liana = require('forest-express-mongoose');

function banUser(req, res) {
  // Your business logic to ban a user here.
  res.status(204).send();
}

// liana.ensureAuthenticated middleware takes care of the authentication for you.
router.post('/forest/actions/ban-user', liana.ensureAuthenticated, banUser);

1

Declare the action in the collection schema

/forest/customers.js

var Liana = require('forest-express-sequelize');

Liana.collection('customers', {
  actions: [{ name: 'Ban user' }]
});


⚠ Notice, that Forest requires automatically the files inside the /forest directory. It’s nor necessary nor advised to require theses files manually in your code.

2

Restart your Express server

Your action is now visible on Forest

Action 1

3

Handle the route

Declare the route to the Express Router

var liana = require('forest-express-sequelize');

function banUser(req, res) {
  // Your business logic to ban a user here.
  res.status(204).send();
}

// liana.ensureAuthenticated middleware takes care of the authentication for you.
router.post('/forest/actions/ban-user', liana.ensureAuthenticated, banUser);

Handling input values

Sometimes, asking the admin to fill in a form before triggering the action is useful. Asking the amount for a refund, the subject and body message for sending an email or giving a movie a rating are perfect examples where you need to take into account user input.

1

Add the form fields to your action

lib/forest_liana/collections/rental.rb

Declare the list of fields you want to be filled before the action is triggered by passing a fields argument to the action method.

class Forest::Rental
  include  ForestLiana::Collection

  collection :rentals
  action 'refund', fields: [{
    field: 'Amount', type: 'Number'
  }]
end


Five types of field are currently supported: Boolean, Date, Number, String and File.

2

Restart your Rails server

The action form will appear when triggering the refund action.

Action 2

1

Add the form fields to your action

/forest/customers.js

Declare the list of fields you want to be filled before the action is triggered by passing a fields argument to the action method.

var Liana = require('forest-express-mongoose');

Liana.collection('rentals', {
  actions: [{
    name: 'Refund',
    fields: [
      { field: 'Amount', type: 'Number' }
    ]
  }]
});


Five types of field are currently supported: Boolean, Date, Number, String and File.

2

Restart your Express server

The action form will appear when triggering the ‘Ban user’ action.

Action 2

1

Add the form fields to your action

/forest/customers.js

Declare the list of fields you want to be filled before the action is triggered by passing a fields argument to the action method.

var Liana = require('forest-express-sequelize');

Liana.collection('rentals', {
  actions: [{
    name: 'Refund',
    fields: [
      { field: 'Amount', type: 'Number' }
    ]
  }]
});


Five types of field are currently supported: Boolean, Date, Number, String and File.

2

Restart your Express server

The action form will appear when triggering the ‘Ban user’ action.

Action 2

HTTP request payload

Selected records ids are passed to the HTTP request when triggering the action button. The fields/values will be passed to the values attributes.

{
  "data": {
    "attributes": {
      "ids": ["4285","1343"],
      "values": {
        "Amount":"1.56"
      }
    }
  },
  "type":"rentals"
}

Status message

You can respond to the HTTP request with a status message to notify the user whether the action was successfuly executed.

To display a custom error message, you have to respond with a 400 Bad Request status code and a simple payload like this: { error: 'The error message.' }.

To display custom success message, you have to respond with a 200 OK status code and a simple payload like this: { success: 'The success message.' }.

Smart Business Logic

Forest provides instantly all the common tasks you need as an admin of your application. Sometimes, these tasks are very specific to your business. Our goal is to provide the simplest and most beautiful way to extend the default behavior automatically implemented by the Liana. That’s why the Liana only generates a standard REST API. The learning curve to extend it is next to none, meaning you can set up your own custom business logic in a matter of minutes.

Extending the Admin API

1

Create a Rails Decorator

app/decorators/controllers/forest_liana/resources_controller.rb

ForestLiana::ResourcesController.class_eval do
  def destroy
    # Your business logic here.
  end
end

The previous example shows how to extend a DELETE on a record. list, get, update, create and destroy are the methods you can override or extend.

Instead of reimplementing all the business logic of a method, you can call the default behavior by invoking your aliased method.

ForestLiana::ResourcesController.class_eval do
  alias_method :default_destroy, :destroy

  def destroy
    # Your business logic here.
    default_destroy
  end
end

The parameters (params) passed to the method includes the collection and the ID of the record (except for the list method). A before_filter named find_resource is executed before the method. This filter is in charge of injecting to @resource the current model you are acting on.

1

Handle the route

Declare the route before the Forest Liana middleware

app.delete('/forest/users/:recordId', function (req, res, next) {
  // Your business logic here.
});

Instead of reimplementing all the business logic of a method, you can call the default behavior by invoking next().

Smart Collections

Forest is able to render your data models to its customizable UI. It covers the majority of use cases. If needed, you can create a Smart Collection to go one step further in the customization of your admin. A Smart Collection is a Forest Collection based on your API implementation. Reports or aggregated tables are two of the most common use cases.

Creating a Smart Collection

In the following example, the application has one table Item. Each item has a brand field (String). We create a collection Brand that contains each brand and their corresponding number of items.

Forest uses the JSON API standard. Your Smart Collection implementation should return a valid JSON API payload. You can do it manually or use the library you want. The following examples show you how to do it manually.

1

Create the Smart Collection

lib/forest_liana/collections/brand.rb

class Forest::Brand
  include ForestLiana::Collection

  collection :brands
  field :brand, type: 'String'
  field :count, type: 'Number'
end

2

Declare the route

config/routes.rb (before mounting the Forest Rails engine)

namespace :forest do
  resources :brands
end

3

Implement the API

app/controllers/forest/brands_controller.rb

# ForestLiana::ApplicationController takes care of the authentication for you.
class Forest::BrandsController < ForestLiana::ApplicationController
  def index
    brands = Item.all
      .group(:brand)
      .order(:brand)
      .count
      .map.with_index do |f, index|
        {
          id: index,
          type: 'brands',
          attributes: {
            brand: f.first,
            count: f.second
          }
        }
      end

    render json: { data: brands }
  end
end

4

Restart your Rails server

The Smart Collection will appear in the navigation bar.

1

Create the Smart Collection

/forest/brands.js

var Liana = require('forest-express-mongoose');

Liana.collection('brands', {
  fields: [
    { field: 'brand', type: 'String' },
    { field: 'count', type: 'Number' }
  ]
});

2

Handle the route

Declare the route to the Express Router

var Liana = require('forest-express-mongoose');

router.get('/forest/brands', Liana.ensureAuthenticated, function (req, res) {
  Item
    .aggregate()
    .group({
      _id: '$brand',
      count: { $sum: 1 }
    })
    .sort({
      _id: 1
    })
    .project({
      brand: '$_id',
      count: '$count',
      _id: false
    })
    .exec(function (err, values) {
      values = values.map(function (value, index) {
        return {
          id: index,
          type: 'brands',
          attributes: {
            brand: value.brand,
            count: value.count
          }
        };
      });

      res.send({ data: values });
    });
});

3

Restart your Express server

The Smart Collection will appear in the navigation bar.

1

Create the Smart Collection

/forest/brands.js

var Liana = require('forest-express-sequelize');

Liana.collection('brands', {
  fields: [
    { field: 'brand', type: 'String' },
    { field: 'count', type: 'Number' }
  ]
});

2

Handle the route

Declare the route to the Express Router

var Liana = require('forest-express-sequelize');
var models = require('../models');

router.get('/forest/brands', Liana.ensureAuthenticated, function (req, res) {
  // Your business logic here to retrieve the data you want.
  .then(function (values) {
    values = values.map(function (value, index) {
      return {
        id: index,
        type: 'brands',
        attributes: {
          brand: value.brand,
          count: value.count
        }
      };
    });

    res.send({ data: values });
  });
});

3

Restart your Express server

The Smart Collection will appear in the navigation bar.

Smart Field

Smart Collections are useful if you need to create a Forest Collection based on your API implementation, from scratch. However, it can be somewhat overengineered if you just want to add a new field to an existing collection without having to reimplement all your filters, sort, pagination, and so on. Computed fields are the most used case for Smart Fields (e.g. fullname, age, etc.), but you could do much more than that (e.g. scoring, extract info from JSON, etc.)

Creating a Smart Field

1

Create the Smart Field

lib/forest_liana/collections/user.rb

class Forest::User
  include ForestLiana::Collection

  collection :users

  field :fullname, type: 'String' do
    "#{object.firstname} #{object.lastname}"
  end
end

2

Restart your Rails server

The Smart Field will appear in your collection.

1

Create the Smart Field

/forest/user.js

'use strict';
var Liana = require('forest-express-mongoose');

Liana.collection('user', {
  fields: [{
    field: 'fullname',
    type: 'String',
    value: function (object) {
      return object.firstName + ' ' + object.lastName;
    }
  }]
});

2

Restart your Express server

The Smart Field will appear in your collection.

1

Create the Smart Field

/forest/user.js

'use strict';
var Liana = require('forest-express-sequelize');

Liana.collection('user', {
  fields: [{
    field: 'fullname',
    type: 'String',
    value: function (object) {
      return object.firstName + ' ' + object.lastName;
    }
  }]
});

2

Restart your Express server

The Smart Field will appear in your collection.

Creating a “hasMany” Smart Field

1

Create the Smart Field

lib/forest_liana/collections/actor.rb

class Forest::Actor
  include ForestLiana::Collection

  collection :actors

  has_many :top_movies, type: ['string'], reference: 'movies.id'
end

2

Declare the route

config/routes.rb

namespace :forest do
  get '/actors/:actor_id/relationships/top_movies' => 'actors#top_movies'
end

3

Create the controller

app/controllers/forest/actors_controller.rb

class Forest::ActorsController < ForestLiana::ApplicationController
  def top_movies
    movies = Actor
      .find(params['actor_id'])
      .movies
      .order('imdb_rating DESC')
      .limit(3)

    render json: serialize_models(movies, include: [], count: movies.count)
  end
end

2

Restart your Rails server

The Smart Field will appear in your collection.

1

Create the Smart Field

/forest/actor.rb

'use strict';
var Liana = require('forest-express-mongoose');

Liana.collection('actors', {
  fields: [{
    field: 'topmovies',
    type: ['String'],
    reference: 'movies.id'
  }]
});

2

Handle the route

Declare the route to the Express Router

var liana = require('forest-express-mongoose');
var Serializer = require('forest-express').ResourceSerializer;

function topMovies(req, res) {
  // your business logic to retrieve the top 3 movies
  .then((movies) => {
    return new Serializer(liana, models.movies, movies, {}, {
      count: movies.length
    }).perform();
  })
  .then((projects) => res.send(projects))
  .catch(next);
}

router.get('/forest/actor/:actorId/relationships/movies', topMovies);

3

Restart your Express server

The Smart Field will appear in your collection.

1

Create the Smart Field

/forest/actor.rb

'use strict';
var Liana = require('forest-express-sequelize');

Liana.collection('actors', {
  fields: [{
    field: 'topmovies',
    type: ['String'],
    reference: 'movies.id'
  }]
});

2

Handle the route

Declare the route to the Express Router

var liana = require('forest-express-sequelize');
var Serializer = require('forest-express').ResourceSerializer;

function topMovies(req, res) {
  // your business logic to retrieve the top 3 movies
  .then((movies) => {
    return new Serializer(liana, models.movies, movies, {}, {
      count: movies.length
    }).perform();
  })
  .then((projects) => res.send(projects))
  .catch(next);
}

router.get('/forest/actor/:actorId/relationships/movies', topMovies);

3

Restart your Express server

The Smart Field will appear in your collection.

SmartField 1

How to’s

Deploying to production

Forest makes it really simple to deploy your admin to production.

1

Create a new production environment into Forest

Project settings -> Environments -> + Add a new environment

Environment 1

2

Configure those environment variables

On your production server

Provide your FOREST_ENV_SECRET (given by Forest) and the FOREST_AUTH_SECRET (random string) in the environment variables of your production server.

3

Deploy your code

On your production server

3

Deploy your Forest UI configuration (optional)

On your production environment

Now that you’re all set, you can duplicate the UI configuration of your development environment into your production environment to avoid having to repeat everything from the beginning. It will overwrite the default UI configuration.

Select your environment, click on “Deploy”, then “Save”. You’re now ready to use Forest on production!

Environment 2

Impersonating a user

Implementing the impersonate action allows you to login as one of your users and see exactly what they see on their screen.

The following example shows how to create an Impersonate Action on a collection named User.

1

Create the Impersonate action

lib/forest_liana/collections/user.rb

class Forest::User
  include  ForestLiana::Collection

  collection :users
  action 'Impersonate'
end

2

Restart your Rails server

Your action is now visible on Forest

1

Declare the action in the collection schema

/forest/users.js

var Liana = require('forest-express-mongoose');

Liana.collection('users', {
  actions: [{ name: 'Impersonate' }]
});

2

Restart your Express server

Your action is now visible on Forest

More details about creating an action can be found in the “Simple custom action” section.

Impersonate 1

The impersonate process can be done in 2 steps:

  • POST '/actions/impersonate' returns a generated URL with a JWT token as a query param. The token contains the user ID to impersonate and the details of the admin who triggered the action.
  • GET '/actions/impersonate?token=...' verifies the JWT token, fetches the user from the database and configures the session depending on your authentication system.

Impersonate 2

3

Declare the routes

config/routes.rb

namespace :forest do
  post '/actions/impersonate' => 'actions#impersonate_token'
  get '/actions/impersonate' => 'actions#impersonate'
end

4

Create the controller

app/controllers/forest/actions_controller.rb

class Forest::CustomersController < ForestLiana::ApplicationController

  def impersonate_token
    # Find the user to impersonate.
    user = User.find(params['data']['attributes']['ids'].first)

    # Build the payload of the JWT token.
    payload = {
      user_id: user.id,
      admin_email: forest_user['data']['data']['email'],
      admin_teams: forest_user['data']['data']['teams']
    }

    # Generate the token using a random secret key.
    token = JWT.encode(payload, ENV['IMPERSONATE_SECRET_KEY'], 'HS256')

    # Respond with the URL.
    render json: {
      html: "<a href='http://localhost:3000/forest/actions/impersonate?token=#{token}'>"\
              "Login as #{user.email}</a>".html_safe
    }
  end

  def impersonate
    # Decode the JWT token.
    payload = JWT.decode(params['token'], ENV['IMPERSONATE_SECRET_KEY'].first)

    # Fetch the user from the database.
    user = User.find(payload['user_id'])

    # Impersonate the user using the gem Pretender
    # (https://github.com/ankane/pretender).
    impersonate_user(user)

    # Redirect to the root page of the application.
    redirect_to '/'
  end

end

3

Handle the route

Declare the route to the Express Router

var liana = require('forest-express-mongoose');

function impersonateToken(req, res, next) {

  // Find the user to impersonate
  User.findById(req.body.data.attributes.ids[0], (err, user) => {
    if (err) { return next(err); }

    // Build the payload of the JWT token
    var payload = {
      userId: user.id,
      adminEmail: req.user.data.email,
      adminTeams: req.user.data.teams,
    };

    // Generate the token using a random secret key
    var token = jwt.sign(payload, process.env.IMPERSONATE_SECRET_KEY);

    // Respond with the URL
    res.json({
      html: '<a href="http://localhost:3000/forest/actions/impersonate?token=' +
        token + '">Login as…</a>'
    });
  });
}

function impersonate(req, res, next) {
  // Decode the JWT token.
  var payload = jwt.verify(req.query.token, process.env.IMPERSONATE_SECRET_KEY);

  // Fetch the user from the database.
  User.findById(payload.userId, (err, user) => {
    if (err) { return next(err); }

    // Impersonate the user using Passport.js and redirect to the root of
    // the application.
    req.login(user, () => res.redirect('/'));
  });
}

router.post('/forest/actions/impersonate', liana.ensureAuthenticated,
  impersonateToken);

router.get('/forest/actions/impersonate', impersonate);

3

Handle the route

Declare the route to the Express Router

var liana = require('forest-express-sequelize');
var models = require('../models');

function impersonateToken(req, res, next) {
  // Find the user to impersonate
  models.user
    .findById(req.body.data.attributes.ids[0])
    .then((user) => {
      // Build the payload of the JWT token
      var payload = {
        userId: user.id,
        adminEmail: req.user.data.email,
        adminTeams: req.user.data.teams,
      };

      // Generate the token using a random secret key
      var token = jwt.sign(payload, process.env.IMPERSONATE_SECRET_KEY);

      // Respond with the URL
      res.json({
        html: '<a href="http://localhost:3000/forest/actions/impersonate?token=' +
          token + '">Login as…</a>'
      });
    });
}

function impersonate(req, res, next) {
  // Decode the JWT token.
  var payload = jwt.verify(req.query.token, process.env.IMPERSONATE_SECRET_KEY);

  // Fetch the user from the database.
  models.user
    .findById(payload.userId)
    .then((user) => {
      // Impersonate the user using Passport.js and redirect to the root of
      // the application.
      req.login(user, () => res.redirect('/'));
    });
}

router.post('/forest/actions/impersonate', liana.ensureAuthenticated,
  impersonateToken);

router.get('/forest/actions/impersonate', impersonate);

Importing data

Forest natively supports data creation but it’s sometimes more efficient to simply import it. This “How-to” shows a way to achieve that in your Forest admin.

1

Create the "Bulk import" smart action with a File input field

-> Smart Action - Handling input values

2

Restart your Rails server

Your action is now visible

Importing data 1

3

Declare the route

config/routes.rb

namespace :forest do
  post '/actions/bulk-import' => 'actions#bulk_import'
end

4

Create the controller

app/controllers/forest/actions_controller.rb

In the following example, we use data_uri to parse the encoded file. You should add it to your Gemfile too.

class Forest::ActionsController < ForestLiana::ApplicationController

  def bulk_import
    uri = URI::Data.new(params.dig('data', 'attributes', 'values', 'file'))
    uri.data.force_encoding('utf-8')

    CSV.parse(uri.data).each do |row|
      # Your business logic to create your data here.
    end

    render json: { success: 'Data successfuly imported!' }
  end

end

2

Restart your Express server

Your action is now visible

Importing data 1

3

Handle the route

Declare the route in the Express Router

In the following example, we use parse-data-uri and csv NPM packages to parse the encoded file. You should add it to your package.json too.

const parseDataUri = require('parse-data-uri');
const csv = require('csv');

function bulkImport(req, res, next) {
  let parsed = parseDataUri(req.body.data.attributes.values.file);

  csv.parse(parsed.data, function (err, row) {
    // Your business logic to create your data here.

    res.send({ success: 'Data successfuly imported!' })
  });
}

router.put('/forest/actions/bulk-import', liana.ensureAuthenticated, updateMovie);

Uploading images

Image uploading is one of the most common operations people do in their admin. There’s plenty of ways to handle it in your application. Forest supports natively the most common file attachment libraries. In case you’re not using one of them, Forest gives you the flexibility to plug your own file upload business logic.

Paperclip and CarrierWave are both well known libraries to manage file attachments in Rails. Forest supports them natively, which means you don’t need to configure anything more to upload and crop your images in Forest.

The following example shows you how to handle the update of a record image. The image field should be a string that contains the image URL. You have to configure the file picker widget on it.

Image upload 1

Having done that, your image is now rendered on your Details view. You can upload a new one when editing your record and clicking on the image. Optionally, you can also crop it.

Image upload 2

Hitting the Apply changes button will update your record with your new image encoded in base64 (RFC 2397).

Now, you have to extend the default behavior of the PUT method on your admin API to upload your image where you want. The following example shows you how to upload the image to AWS S3.

1

Override the update method

app/decorators/controllers/forest_liana/resources_controller.rb

ForestLiana::ResourcesController.class_eval do
  alias_method :default_update, :update

  # Regexp to match the RFC2397 prefix.
  REGEXP = /\Adata:([-\w]+\/[-\w\+\.]+)?;base64,(.*)/m

  def update
    # Create the S3 client.
    s3 = AWS::S3.new(access_key_id: ENV['S3_ACCESS_KEY'],
                     secret_access_key: ENV['S3_SECRET_KEY'])
    bucket = s3.buckets[ENV['S3_BUCKET']]

    # Parse the "data" URL scheme (RFC 2397).
    data_uri_parts = raw_data.match(REGEXP) || []

    # Upload the image.
    obj = bucket.objects.create(filename(data_uri_parts),
                                base64_image(data_uri_parts),
                                opts(data_uri_parts))

    # Inject the new poster URL to the params.
    url = obj.public_url().to_s
    params['resource']['data']['attributes']['poster'] = url
    params['data']['attributes']['poster'] = url

    # Finally, call the default PUT behavior.
    default_update
  end

  private

  def raw_data
    params['data']['attributes']['poster']
  end

  def base64_image(data_uri_parts)
    Base64.decode64(data_uri_parts[2])
  end

  def extension(data_uri_parts)
    MIME::Types[data_uri_parts[1]].first.preferred_extension
  end

  def filetype(data_uri_parts)
    MIME::Types[data_uri_parts[1]].first.to_s
  end

  def filename(data_uri_parts)
    ('a'..'z').to_a.shuffle[0..7].join + ".#{extension(data_uri_parts)}"
  end

  def opts(data_uri_parts)
    {
      acl: 'public_read',
      content_type: filetype(data_uri_parts)
    }
  end

end

2

Restart your Rails server

File upload is now handled.

1

Override the PUT method

Declare the route before the Forest Express middleware

var AWS = require('aws-sdk');

function randomFilename() {
  return require('crypto').randomBytes(48, function(err, buffer) {
    var token = buffer.toString('hex');
  });
}

function updateMovie(req, res, next) {
  // Create the S3 client.
  var s3Bucket = new AWS.S3({ params: { Bucket: process.env.S3_BUCKET }});

  // Parse the "data" URL scheme (RFC 2397).
  var rawData = req.body.data.attributes.pictureUrl;
  var base64Image = rawData.replace(/^data:image\/\w+;base64,/, '');

  // Generate a random filename.
  var filename = randomFilename();

  var data = {
    Key: filename,
    Body: new Buffer(base64Image, 'base64'),
    ContentEncoding: 'base64',
    ACL: 'public-read'
  };

  // Upload the image.
  s3Bucket.upload(data, function(err, response) {
    if (err) { return reject(err); }

    // Inject the new poster URL to the params.
    req.body.data.attributes.pictureUrl = response.Location;

    // Finally, call the default PUT behavior.
    next();
  });
};

router.put('/forest/movies/:movieId', liana.ensureAuthenticated, updateMovie);

2

Restart your Express server

File upload is now handled.

Image upload 3