Node.js Developer Guide
Other documentationsDemoCommunityGitHub
  • Forest Admin
  • Getting started
    • How it works
    • Quick start
    • Install
      • Create your agent
      • Expose an HTTP endpoint
        • For standalone agents
        • On Express
        • On Koa
        • On Fastify
        • On NestJS
      • Autocompletion & Typings
      • Troubleshooting
    • Migrating legacy agents
      • What's new
      • Pre-requisites
      • Recommendations
      • Migration steps
        • Run new agent in parallel
        • Configure database connection
        • Code transformations
          • API Charts
          • Live Queries
          • Smart Charts
          • Route overrides
          • Smart Actions
          • Smart Fields
          • Smart Relationships
          • Smart Segments
        • Compare schemas
        • Swap agents
      • Post-migration
        • Dropping Sequelize
        • Optimize your agent
  • Data Sources
    • Getting Started
      • Collection selection
      • Naming conflicts
      • Cross-data source relationships
      • Query interface and Native Queries
        • Fields and projections
        • Filters
        • Aggregations
    • Provided data sources
      • SQL (without ORM)
      • Sequelize
      • Mongoose
      • MongoDB
    • Write your own
      • Replication strategy
        • Persistent cache
        • Updating the replica
          • Scheduled rebuilds
          • Change polling
          • Push & Webhooks
        • Schema & References
        • Write handlers
      • Translation strategy
        • Structure declaration
        • Capabilities declaration
        • Read implementation
        • Write implementation
        • Intra-data source Relationships
      • Contribute
  • Agent customization
    • Getting Started
    • Actions
      • Scope and context
      • Result builder
      • Static Forms
      • Widgets in Forms
      • Dynamic Forms
      • Form layout customization
      • Related data invalidation
    • Charts
      • Value
      • Objective
      • Percentage
      • Distribution
      • Leaderboard
      • Time-based
    • Fields
      • Add fields
      • Move, rename and remove fields
      • Override binary field mode
      • Override writing behavior
      • Override filtering behavior
      • Override sorting behavior
      • Validation
    • Hooks
      • Collection hook
      • Collection override
    • Pagination
    • Plugins
      • Provided plugins
        • AWS S3
        • Advanced Export
        • Flattener
      • Write your own
    • Relationships
      • To a single record
      • To multiple records
      • Computed foreign keys
      • Under the hood
    • Search
    • Segments
  • Frontend customization
    • Smart Charts
      • Create a table chart
      • Create a bar chart
      • Create a cohort chart
      • Create a density map
    • Smart Views
      • Create a Map view
      • Create a Calendar view
      • Create a Shipping view
      • Create a Gallery view
      • Create a custom tinder-like validation view
      • Create a custom moderation view
  • Deploying to production
    • Environments
      • Deploy on AWS
      • Deploy on Heroku
      • Deploy on GCP
      • Deploy on Ubuntu
      • Deploy on Azure
    • Development workflow
    • Using branches
    • Deploying your changes
    • Forest Admin CLI commands
      • init
      • login
      • branch
      • switch
      • set-origin
      • push
      • environments:create
      • environments:reset
      • deploy
  • Under the hood
    • .forestadmin-schema.json
    • Data Model
      • Typing
      • Relationships
    • Security & Privacy
Powered by GitBook
On this page
  • Code cheatsheet
  • Do you still need a computed field?
  • Steps
  • Step 1: Implement a read-only version of the field
  • Step 2: Implement write handler
  • Step 3: Implement the filters you use

Was this helpful?

  1. Getting started
  2. Migrating legacy agents
  3. Migration steps
  4. Code transformations

Smart Fields

PreviousSmart ActionsNextSmart Relationships

Last updated 7 months ago

Was this helpful?

This is the official documentation of the @forestadmin/agent Node.js agent.

In legacy agents declaring a smart field was done in one big step.

In the new agent, the process was split into multiple steps, depending on the capabilities of the field (writing, filtering, sorting, etc.).

This was done to reduce the complexity of the code and to make it easier to understand, but also because it allows customers to reuse the same API when customizing the behavior of normal fields, and thus reduce the API surface that you need to learn.

You can find the full documentation of field customization .

Code cheatsheet

Legacy agent
New agent

get: (record) => { ... }

getValues: (records) => { ... }

set: (record, value) => { ... }

.replaceFieldWriting(...)

filter: ({ condition, where }) => { ... }

.replaceFieldOperator(...) .emulateFieldOperator(...) .emulateFieldFiltering(...)

type: 'String'

columnType: 'String'

enums: ['foo', 'bar']

columnType: 'Enum', enumValues: ['foo', 'bar']

reference: 'otherCollection.id'

Do you still need a computed field?

Smart fields were a powerful tool, but they were also a performance bottleneck.

In the new agent, we have introduced two new concepts that can replace many of the use cases of smart fields:

If you were using a smart field to move a field from one collection to another or to create a link to another record in the UI, you can likely use one of these much simpler solutions.

