Developer Guide For Express/Sequelize

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 investments of time and resources.

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 or crosses 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 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.

// 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

Forest allows to handle even the most complex scenarios. The Smart label enables you to implement your own business logic with code, when necessary. Check this page out for more details on all Smart features

Term Definition
Admin API The API generated by the Forest Liana to manage all your application data and business operations.
Collection A set of records having the same type usually stored in the database.
Data Token A token used to authenticate your requests on your Admin API.
Field An attribute of a collection.
Forest Liana The locally-installed plugin that analyzes your data model and generates the Admin API
Forest UI The web application of Forest Admin, accessible from any browser at https://app.forestadmin.com
Forest UI Schema A schema of your data model generated by the Forest Liana in order to initialize your Forest UI
FOREST_AUTH_SECRET A private token - chosen by yourself - used to sign the data token.
FOREST_ENV_SECRET A private token - given by Forest Admin - used to identify your project environment in Forest.
DATABASE_URL The path to the database used by your app.
Relationship A connection between two collections.
Segment A subset of a collection gathering filtered records.
Smart Action A button that triggers server-side logic.
Smart Collection A group of records gathered from different sources and implemented following your business logic.
Smart Field A field that displays a computed value in your collection
Smart Relationship A relationship that displays a link to another collection.
Smart Route A route is simply the mapping between an API endpoint and the business logic behind this endpoint.
Smart View A custom view you can code using HTML/CSS/JSS to display data in any way you want.

For the purpose of this guide and our Live Demo, we use third parties librairies that will be present in our coding examples:

Layout Editor

The Layout Editor is an essential tool to manage your teams permissions and optimize the admin interface of each business unit.

No coding required, you can directly configure the interface directly using the Forest Layout Editor.

Layout Editor 1

This feature is available for admin and editor roles only.

Note that if you change your admin configuration, it will be reflected on the entire team on which you are currently logged in (e.g. if you hide a collection within the "Admin" team, all users of this team will no longer see this collection).

Showing, hiding and re-ordering collections

You may want to display only relevant information to your teammates. Sometimes, sensible data shouldn’t be exposed to everyone within your company.

In the navigation sidebar containing your collections, activate the layout editor to:

  • reorder collections by drag and drop
  • hide or show collections by checking or not the box next to your collection name.

Layout Editor 2

Showing, hiding and re-ordering fields

In the same way as for collections, for the sake of clarity for operations, you can activate the layout editor to:

  • reorder fields by drag and drop
  • hide or show fields that are not relevant (e.g. id, updated_at) by checking or not the box next to your collection name.

Layout Editor 3

Collection settings

If you’ve activated the Layout Editor, you’ve certainly noticed the orange “cog” icon beside each collection name in the navigation sidebar. Clicking on this icon give you access to your collection settings.

Let’s take a closer look at each of these settings…

General

This is where you can rename your collection to make it more user-friendly, set a nice icon, change the default sorting (e.g. created_at) and more.

Layout Editor 4

Fields

In the Fields section of your collection settings, you’ll be able to:

  • change the display name of your field.
  • add a description to your field for a clearer explanation.
  • set the field in read-only mode so that users won’t be able to modify it.
  • choose from a list of widget to better display your data (google map, rich text editor, file picker, document viewer, date picker and more).

Layout Editor 5

Note that if you are choosing date picker` for a date field, you will be able to change the `format`. Please refer to momentjs syntax for formatting options.

Segments

In this section, you can create segments on your collections.

Segments are made for those who are willing to systematically visualize data according to specific sets of filters. It allows you to save your filters configuration so that you don’t have to compute the same actions every day (e.g. signup this week, pending transactions).

Layout Editor 6

If you’re looking to implement a more complex segment, you might want to take a look at Smart Segments.

Smart Actions

Smart actions are specific to your business. A smart action can be triggered from a record (e.g. user, company) or from the collection.

In the Smart Actions section of your collection settings you have 3 options:

  • display or not the action to restrict the access from your teammates
  • ask for confirmation before triggering the action
  • make the action visible on some segments only

Layout Editor 7

Smart Views

In this section you can configure your Smart Views using JS, HTML, and CSS.

To make a specific smart view as a default view, click on your collection, activate the layout editor, and drag and drop your view to the top at the top right of your screen.

Layout Editor 8

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 six 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, …)
  • Percentage (% of paying customers, …)
  • Leaderboard (Companies who emitted the most transactions, …)
  • Objective (Orders passed per year VS objective, …)

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 the charts you want.

Simple mode

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.
  • 1 or multiple filters.

Analytics 1`

