Fields

This is the official documentation of Forest Admin Cloud.

A Field within a Collection specifies the attribute's structure and content. Fields are usually auto-detected and linked to the appropriate collection based on your connected data source.

To make an admin panel better for users, it is often important to create new fields that are not directly tied to a database field. These fields may be dynamically computed or sourced from other data sources or services.

For example, instead of storing a user's age, a database typically stores their birthdate. Showing the age instead of the birthdate directly on the user interface improves readability and simplifies data segmentation. Several other examples exist, such as the full name, the number of orders, the total amount spent, a tag indicating the risk level of a transaction, etc.

The following sections assume that you have correctly followed all the steps in the Code Customization Getting Started guide.

TL;DR

Ensure you update the collection and fields names as needed.

src/index.ts
import type { Agent } from '@forestadmin/forest-cloud';
import { Schema } from '../typings';

export default function customizeAgent(agent: Agent<Schema>) {
  agent.customizeCollection('users', (collection) => {
    collection.addField('full_name', {
      columnType: 'String',
      dependencies: ['first_name', 'last_name'],
      getValues: (records, context) => {
        return records.map(r => `${r.first_name} ${r.last_name}`);
      }
    });
  });
}

To make the code easier to read, all the code snippets below should be wrapped in the following code.

import type { Agent } from '@forestadmin/forest-cloud';
import { Schema } from '../typings';

export default function customizeAgent(agent: Agent<Schema>) {
  // Insert the code snippet here.
}

Creating a Field

First, you need to call the customizeCollection() method on the agent.

agent.customizeCollection('users', (collection) => {
  // ...
});

Arguments:

  • name* String: The name of the collection to customize.

  • handle* Function: A function that has the collection instance as an argument to start customizing it.

To create a field, use the addField() method on your collection instance.

agent.customizeCollection('users', (collection) => {
  collection.addField('full_name', {
    columnType: 'String',
    dependencies: [],
    getValues: () => {
      return [];
    }
  });
});

Arguments:

  • name* String: The name of the field.

  • definition* Object: A JavaScript object that contains the definition of the field:

    • columnType* String: The type of the new field which can be any primitive or composite type.

    • dependencies* [String]: An array of field or relationship names that the custom field value depends on.

    • getValues* Function: A function that processes new values in batches, returning an array in the same order as the input records.

      • records [Object]: An array of JavaScript objects representing the list of records that need to have the field value computed.

      • context Object: The context data.


Creating fields can impact the performance of your admin panel. It is crucial to review best practices for fields to guarantee an optimal experience for your admin panel users.

Importing a field

Reflecting your database structure as-is in the admin panel can make it harder for users. A good practice is to simplify the interface to help users work faster. One common way is to bring a field from a relationship directly into its parent record.

Database schema in this example
Table: users
+----+-----------+------------+------------+
| ID | firstName | lastName   | addressId  |
+----+-----------+------------+------------+
|    |           |            |            |
+----+-----------+------------+------------+

Table: addresses
+----+------------+--------------+-----------+------------+
| ID | streetName | streetNumber | city      | countryId  |
+----+------------+--------------+-----------+------------+
|    |            |              |           |            |
+----+------------+--------------+-----------+------------+

Table: countries
+----+-------+
| ID | name  |
+----+-------+
|    |       |
+----+-------+
agent.customizeCollection('users', (collection) => {
  users
    .importField('city', { path: 'addresses:city', readonly: true })
    .importField('country', { path: 'addresses:countries:name', readonly: true });
});

The users collection now includes two direct fields: city and country. You can set these fields to be editable or not by toggling the readonly option.

Renaming a field

Renaming a field can improve readability and can be done using the renameField() method.

agent.customizeCollection('users', (collection) => {
  users.renameField('account_v3_uuid_new', 'account');
});

Renaming fields only affects their display in the admin panel. To access them in your code, always use their original names.

Removing a field

