Skip to main content
Translation datasources require advanced knowledge of Forest’s query interface.
The translation strategy is an advanced approach for creating your own datasources that involves translating Forest’s query interface into the target API’s query language.

Overview

A full-featured query translation module typically exceeds 1000 lines of code. This approach suits full-featured databases and requires deep understanding of Forest’s internals.
Translation datasource capabilities diagram

Key steps

Implementing this strategy requires completing three main phases:
  1. Structure declaration - Define the data structure
  2. Capabilities declaration - Specify API capabilities
  3. Translation layer implementation - Code the actual query translation

Minimal example

class MyCollection extends BaseCollection {
  constructor(dataSource) {
    super('myCollection', dataSource);
    // Add fields with type, filtering, and sorting capabilities
  }

  async list(caller, filter, projection) {
    // Translate Forest query to API format
    const params = QueryGenerator.generateListQueryString(filter, projection);
    const response = axios.get('https://my-api/my-collection', { params });
    return response.body.items;
  }
}

Structure declaration

Columns

Define fields with types, validation, and default values:
const { BaseCollection } = require('@forestadmin/datasource-toolkit');

class MovieCollection extends BaseCollection {
  constructor() {
    // [...]

    this.addField('id', {
      type: 'Column',
      columnType: 'Number',
      isPrimaryKey: true,
    });

    this.addField('title', {
      type: 'Column',
      columnType: 'String',
      validation: [{ operator: 'Present' }],
    });

    this.addField('mpa_rating', {
      type: 'Column',
      columnType: 'Enum',
      enumValues: ['G', 'PG', 'PG-13', 'R', 'NC-17'],
      defaultValue: 'G',
    });

    this.addField('stars', {
      type: 'Column',
      columnType: [{ firstName: 'String', lastName: 'String' }],
    });
  }
}

Typing

The typing system for columns is the same as the one used when declaring fields in the back-end customization step.

Validation

Forest permits declaring validation rules on primitive-type fields. These rules validate records during creation/updating in the back-office interface. The validation API mirrors the condition tree structure but excludes a “field” entry. Example validation clause:
{
  "aggregator": "and",
  "conditions": [
    { "operator": "present" },
    { "operator": "like", "value": "found%" },
    { "operator": "today" }
  ]
}

Relationships

Important: Only intra-datasource relationships belong at the collection level. For inter-datasource relationships, use jointures during customization. Data sources using the query translation strategy require careful implementation for relationships.
const { BaseCollection } = require('@forestadmin/datasource-toolkit');

class MovieCollection extends BaseCollection {
  constructor() {
    // [...]

    this.addField('director', {
      type: 'ManyToOne',
      foreignCollection: 'people',
      foreignKey: 'directorId',
      foreignKeyTarget: 'id',
    });

    this.addField('actors', {
      type: 'ManyToMany',
      foreignCollection: 'people',
      throughCollection: 'actorsOnMovies',

      originKey: 'movieId',
      originKeyTarget: 'id',
      foreignKey: 'actorId',
      foreignKeyTarget: 'id',
    });
  }
}

Capabilities declaration

Data source implementers don’t need to translate every possible query type. Forest ensures only supported query features are available by having collections declare capabilities on construction.

Required features

All datasources must support:
  • Listing records
  • And nodes in condition trees
  • Or nodes in condition trees
  • Equal operator on primary keys
  • Paging (skip, limit)
Note: Translating the Or node is a strong constraint, as many backends will not allow it: providing a working implementation may require making multiple queries and recombining the results.

Optional features (opt-in)

Unlocked featureRequired capabilities
Pagination page count displayCount
ChartsAll field support in Aggregation
RelationsIn on primary/foreign keys
Select all for actions/deleteIn and NotIn on primary key
Frontend filters, scopes, segmentsPer-field operator support
Operator emulationIn on primary keys
Search emulationContains on strings; Equal on numbers/UUIDs/enums

UI filter requirements by field type

To unlock GUI filtering:
  • Boolean: Equal, NotEqual, Present, Blank
  • Date: All date operators
  • Enum: Equal, NotEqual, Present, Blank, In
  • Number: Equal, NotEqual, Present, Blank, In, GreaterThan, LessThan
  • String: Equal, NotEqual, Present, Blank, In, StartsWith, EndsWith, Contains, NotContains
  • UUID: Equal, NotEqual, Present, Blank

Collection-level capabilities

Count

Enables pagination widget to display total page count. Requires implementing the aggregate method:
class MyCollection extends BaseCollection {
  constructor() {
    this.enableCount();
  }
}
Allows custom search implementation instead of default condition tree approach. Useful for full-text search (ElasticSearch, etc.):
class MyCollection extends BaseCollection {
  constructor() {
    this.enableSearch();
  }
}

Segments

Define segments at datasource level when condition trees are insufficient or segments are shared across projects:
class MyCollection extends BaseCollection {
  constructor() {
    this.addSegments(['Active records', 'Deleted records']);
    // All filter-accepting methods MUST handle segment fields
  }
}