Query mode

Premium feature

The Query mode has been designed to provide you with a flexible, easy to use and accessible interface when hard questions need to be answered. Simply type SQL queries using the online editor and visualize your data graphically.

Analytics 7`

The syntax of the SQL examples below can be different depending on the database type (SQLite, MySQL, Postgres, MS SQL, etc.). Please, refer to your database documentation for more information.

Single value

The returned column must be name value. In the following example, we simply count the number of “customers”.

SELECT COUNT(*) AS value
FROM customers;


Single value (with growth percentage)

The returned columns must be name value and previous. In the following example, we simply count the number of “customers” in January 2018 and compare this value to the number of “customers” in the previous month.

SELECT current.count AS value, previous.count AS previous
FROM (
  SELECT COUNT(*)
  FROM customers
  WHERE created_at BETWEEN '2018-01-01' AND '2018-02-01'
) as current, (
  SELECT COUNT(*)
  FROM customers
  WHERE created_at BETWEEN '2017-12-01' AND '2018-01-01'
) as previous;


Repartition

The returned columns must be name key and value. In the following example, we simply count the number of “customers” distributed by country.

SELECT country AS key, COUNT(*) as value
FROM "customers"
GROUP BY country;


Time-based

The returned columns must be name key and value. In the following example, we simply count the number of “customers” per month.

SELECT DATE_TRUNC('month', "createdAt") AS key, COUNT(*) as value
FROM customers
GROUP BY created_at;


Leaderboard

The returned columns must be named ‘key’ and value and LIMIT must be defined. In the following example, we limited the leaderboard to 12 items.

Leaderboard 1

Objective

The returned columns must be named ‘key’ and objective. In the following example, we set manually the objective to 120.

Objective-chart 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

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

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

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);

Creating analytics per record

Forest’s dashboard is handy when it comes to monitoring the overall KPIs. But you may find the analytics module useful for a more in-depth examination of a specific company, user or any other items.

Analytics 6`

“Analytics per record” only supports Smart Charts. The parameter record_id is automatically passed in the HTTP body to access the record the user is currently seeing.

Fields

What is a field?

A field is simply an attribute defined in your database. Examples of fields: first name, gender, status, etc.

Customizing a field

Forest allows you to customize how a field appears in your admin interface. You can rename it, choosing the right widget to display (e.g. text area, image viewer, Google map), adding a description or even setting the read/write access.