Steps

Step 1: Implement a read-only version of the field

Computed fields in the new agent are declared by calling the addField function on the appropriate collection.

Many changes have been made to the API.

Dependencies are explicit

You will notice that a new dependencies property is required when declaring a computed field.

It is an array of field names that tells Forest Admin which fields the getValues() function depends on. Unlike the legacy agent, the new agent will not automatically fetch the whole record.

Fields now work in batches

Even if it adds some complexity, exposing a batch API to our customers is a much better solution for performance.

The get function is now called getValues: it no longer takes a single record as its first argument, but an array of records, and must return an array of values, one for each record, in the same order.

Other changes

There are other minor changes to the API:

  • the type property was renamed to columnType.

  • The field property no longer exists. The field name is now the first argument of the addField function.

  • The enums property was renamed to enumValues.

Example

In the following example, we will port a field that fetches the full address of a user from a third-party service.

Note that when displaying a list of records, the new agent will only make one call to your handler, and then display the results for all records, instead of making one call per record.

collection('users', {
  fields: [
    {
      field: 'full_address',
      type: 'String',
      get: async user => {
        const addr = await geoWebService.getAddress(customer.address_id);

        return [addr.line_1, addr.line_2, addr.city, addr.country].join('\n');
      },
    },
  ],
});
agent.customizeCollection('users', users => {
  users.addField('full_address', {
    columnType: 'String',
    dependencies: ['address_id'],
    getValues: users =>
      users.map(async user => {
        const addr = await geoWebService.getAddress(customer.address_id);

        return [addr.line_1, addr.line_2, addr.city, addr.country].join('\n');
      }),
  });
});

Step 2: Implement write handler

If you want your computed field to be writable, you will need to call the .replaceFieldWriting() function.

This part is very similar to the legacy agent. The API change is because this function can be used to make any field writable, not just computed fields, or to change the default writing behavior of a normal field.

collection('users', {
  fields: [
    {
      field: 'full_address',
      type: 'String',
      get: /* ... same as before ... */,
      set: async (user, value) => {
        const address = await geoWebService.getAddress(customer.address_id);
        address.line_1 = value.split('\n')[0];
        address.line_2 = value.split('\n')[1];
        address.city = value.split('\n')[2];
        address.country = value.split('\n')[3];

        await geoWebService.updateAddress(address);

        // You can optionally return a hash of attributes to update the record
        return {};
      },
    },
  ],
});
agent.customizeCollection('users', users => {
  users
    .addField('full_address', { /* ... same as before ... */ })
    .replaceFieldWriting('full_address', (value) => {
      const address = await geoWebService.getAddress(customer.address_id);
      address.line_1 = value.split('\n')[0];
      address.line_2 = value.split('\n')[1];
      address.city = value.split('\n')[2];
      address.country = value.split('\n')[3];

      await geoWebService.updateAddress(address);

      // You can optionally return a hash of attributes to update the record
      return {};
    });
});

Step 3: Implement the filters you use

Structure

Implementing filters in the new agent is done operator by operator, instead of using a single function.

This allows more fine-grained control over the behavior of each operator and makes it possible to implement only the operators you need.

Return value

Because the new Forest Admin agent is designed to work with multiple databases, the return value of the filter function is not a Sequelize or Mongoose condition anymore.

Emulation

Example

collection('users', {
  fields: [
    {
      field: 'full_address',
      type: 'String',
      get: /* ... same as before ... */,
      filter: ({ condition, where }) => {
        switch (condition.operator) {
          case 'equal':
            return { $and: [/* ... Sequelize or Mongoose conditions ... */] };
          case 'less_than':
            return { $and: [/* ... Sequelize or Mongoose conditions ... */] };

          // [...] all other operators
        }
      },
    },
  ],
});
agent.customizeCollection('users', users => {
  users
    .addField('full_address', {
      /* ... same as before ... */
    })

    // Implement the operators you actually use for best performance.
    .replaceFieldOperator('full_address', 'Equal', (value, context) => ({
      aggregator: 'And',
      conditions: [],
    }))
    .replaceFieldOperator('full_address', 'LessThan', (value, context) => ({
      aggregator: 'And',
      conditions: [],
    }))

    // Emulate other operators.
    .emulateFieldOperator('full_address', 'GreaterThan')
    .emulateFieldOperator('full_address', 'NotEqual')

    // [Or] Emulate all operators which are not already defined in one call.
    .emulateFieldFiltering('full_address');
});

You can and .

The reference property no longer exists: use the .

Also note that unlike in the legacy agent, will be performed by the agent, so the number of operators that you need to implement to unlock all filtering is much lower.

Instead, you'll be building a that will be translated to the appropriate database syntax by the agent.

At the cost of performance, you can tell the agent to the behavior of a given operator by calling the .emulateFieldOperator() function.

here
Relationships
smart relationships guide
Use a smart relationship
Moving fields
emulate
automatic operator replacement
condition tree
fetch data from relations
fetch data from other computed fields