Field-level capabilities

Write support

Mark fields as read-only:
this.addField('id', {
  isReadOnly: true,
});

Filtering operators

Declare supported operators per field:
this.addField('id', {
  filterOperators: new Set([
    'Equal',
    // additional operators
  ]),
});

Sort support

Flag sortable fields:
this.addField('id', {
  isSortable: true,
});

Read implementation

Emulation strategy

Emulation enables rapid development by allowing features to be tested in Node.js before optimization. This approach trades performance for faster iteration.

Basic list implementation

const { BaseCollection } = require('@forestadmin/datasource-toolkit');
const axios = require('axios');

class MyCollection extends BaseCollection {
  async list(caller, filter, projection) {
    // Fetch all records
    const response = await axios.get('https://my-api/my-collection');
    const result = response.data.items;

    // Apply in-process emulation
    if (filter.conditionTree)
      result = filter.conditionTree.apply(result, this, caller.timezone);
    if (filter.sort) result = filter.sort.apply(result);
    if (filter.page) result = filter.page.apply(result);

    return projection.apply(result);
  }
}

Aggregate method

The aggregate method handles both record counting and chart data generation:
async aggregate(caller, filter, aggregation, limit) {
  const records = await this.list(caller, filter, aggregation.projection);
  return aggregation.apply(records, caller.timezone, limit);
}

Optimization: count queries

Handle count operations separately if your API supports efficient counting:
async aggregate(caller, filter, aggregation, limit) {
  if (aggregation.operation === 'Count' && aggregation.groups.length === 0) {
    return [{ value: await this.count(caller, filter) }];
  }
  // Handle general case
}

Write implementation

Making your records editable is achieved by implementing the create, update and delete methods. Important: The three write methods accept filter parameters, but unlike the list method, pagination support is unnecessary.
const { BaseCollection } = require('@forestadmin/datasource-toolkit');
const axios = require('axios'); // client for the target API

/** Naive implementation of create, update and delete on a REST API */
class MyCollection extends BaseCollection {
  constructor() {
    this.addField('id', { /* ... */ isReadOnly: true });
    this.addField('title', { /* ... */ isReadOnly: false });
  }

  async create(caller, records) {
    const promises = records.map(async record => {
      const response = await axios.post('https://my-api/my-collection', record);
      return response.data;
    });

    return Promise.all(promises); // Must return newly created records
  }

  async update(caller, filter, patch) {
    const recordIds = await this.list(caller, filter, ['id']); // Retrieve ids
    const promises = recordIds.map(async ({ id }) => {
      await axios.patch(`https://my-api/my-collection/${id}`, patch);
    });

    await Promise.all(promises);
  }

  async delete(caller, filter) {
    const recordIds = await this.list(caller, filter, ['id']); // Retrieve ids
    const promises = recordIds.map(async ({ id }) => {
      await axios.delete(`https://my-api/my-collection/${id}`);
    });

    await Promise.all(promises);
  }
}

Method details

  • create(): Must return the newly created records with all fields populated
  • update(): Receives filter and patch object; updates matching records
  • delete(): Receives filter; deletes all matching records

Intra-datasource relationships

When building your own datasources using the translation strategy, collections must handle intra-datasource relationships that are declared in their structure.

Relationship types and requirements

Automatic handling:
  • one-to-many relationships
  • many-to-many relationships
For these types, Forest will automatically call the destination collection with a valid filter, requiring no additional implementation work. Manual implementation required:
  • many-to-one relationships
  • one-to-one relationships
These require developers to make all fields from the target collection available on the source collection (under a prefix).

Handling prefixed fields

When a many-to-one relationship exists, the collection must accept references using dot notation throughout its operations.

Structure declaration example

class MovieCollection extends BaseCollection {
  constructor() {
    super('movies', null);

    this.addField('director', {
      type: 'ManyToOne',
      foreignCollection: 'people',
      foreignKey: 'directorId',
      foreignKeyTarget: 'id',
    });
  }
}

Query example

The system can execute calls using both source and target collection fields:
await dataSource.getCollection('movies').list(
  caller,
  {
    conditionTree: {
      aggregator: 'And',
      conditions: [
        { field: 'title', operator: 'Equal', value: 'E.T.' },
        { field: 'director:firstName', operator: 'Equal', value: 'Steven' },
      ]
    },
    sort: [{ field: 'director:birthDate', ascending: true }]
  },
  ['id', 'title', 'director:firstName', 'director:lastName']
);

Expected response structure

{
  "id": 34,
  "title": "E.T",
  "director": { "firstName": "Steven", "lastName": "Spielberg" }
}

Implementation scope

Developers implementing your own datasources must handle prefixed field references in:
  • Filters (condition trees)
  • Projections (field selections)
  • Aggregations (calculation operations)
Want to share your datasource with the community? Check out the Forest experimental repository to contribute.