Field 1`

To customize a field, go to Collection Settings -> Fields. Then select the field to start configuring it.

What is a Smart Field?

A field that displays a computed value in your collection.

Smart field

A Smart Field is a column that displays processed-on-the-fly data. It can be as simple as concatenating attributes to make them human friendly, or more complex (e.g. total of orders).

Try it out with 1 these 2 examples (it only takes 3 minutes):

Example: Concatenate First and Last names

1

Create the Smart Field

/forest/user.js

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

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

2

Restart your Express server

The Smart Field will appear in your collection.

Example: Number of orders for a customer

1

Create the Smart Field

/forest/user.js

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

Liana.collection('user', {
  fields: [{
    field: 'numberOfOrders',
    type: 'Number',
    get: function (object) {
      // returns a Promise
      return models.environment.count({
        where: { projectId: project.id }
      });
    }
  }]
});

2

Restart your Express server

The Smart Field will appear in your collection.

Updating a Smart Field

In order to update a Smart Field, you just need to write the logic to “unzip” the data. Note that the set method should always return the object it’s working on. In the example hereunder, the user is returned.

If you need an Async process, you can also return a Promise that will return the user at the end.

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

Liana.collection('user', {
  fields: [{
    field: 'fullName',
    type: 'String',
    get: function (user) {
      if (user.firstName && user.lastName) {
        return `${user.firstName} ${user.lastName}`;
      } else {
        return null;
      }
    },
    set: function (user, value) {
      var names = value.split(' ');
      user.firstName = names[0];
      user.lastName = names[1];

      return user;
    }
  }]
});

Searching on a Smart Field

To perform search on a Smart Field, you also need to write the logic to “unzip” the data, then the search query which is specific to your zipping. In the example hereunder, the firstname and lastname are searched separately after having been unzipped.

If you are working with Async, you can also return a Promise.

'use strict';
const _ = require('lodash');
const Liana = require('forest-express-sequelize');

Liana.collection('user', {
  fields: [{
    field: 'fullName',
    type: 'String',
    get: function (user) {
      if (user.firstName && user.lastName) {
        return `${user.firstName} ${user.lastName}`;
      } else {
        return null;
      }
    },
    search: function (query, search) {
      let s = models.sequelize;
      let split = search.split(' ');

      var searchCondition = s.and(
        { firstName: { $ilike: split[0] }},
        { lastName: { $ilike: split[1] }}
      );

      let searchConditions = _.find(query.where.$and, '$or');
      searchConditions.$or.push(searchCondition);
    }
  }]
});

⚠️ Note that, sorting on Smart Field is not supported in Forest. Indeed, being able to sort on Smart Field would mean that we have to compute the Smart Field values for all the records of your collection, and then sort the records on this value.

While this is something we could implement, it would not be something scalable. Imagine if your collection has millions of records.

If this sorting is really important for your operations, you should consider the creation of a dedicated column in the database.

Restrict the search on specific fields

Sometimes, searching on all fields is not relevant and may even involve big performance issues. You can restrict the search on specific fields only using the option searchFields, which should be defined in your forest/ folder. In this example, we configure Forest to only search on the fields name and industry of our collection Company.

Available Widget Options

Widgets fields can be very handy and a great time saver to users with no technical knowledge. Depending on the type of your field, you can choose different widgets (see list below). You can find these options in your collection settings under the fields section.

Depending on the field type in your DB, you have different choices of Widgets available:

Type Widget Description
string link Display clickable link instead of a raw string
  text area Enlarge the text area.
  color picker Change the field to a color picker (hexadecimal code).
  image viewer Display an image instead of a URL link.
  file picker Add a file from your computer.
  rich text editor Write formatted text.
  JSON editor Display a JSON field.
  dropdown Put a drop-down menu of fields among pre-defined options.
  document viewer Display your document in a preview window.
  map Display a location on a map (⚠️ your field must be a “string” respecting the following format “lat, long”).
  address Allow auto-complete on an address field.
date text input Convert your date into a string.
  date picker Use the date picker to select a date.
number/integer belongsTo select Display a dropdown list of related data.
  price Compute your number in the currency of your choice.
array carrousel Display an array of images URLs as a carrousel.


You can see below some of the widgets and how they look:

Widgets list

Relationships

What is a relationship?

A relationship is a connection between two collections.

Forest supports natively all the relationships defined in your Sequelize models (belongsTo, hasMany, …). Check the Sequelize documentation to create new ones.

relationship

What is a Smart Relationship?

Sometimes, you want to create a relationship between two set of data that does not exist in your database. A concrete example could be creating a relationship between two collections available in two different databases. Creating a Smart Relationship allows you to customize with code how your collections are linked together.

Try it out with one these 2 examples (it only takes a few minutes):

Example: “belongsTo” Smart Relationship

In the following example, we add the last delivery man of a customer to your Forest admin on a collection customers.

1

Create the Smart Relationship

/forest/customer.js

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

Liana.collection('user', {
  fields: [{
    field: 'delivery_man',
    type: 'String',
    reference: 'deliveryMan.id',
    get: function (object) {
      // returns a Promise
      return models.deliveryMan
        .findOne({
          // where: { ... }
        });
    }
  }]
});

Example: “hasMany” Smart Relationship

In the following example, we add the top 3 movies of an actor to your Forest admin on a collection actors.

1

Create the Smart Field

/forest/actor.js

'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');

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

router.get('/forest/actor/:actorId/relationships/movies', topMovies);
⚠️ Make sure to serialize the response properly using the liana.ResourceSerializer to allow Forest to display your data.
3

Restart your Express server

The Smart Field will appear in your collection.

SmartField 1

Actions

What is an action?

An action is a button that triggers server-side logic through an API call. Without a single line of code, Forest supports natively all common actions required on an admin interface such as CRUD (Create, Read, Update, Delete), sort, search, data export.

What is a Smart Action?

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.

Action

Try it out with this example (it only takes 3 minutes):

Example: Banning a user

In the following example, we add the Ban user action to the admin on a collection customers. By default, the API triggered created by a Smart Action is simply a POST on /forest/actions/<dasherize_name_of_the_action>. You can customize the route with the optional parameter endpoint to get a user friendly name and control the API call.

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

You can declare the list of fields when your action requires parameters from the Human in front of the screen. Available options for each field are:

  • Add a description that will help humans understand what value is expected.

  • Determine whether a field is to be required or not.

  • Set a default value.

  • Add a widget to provide a simple and easy-to-use way of entering value for your end-users. Five widgets are currently supported: JSON editor, rich text editor, date picker, text area, text input.

1

Add the form fields to your action

/forest/customers.js

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

Liana.collection('reviews', {
  actions: [{
    name: 'Approve',
    fields: [{
      field: 'Comment',
      type: 'String',
      description: 'Personal description',
      isRequired: true,
      defaultValue: 'I approve this comment',
      widget: 'text area'
    }]
  }]
});


Six types of field are currently supported: Boolean, Date, Dateonly, Number, String, File and Enum. If you choose the Enum type, you can pass the list of possible values through the enums key:

{ field: 'Country', type: 'Enum', enums: ['USA', 'CA', 'NZ'] }
2

Restart your Express server

The action form will appear when triggering the approve 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"],
      "collection_name": "reviews",
      "values": {
        "Comment":"I approve this comment"
      }
    }
  },
  "type":"reviews"
}

Customizing response

You can respond to the HTTP request with a simple status message to notify the user whether the action was successfully executed or respond in a more personalized and visible way with an html page.

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.' }.

You can also display a custom html page as a response using { html: '<h1>Congratulations!</h1><p>The comment has been successfully approved</p> ... ... ...'}.

Action 2

Downloading a file

The response of an action will be considered as a file to download if you pass the option ‘download’ to the action declaration. It’s very useful if you need actions like “Generate an invoice” or “Download PDF”.

1

Add the download option to your action

/forest/customer.js

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

Liana.collection('customer', {
  actions: [{ name: 'Download file', download: true }]
});
2

Send the file as a response

In your Express route

function generateInvoice(request, response) {
  var options = {
    root: __dirname + '/../public/docs',
    dotfiles: 'deny',
    headers: {
      'Access-Control-Expose-Headers': 'Content-Disposition',
      'Content-Disposition': 'attachment; filename="invoice-234.pdf"'
    }
  };

  var fileName = 'invoice-234.pdf';
  response.sendFile(fileName, options, (error) => {
    if (error) { next(error); }
  });
}
3

Restart your Express server

The action returns now a file as a response

If you want to create an action accessible from the details or the summary view of a record involving related data, this section may interest you.

In the example below, the “Add new transaction” action (1) is accessible from the summary view (2). This action creates a new transaction and automatically refresh the “Emitted transactions” related data section (3) to see the new transaction.

Refresh related data

Below is the sample code. We use faker to generate random data in our example. Remember to install it if you wish to use it (npm install faker or yarn install faker).

Prefill a form with default values

Forest Admin allows you to set default values to your form. In this example, we will prefill the form with data coming from the record itself (1), with just a few extra lines of code.

Smart Action Default Value

Triggering an action from the collection

Passing the option global: true makes your Smart Action visible directly from your collection without having to select records before. For example, our “Import data” Smart Action example uses this option.

1

Add the global option to your action

/forest/customer.js

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

Liana.collection('customer', {
  actions: [{ name: 'Import data', global: true }]
});

Action 4

Restrict a smart action to specific users

When using Forest with several teams and when you have clear roles defined it becomes relevant to restrict a smart action only to a few collaborators. This option is accessible through the Edit layout mode (1) in the Smart actions’ section (3) of the collection’s options (2), in a dropdown menu (4).

Restrict Smart Action

Require approval for a Smart action

Enterprise-only

Critical actions for your business may need approval before being processed.

The option “This smart action requires an approval” (3) is accessible in the Smart action section (1) of your collection’s settings. In our example, the “Approve transaction” action (2) requires approval. You can select approvers among your users(4). Once this option is activated, a warning pop up indicates that the approval workflow is need to perform this action.

Approval workflow 1

Actions requiring approval will be available in the Collaboration menu (5) in the “Approvals” section (6):

  • “Requested” for all incoming requests (yours to approve or not)
  • “To Review” (7) for requests you need to review
  • “History” for all past requests.

In “To Review” you will be able to reject or approve the request (8) with an optional message for more details.

Approval workflow 2

You can access the detail on an action (9) to have more context (10) by clicking on it.

Approval workflow 3

Segments

What is a segment?

A Segment is a subset of a collection gathering filtered records.

Segments are made for those who are willing to systematically visualize data according to specific sets of filters. It allows you to save your filters configuration so that you don’t have to compute the same actions every day (e.g. signup this week, pending transactions).

Creating a Segment

Simple segment

Forest provides a straightforward UI to configure step-by-step the segments you want. The only information the UI needs to create a segment within a collection is a name and some filters.

Segment 1

SQL Query segment

Forest gives you a second option to create segment. SQL queries allow you to create advanced filters and connect your data through a few lines of code. In our example, through the Edit layout (1) we access the Transactions collection’s settings (2). Then, in Segments (3), we create a New segment (4) for which we choose a name (5). Pick the Querymode instead of the simple one (6), type in your SQL query (7) and do not forget to save (8).

SQL Segment 1 SQL Segment 2

Creating a Smart Segment

Sometimes, segment filters are complicated and closely tied to your business. Forest allows you to code how the segment is computed.

In the following example, we add the VIP segment to your Forest admin on a collection customers.

1

Declare the segment in the collection schema

/forest/customers.js

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

Liana.collection('customers', {
  segments: [{
    name: 'VIP',
    where: () => {
      // Compute and return a list of customer IDs.
    }
  }]
});

You’re free to implement the business logic you need. The only requirement is to return a list of your collection IDs (customer IDs in this example).

2

Restart your Express server

Your segment is now visible on Forest

Segment 2

Setting up independent columns visibility

By default, Forest applies the same configuration to all segments of the same collection.

However, the “independent columns configuration” option (1) allows you to display different columns on your different segments.

Segment visibility

Collections

What is a collection?

A collection is a set of data elements physically stored in the database displayed in tabular form (by default), with rows (i.e. records) and columns named (i.e. fields). A collection has a specified number of columns, but can have any number of rows.

Forest automatically analyses your data models and instantly make your collections available in the Forest 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.

What is a Smart Collection?

A Smart Collection is a Forest Collection based on your API implementation. It allows you to reconcile fields of data coming from different or external sources in a single tabular view (by default), without having to physically store them into your database.

Fields of data could be coming from many other sources such as other B2B SaaS (e.g. Zendesk, Salesforce, Stripe), in-memory database, message broker, etc.

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

/forest/brands.js

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

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


You can add the option isSearchable: true to your collection to display the search bar.

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.

Views

What is a view?

A view is simply the way to visualize your data in the Forest UI. By default, Forest renders your data using a table view when you access multiple records and a form view to access, edit and create a specific record.

What is a Smart View?

Premium feature

Smart Views lets you code your view using JS, HTML, and CSS. They are taking data visualization to the next level. Ditch the table view and display your orders on a Map, your events in a Calendar, your movies, pictures and profiles in a Gallery. All of that with the easiness of Forest.

Map View 1

Creating a Smart View

Forest provides an online editor to inject your Smart View code. The editor is available on the collection settings of a collection, then in the “Smart views” tab.

The code of a Smart View is a Ember Component and simply consists of a template and a Javascript.

The template is written using Handlebars. Don’t panic it’s very simple.

Smart View 1 Collection settings -> Smart Views.

Getting your records

The records of your collection are accessible in the records property. Here’s how to iterate over them in the template section:

{{#each records as |record|}}

{{/each}}


Accessing a specific record

For each record, you will access its attributes through the forest-attributeName property. The forest- preceding the field name is required.

<p>
  {{record.forest-firstName}}
</p>

Accessing belongsTo relationships

Accessing a belongsTo relationship works in exactly the same way as accessing a simple field. Forest triggers automatically an API call to retrieve the data from your Admin API only if it’s necessary. Let’s take the following schema example and display the address field of the related Customer.

Smart collection

<p>
  {{record.forest-order.forest-customer.forest-address}}
</p>

Accessing hasMany relationships

Accessing a hasMany relationship works in exactly the same way as accessing a simple field. Forest triggers automatically an API call to retrieve the data from your Admin API only if it’s necessary. Let’s take the following schema example and display the available_at field of the related Menu.

Smart collection

{{#each record.forest-menus as |menu|}}
  <p>{{menu.forest-available_at}}</p>
{{/each}}

Refreshing data

In order to refresh the records on the page, trigger the fetchRecords action:

<button {{action 'fetchRecords'}}>
  Refresh data
</button>

Fetching data

Trigger an API call to your Admin API in order to fetch records from any collection of your admin.

We will use the store service directly available from your javascript file to do that:

'use strict';
import Ember from 'ember';

export default Ember.Component.extend({
  // ...
  store: Ember.inject.service('store'),
  fetchData: function () {
    let params = {
      filter: {
        'available_at': '>' + moment().startOf('isoWeek').toISOString(), // Beginning of this week
                        ',<' + moment().endOf('isoWeek').toISOString(), // End of this week
      },
      filterType: 'and',
      timezone: 'Europe/Paris',
      'page[size]': 100
    };

    // This will trigger an API call to your Admin API.
    this.get('store').query('forest_chef-availability', params)
      .then((availabilities) => {
        this.set('availabilities', availabilities);
      });
  }.on('init'),
  / ...
});

In the example above, the result is set to the variable availabilities. You can access it directly from your template:

{{#each availabilities as |availability|}}
  <p>{{availability.id}}</p>
{{/each}}


A full working example is available in the Calendar example.

Available parameters

  • filter: A list of filter you want to apply. Syntax is a javascript object that contains <field>: '<operator><value>,<operator><value>,...'
  • filterType: and or or
  • timezone: The timezone string
  • page[number]: The page number
  • page[size]: The number of records per page

Deleting records

The deleteRecords action lets you delete one or multiple records. A panel will automatically ask for a confirmation when a user triggers the delete action.

<button {{action 'deleteRecords' record}}>
  Delete record
</button>

Triggering a Smart Action

Here’s how to trigger your Smart Actions directly from your Smart Views:

<button {{action 'triggerSmartAction' collection 'Generate invoice' record}}>
  Generate invoice
</button>

You can pass an array or a single record.

Available properties

  • collection (Model): The current collection.
  • currentPage (Number): The current page.
  • isLoading (Boolean): Indicates if the UI is currently loading records or
  • numberOfPages (Number): The total number of available pages.
  • records (Array): Your data entries.
  • searchValue (String): The current search.

Examples

Try it out with one these 3 examples (each one taking about 10 minutes):

Example: Map view

Map View 1

1

Create the Smart View

Open the Collection settings - Smart Views

2

Implement the template

TEMPLATE

<style>
  #map {
    width: 100%;
    height: 100%;
    z-index: 4;
  }
</style>


<div id="map"></div>
3

Implement the javascript

JAVASCRIPT

'use strict';
import Ember from 'ember';

export default Ember.Component.extend({
  tagName: '',
  router: Ember.inject.service('-routing'),
  map: null,
  loaded: false,
  loadPlugin: function() {
    var that = this;
    Ember.run.scheduleOnce('afterRender', this, function () {
      Ember.$.getScript('//cdnjs.cloudflare.com/ajax/libs/leaflet/1.3.1/leaflet.js', function () {
        that.set('loaded', true);
      });

      var cssLink = $('<link>');
      $('head').append(cssLink);

      cssLink.attr({
        rel:  'stylesheet',
        type: 'text/css',
        href: '//cdnjs.cloudflare.com/ajax/libs/leaflet/1.3.1/leaflet.css'
      });
    });
  }.on('init'),
  displayMap: function () {
    if (!this.get('loaded')) { return; }

    var markers = [];
    $('#map_canvas').height($('.l-content').height());

    this.get('records').forEach(function (record) {
      var split = record.get('forest-geoloc').split(',');
      markers.push([split[0], split[1], record.get('id')]);
    });

    this.map = new L.Map('map');

    var osmUrl='//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
    var osmAttrib='Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors';
    var osm = new L.TileLayer(osmUrl, { attribution: osmAttrib });

    this.map.setView(new L.LatLng(37.7869148, -122.3998675), 13);
    this.map.addLayer(osm);

    this.addMarker(markers);
  }.observes('records.[]', 'loaded').on('didInsertElement'),
  addMarker: function (markers) {
    var that = this;

    markers.forEach(function (marker) {
      var lat = parseFloat(marker[0]);
      var lng = parseFloat(marker[1]);

      var recordId = marker[2];
      var record = that.get('records').findBy('id', recordId);
      var displayValue = record.get(
        that.get('collection.displayFieldWithNamespace')) ||
        record.get('forest-email') || record.get('id');

      marker = L.marker([lat, lng]).addTo(that.map);

      marker.bindPopup('<strong>Delivery man</strong><p>' + displayValue + '</p>')

      marker.on('mouseover', function (e) { this.openPopup(); });
      marker.on('mouseout', function (e) { this.closePopup(); });
      marker.on('click', function () {
        that.get('router')
          .transitionTo('rendering.data.collection.list.viewEdit.details',
            [that.get('collection.id'), recordId]);
      });

      setInterval(function () {
        marker.setLatLng(new L.latLng(lat -= 0.0001, lng -= 0.0001));
      }, Math.floor(Math.random() * 2000) + 300);
    });
  }
});


Having done that, your Map view is now rendered on your list view.

Example: Calendar view

Calendar View 1

1

Create the Smart View

Open the Collection settings - Smart Views

2

Implement the template

TEMPLATE

<style>
  .calendar {
    padding: 20px;
    background: white;
    height:100%;
    overflow: scroll;
  }
  .calendar .fc-toolbar.fc-header-toolbar .fc-left {
    font-size: 14px;
    font-weight: bold;
  }
  .calendar .fc-day-header {
    padding: 10px 0;
    background-color: #f7f7f7;
  }
  .calendar .fc-event {
    background-color: #f7f7f7;
    border: 1px solid #ddd;
    color: #555;
    font-size: 14px;
  }
  .calendar .fc-day-grid-event {
    background-color: #4285f4;
    color: white;
    font-size: 10px;
    border: none;
    padding: 2px;
  }
</style>

<div id='' class="calendar"></div>
3

Implement the javascript

JAVASCRIPT

'use strict';
import Ember from 'ember';
import SmartViewMixin from 'client/mixins/smart-view-mixin';

export default Ember.Component.extend(SmartViewMixin.default, {
  router: Ember.inject.service('-routing'),
  store: Ember.inject.service(),
  conditionAfter: null,
  conditionBefore: null,
  loaded: false,
  calendarId: null,
  loadPlugin: function() {
    var that = this;
    Ember.run.scheduleOnce('afterRender', this, function () {
      if (this.get('viewList.recordPerPage') !== 50) {
        this.set('viewList.recordPerPage', 50);
        this.sendAction('updateRecordPerPage');
      }

      that.set('calendarId', `${this.get('element.id')}-calendar`);

      Ember.$.getScript('//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.1.0/fullcalendar.min.js', function () {
        that.set('loaded', true);

        $(`#${that.get('calendarId')}`).fullCalendar({
          allDaySlot: false,
          minTime: '00:00:00',
          defaultDate: new Date(2018, 5, 1),
          eventClick: function (event, jsEvent, view) {
            that.get('router')
              .transitionTo('rendering.data.collection.list.viewEdit.details',
                [that.get('collection.id'), event.id]);
          },
          viewRender: function(view, element) {
            const field = that.get('collection.fields').findBy('field', 'availableAt');

            if (that.get('conditionAfter')) {
              that.sendAction('removeCondition', that.get('conditionAfter'), true);
              that.get('conditionAfter').destroyRecord();
            }
            if (that.get('conditionBefore')) {
              that.sendAction('removeCondition', that.get('conditionBefore'), true);
              that.get('conditionBefore').destroyRecord();
            }

            const conditionAfter = that.get('store').createRecord('condition');
            conditionAfter.set('field', field);
            conditionAfter.set('operator', 'is after');
            conditionAfter.set('value', view.start);
            conditionAfter.set('smartView', that.get('viewList'));
            that.set('conditionAfter', conditionAfter);

            const conditionBefore = that.get('store').createRecord('condition');
            conditionBefore.set('field', field);
            conditionBefore.set('operator', 'is before');
            conditionBefore.set('value', view.end);
            conditionBefore.set('smartView', that.get('viewList'));
            that.set('conditionBefore', conditionBefore);

            that.sendAction('addCondition', conditionAfter, true);
            that.sendAction('addCondition', conditionBefore, true);

            that.sendAction('fetchRecords', { page: 1 });
          }
        });
      });

      var cssLink = $('<link>');
      $('head').append(cssLink);

      cssLink.attr({
        rel:  'stylesheet',
        type: 'text/css',
        href: '//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.1.0/fullcalendar.min.css'
      });
    });
  }.on('init'),
  setEvent: function () {
    if (!this.get('records')) { return; }

    var events = [];
    var calendar = $(`#${this.get('calendarId')}`);
    calendar.fullCalendar('removeEvents');

    this.get('records').forEach(function (availability) {
      var event = {
          id: availability.get('forest-chef').get('id'),
          title: availability.get('forest-chef').get('forest-lastname'),
          start: availability.get('forest-available_at'),
      };

      calendar.fullCalendar('renderEvent', event, true);
    });
  }.observes('loaded', 'records.[]')
});