To hide fields in the UI for technical, confidentiality, or any other reasons, use the removeField() method.

agent.customizeCollection('users', (collection) => {
  users.removeField('password');
});

Removing fields only affects their display in the admin panel. However, you can still access them in your code, for example, as dependencies to compute new fields.

Examples

To make the code easier to read, all the code snippets below should be wrapped in the following code. Ensure you update the collection and action names as needed.

import type { Agent } from '@forestadmin/forest-cloud';
import { Schema } from '../typings';

export default function customizeAgent(agent: Agent<Schema>) {
  agent.customizeCollection('users', (collection) => {
    // Insert the code snippet here.
  });
}

Concatenating two fields

collection.addField('full_name', {
  columnType: 'String',
  dependencies: ['first_name', 'last_name'],
  getValues: (records, context) => {
    return records.map(r => `${r.first_name} ${r.last_name}`);
  }
});

Depending on a "many-to-one" relationship

In the example below, adding address:city to the list of dependencies makes the related data available in the getValues() function.

Database schema in this example
Table: users
+----+-----------+----------------+-----------+
| ID | addressId |   firstName    | lastName  |
+----+-----------+----------------+-----------+
|    |           |                |           |
+----+-----------+----------------+-----------+

Table: address
+----+-------+
| ID | city  |
+----+-------+
|    |       |
+----+-------+
collection.addField('displayName', {
  columnType: 'String',
  dependencies: ['firstName', 'lastName', 'address:city'],
  getValues: (records, context) =>
    records.map(r => `${r.firstName} ${r.lastName} (from ${r.address.city})`),
});

Depending on a "one-to-many" relationship

In the example below, we want to add a users.totalSpending field by summing the amounts of all orders.

Database schema in this example
Table: users
+----+
| ID |
+----+
|    |
+----+

Table: orders
+----+-------------+--------+
| ID | customer_id | amount |
+----+-------------+--------+
|    |             |        |
+----+-------------+--------+
  1. Retrieve record IDs: Start by getting all the record IDs.

  2. Filter orders for current users: Use these IDs to filter orders that belong to current users.

  3. Aggregate orders by user: Group the orders by customer_id and sum up the total order amount for each user.

  4. Display totals: Finally, show the total amount spent by each user. If a user hasn't placed any orders, their total will be shown as 0.

collection.addField('totalSpending', {
  columnType: 'Number',
  dependencies: ['id'],
  getValues: async (records, context) => {
    const recordIds = records.map(r => r.id);

    const filter = {
      conditionTree: { field: 'customer_id', operator: 'In', value: recordIds },
    };

    const aggregation = {
      operation: 'Sum',
      field: 'amount',
      groups: [{ field: 'customer_id' }],
    };

    const rows = await context.dataSource
      .getCollection('order')
      .aggregate(filter, aggregation);

    return records.map(record => {
      const row = rows.find(r => r.group.customer_id === record.id);
      return row?.value ?? 0;
    });
  },
});

Fetching data from an API

Suppose we want to see if we can actually send emails to our users' email addresses. We can use a checking tool called a verification API to do this job. The API we're using is not real, and this is how it responds:

{
  "username1@domain.com": {
    "usernameChecked": false,
    "usernameValid": null,
    "domainValid": true
  },
  "username2@domain.com": {
    "usernameChecked": false,
    "usernameValid": null,
    "domainValid": true
  }
}
Database schema in this example
Table: users
+----+-------+
| ID | Email |
+----+-------+
|    |       |
+----+-------+
collection.addField('emailDeliverable', {
  columnType: 'Boolean',
  dependencies: ['email'],
  getValues: async (records, context) => {
    const response = await emailVerificationClient.verifyEmails(
      records.map(r => r.email),
    );

    return records.map(r => {
      const check = response[r.email];
      return check.domainValid && (!usernameChecked || usernameValid);
    });
  },
});

Last updated