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.
Key steps
Implementing this strategy requires completing three main phases:
- Structure declaration - Define the data structure
- Capabilities declaration - Specify API capabilities
- 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 feature | Required capabilities |
|---|
| Pagination page count display | Count |
| Charts | All field support in Aggregation |
| Relations | In on primary/foreign keys |
| Select all for actions/delete | In and NotIn on primary key |
| Frontend filters, scopes, segments | Per-field operator support |
| Operator emulation | In on primary keys |
| Search emulation | Contains 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();
}
}
Search
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)