Having done that, your Calendar view is now rendered on your list view.

Gallery View 1

1

Create the Gallery view

Open the Collection settings - Smart Views

2

Implement the template

TEMPLATE

<style>
  .c-movies.l-table--content {
    text-align: center;
    margin-top: 15px;
    margin-bottom: 50px;
    overflow-y: auto;
  }

  .c-movies__image img {
    height: 350px;
    width: 250px;
    margin: 3px;
    border: 1px solid #bbb;
    border-radius: 3px;
		transition: all .3s ease-out;
  }

  .c-movies__image:hover {
    transform: scale(1.05);
  }
</style>

<section class="l-view__element l-view__element--table u-f-g c-movies l-table l-table--content">
  <div>
    {{#each records as |record|}}
      {{#link-to 'rendering.data.collection.list.viewEdit.details' collection.id record.id class="c-movies__image"}}
        <img class="c-movies__image" src={{record.forest-poster}}>
      {{/link-to}}
    {{/each}}
  </div>
</section>

{{table/table-footer records=records currentPage=currentPage
  fetchPage="fetchPage" currentUser=currentUser customView=customView
  updateRecordPerPage="updateRecordPerPage" collection=collection
  numberOfPages=numberOfPages fetchRecords="fetchRecords"}}
3

Implement the javascript

JAVASCRIPT

'use strict';
import Ember from 'ember';

export default Ember.Component.extend({
  actions: {
    updateRecordPerPage() {
      this.get('customView')
        .save()
        .then(() => this.sendAction('fetchRecords'));
    },
    fetchRecords(olderOrNewer) {
      this.sendAction('fetchRecords', olderOrNewer);
    }
  }
});


Having done that, your Gallery view is now rendered on your list view.

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

Premium feature

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

Pass the Intercom options to the Forest init function.

app.use(Liana.init({
  // ...
  integrations: {
    intercom: {
      accessToken: process.env.INTERCOM_ACCESS_TOKEN,
      intercom: intercomClient,
      mapping: ['user'],
    }
  }
}));

The “mapping” option is the collection on which your integration is pointing.

2

Restart your Express server

Intercom is now plugged to Forest

Intercom 1

Stripe

Premium feature

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

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

Mixpanel

Premium feature

The Mixpanel integration allows you to fetch Mixpanel’s events and display them at a record level into Forest.

1

Install the mandatory package

To benefit from Mixpanel integration, you need to add the package `mixpanel-data-export` before going further

2

Add the Mixpanel integration

In our example we will map the `customers.email` with the data coming from Mixpanel. You may replace by your own relevant collection(s).

By default, Mixpanel is sending the following fields: id, event, date, city, region, country, timezone, os, osVersion, browser, browserVersion. If you want to add other fields from Mixpanel, you have to add them in `customProperties`

app.use(Liana.init({
  // ...
  integrations: {
    mixpanel: {
      apiKey: process.env.MIXPANEL_API_KEY,
      apiSecret: process.env.MIXPANEL_SECRET_KEY,
      mapping: ['customers.email'],
      customProperties: ['Campaign Source', 'plan', 'tutorial complete'],
      mixpanel: require('mixpanel-data-export')
    },
  },
}));
3

Restart your Express server

You will then be able to see the Mixpanel events on a record, a `Customer` in our example

Mixpanel integration 1

How to’s

Deploying to production

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

1

Create a new production environment

Click "Deploy to production"

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

4

Copy layout

Deploy your Forest UI configuration to 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

Managing team, user permissions

Premium feature

As a an admin (See users, roles and permissions), you can create different teams.

Easily configure the interface of your teams to:

  • Give limited access to your employees or contractors.
  • Optimize the admin interface per business unit: success, support, sales or marketing teams.

Team permission 1

To add a new team, go to your project settings -> Teams -> + New team. If you already have configured a team, you can easily copy the configuration from a previous team to the new one.

Once created, invite your teammates to join your team.

Extending the Admin API

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.

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().

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

More details can be found in the Creating an 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.
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);

Impersonate 2

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" with 'File' type and the 'global' option set to true

-> Smart Action - Handling input values

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.

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 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