Complete API reference for @forestadmin/agent (Node.js/TypeScript).
Agent Class
createAgent(options)
Create and configure a Forest agent.
import { createAgent } from '@forestadmin/agent';
const agent = createAgent(options: AgentOptions): Agent;
Parameters:
| Option | Type | Required | Description |
|---|
authSecret | string | Yes | Your FOREST_AUTH_SECRET |
envSecret | string | Yes | Your FOREST_ENV_SECRET |
isProduction | boolean | No | Enable production mode |
logger | function | No | Custom logger function |
loggerLevel | string | No | Log level: ‘Debug’, ‘Info’, ‘Warn’, ‘Error’ |
prefix | string | No | API prefix (default: ‘/forest’) |
schemaPath | string | No | Path to .forestadmin-schema.json |
Example:
const agent = createAgent({
authSecret: process.env.FOREST_AUTH_SECRET,
envSecret: process.env.FOREST_ENV_SECRET,
isProduction: process.env.NODE_ENV === 'production',
});
agent.addDataSource(factory, options?)
Add a datasource to the agent.
agent.addDataSource(
factory: DataSourceFactory,
options?: DataSourceOptions
): Agent;
Parameters:
| Option | Type | Description |
|---|
factory | DataSourceFactory | Datasource factory function |
options.include | string[] | Collections to include |
options.exclude | string[] | Collections to exclude |
options.rename | Record<string, string> | Rename collections |
Example:
import { createSqlDataSource } from '@forestadmin/datasource-sql';
agent.addDataSource(
createSqlDataSource(process.env.DATABASE_URL),
{ exclude: ['internal_logs'] }
);
agent.customizeCollection(name, callback)
Customize a specific collection with the provided callback.
agent.customizeCollection(
name: string,
callback: (collection: CollectionCustomizer) => void
): Agent;
Example:
agent.customizeCollection('users', collection => {
collection.addAction('Send email', {
scope: 'Single',
execute: async (context, resultBuilder) => {
// Action logic
return resultBuilder.success('Email sent!');
},
});
});
agent.addChart(name, definition)
Create a datasource-level API chart.
agent.addChart(
name: string,
definition: DataSourceChartDefinition
): Agent;
Example:
agent.addChart('overview', (context, resultBuilder) => {
return resultBuilder.distribution({
'Active': 150,
'Inactive': 50,
});
});
agent.removeCollection(…names)
Remove collections from the exported schema (they remain usable within the agent).
agent.removeCollection(...names: string[]): Agent;
Example:
agent.removeCollection('internalLogs', 'debugData');
agent.use(plugin, options?)
Load a plugin across all collections.
agent.use<Options>(
plugin: Plugin<Options>,
options?: Options
): Agent;
Example:
import advancedExportPlugin from '@forestadmin/plugin-export-advanced';
agent.use(advancedExportPlugin, { format: 'xlsx' });
agent.start()
Start the agent and connect to Forest servers.
await agent.start(): Promise<void>;
Example:
await agent.start();
console.log('Agent started successfully');
agent.stop()
Stop the agent and close all connections.
await agent.stop(): Promise<void>;
agent.restart()
Reconstruct routing and remount routes at runtime. Called when customizations are refreshed externally (e.g. in cloud environments).
await agent.restart(): Promise<void>;
agent.addAi(provider)
Route Forest’s AI features (natural language search, actions, etc.) through your own agent so your data never leaves your infrastructure. Pass a provider built with createAiProvider (from @forestadmin/ai-proxy); openai and anthropic are supported. See Self-hosted AI for the full guide.
agent.addAi(provider: AiProviderDefinition): Agent;
Can only be called once, calling it a second time throws an error.
Example:
import { createAgent } from '@forestadmin/agent';
import { createAiProvider } from '@forestadmin/ai-proxy';
const agent = createAgent({
authSecret: process.env.FOREST_AUTH_SECRET,
envSecret: process.env.FOREST_ENV_SECRET,
})
.addAi(
createAiProvider({
name: 'my-assistant',
provider: 'anthropic',
apiKey: process.env.ANTHROPIC_API_KEY,
model: 'claude-sonnet-4-5',
}),
);
agent.mountAiMcpServer(options?)
Enable a Model Context Protocol (MCP) server on the agent, allowing AI assistants to interact with your data through standardized tools.
agent.mountAiMcpServer(options?: {
enabledTools?: ToolName[];
}): Agent;
Parameters:
| Option | Type | Description |
|---|
enabledTools | ToolName[] | Restrict which MCP tools are exposed. Defaults to all tools. |
Available tool names: 'describeCollection', 'list', 'listRelated', 'create', 'update', 'delete', 'associate', 'dissociate', 'getActionForm', 'executeAction'
Example:
agent.mountAiMcpServer({
enabledTools: ['describeCollection', 'list', 'listRelated'],
});
The MCP server exposes HTTP endpoints for OAuth and protocol communication:
| Endpoint | Purpose |
|---|
POST /mcp | Main MCP protocol endpoint (Bearer token required) |
POST /oauth/authorize | OAuth authorization |
POST /oauth/token | Token exchange |
GET /.well-known/oauth-protected-resource/mcp | OAuth discovery |
You can also configure enabled tools via the FOREST_MCP_ENABLED_TOOLS environment variable (comma-separated tool names), or set the server port with MCP_SERVER_PORT (default: 3931).
agent.updateTypesOnFileSystem(typingsPath, typingsMaxDepth)
Update the TypeScript typings file generated from your datasources.
await agent.updateTypesOnFileSystem(
typingsPath: string,
typingsMaxDepth: number
): Promise<void>;
Example:
await agent.updateTypesOnFileSystem('./typings.ts', 5);
Collection Customizer
Methods available when customizing a collection through agent.customizeCollection().
Actions
collection.addAction(name, definition)
Add an action to the collection.
collection.addAction(
name: string,
definition: ActionDefinition
): CollectionCustomizer;
Definition Properties:
| Property | Type | Description |
|---|
scope | ’Single’ | ‘Bulk’ | ‘Global’ | Action scope |
execute | function | Action execution handler |
form | FormElement[] | Dynamic form configuration |
description | string | Action description |
generateFile | boolean | Whether action returns a file |
submitButtonLabel | string | Custom button text |
Execute Function:
execute: (
context: ActionContext,
resultBuilder: ResultBuilder
) => Promise<ActionResult>
ActionContext Properties:
context.collection - Collection instance
context.filter - Filter for selected records
context.caller - User who triggered the action
context.formValues - Form values submitted
ActionContext Methods:
context.getRecords(fields) - Get multiple records (Bulk/Single scope)
context.getRecordIds() - Get IDs of selected records
context.getCompositeRecordIds() - Get composite IDs of selected records
context.hasFieldChanged(fieldName) - Check if form field changed
context.getRecord(fields) - Get single record (Single scope only)
context.getRecordId() - Get single record ID (Single scope only)
context.getCompositeRecordId() - Get composite ID (Single scope only)
context.getField(fieldName) - Get single field value (Single scope only)
ResultBuilder Methods:
resultBuilder.success(message?, options?) - Success response
options.html - Custom HTML to display
options.invalidated - Array of collection names to refresh
resultBuilder.error(message?, options?) - Error response
options.html - Custom HTML to display
resultBuilder.webhook(url, method, headers, body) - Trigger webhook
resultBuilder.file(stream, filename, mimeType) - Return file download
resultBuilder.redirectTo(path) - Redirect to URL
resultBuilder.setHeader(name, value) - Add HTTP header to response
Example - Simple Action:
collection.addAction('Mark as verified', {
scope: 'Single',
execute: async (context, resultBuilder) => {
const user = await context.getRecord(['id']);
await updateUser(user.id, { verified: true });
return resultBuilder.success('User marked as verified');
},
});
Example - Action with Form:
collection.addAction('Send notification', {
scope: 'Bulk',
form: [
{
label: 'Message',
type: 'String',
isRequired: true,
},
{
label: 'Channel',
type: 'Enum',
enumValues: ['email', 'sms', 'push'],
isRequired: true,
},
],
execute: async (context, resultBuilder) => {
const { message, channel } = context.formValues;
const users = await context.getRecords(['email']);
for (const user of users) {
await sendNotification(user.email, message, channel);
}
return resultBuilder.success(`Sent ${channel} to ${users.length} users`);
},
});
Example - File Generation:
collection.addAction('Export to PDF', {
scope: 'Bulk',
generateFile: true,
execute: async (context, resultBuilder) => {
const records = await context.getRecords(['name', 'email']);
const pdfStream = await generatePDF(records);
return resultBuilder.file(pdfStream, 'export.pdf', 'application/pdf');
},
});
Fields
collection.addField(name, definition)
Add a computed field to the collection.
collection.addField(
name: string,
definition: ComputedDefinition
): CollectionCustomizer;
Definition Properties:
| Property | Type | Required | Description |
|---|
columnType | ColumnType | Yes | Field data type |
dependencies | string[] | Yes | Fields needed for computation |
getValues | function | Yes | Value computation function |
defaultValue | any | No | Default value |
enumValues | string[] | No | Enum options (if type is Enum) |
Column Types:
'String' - Text
'Number' - Numeric value
'Boolean' - True/false
'Date' - Date with time
'Dateonly' - Date without time
'Time' - Time only
'Enum' - Enumeration
'Json' - JSON object
'Uuid' - UUID
'Point' - Geographic point
'File' - File reference
Example - Simple Computed Field:
collection.addField('fullName', {
columnType: 'String',
dependencies: ['firstName', 'lastName'],
getValues: (records) =>
records.map(r => `${r.firstName} ${r.lastName}`),
});
Example - Async Computed Field:
collection.addField('revenueThisYear', {
columnType: 'Number',
dependencies: ['id'],
getValues: async (records) => {
const ids = records.map(r => r.id);
const revenues = await fetchRevenues(ids);
return revenues;
},
});
collection.importField(name, options)
Import a field from a related collection.
collection.importField(
name: string,
options: { path: string; readonly?: boolean }
): CollectionCustomizer;
Example:
// Import author's name into books collection
collection.importField('authorName', {
path: 'author:fullName',
readonly: true,
});
collection.renameField(currentName, newName)
Rename a field in the exported schema.
collection.renameField(
currentName: string,
newName: string
): CollectionCustomizer;
Example:
collection.renameField('created_at', 'createdAt');
collection.removeField(…names)
Remove fields from the exported schema (they remain usable within the agent).
collection.removeField(...names: string[]): CollectionCustomizer;
Example:
collection.removeField('password', 'internalNotes', 'debugData');
collection.addFieldValidation(name, operator, value?)
Add a validation rule to a field.
collection.addFieldValidation(
name: string,
operator: Operator,
value?: any
): CollectionCustomizer;
Operators:
'Present', 'LongerThan', 'ShorterThan', 'Contains', 'Like', 'Match', 'GreaterThan', 'LessThan', 'Before', 'After'
Example:
collection
.addFieldValidation('email', 'Present')
.addFieldValidation('email', 'Match', /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/)
.addFieldValidation('age', 'GreaterThan', 18)
.addFieldValidation('username', 'LongerThan', 3);
collection.setFieldNullable(name)
Mark a field as optional (nullable).
collection.setFieldNullable(name: string): CollectionCustomizer;
Your database might still refuse empty values if it requires one.
Example:
collection.setFieldNullable('middleName');
collection.replaceFieldWriting(name, definition)
Replace the write behavior of a field.
collection.replaceFieldWriting(
name: string,
definition: WriteDefinition
): CollectionCustomizer;
Example:
// Write fullName as firstName + lastName
collection.replaceFieldWriting('fullName', fullName => {
const [firstName, lastName] = fullName.split(' ');
return { firstName, lastName };
});
collection.replaceFieldBinaryMode(name, mode)
Choose how binary data should be transported to the GUI.
collection.replaceFieldBinaryMode(
name: string,
mode: 'datauri' | 'hex'
): CollectionCustomizer;
Modes:
'datauri' - Best for file uploads, uses FilePicker widget
'hex' - Best for short binary data like UUIDs
Example:
collection.replaceFieldBinaryMode('avatar', 'datauri');
collection.replaceFieldBinaryMode('uuid', 'hex');
Relationships
collection.addManyToOneRelation(name, foreignCollection, options)
Add a many-to-one relationship.
collection.addManyToOneRelation(
name: string,
foreignCollection: string,
options: {
foreignKey: string;
foreignKeyTarget?: string;
}
): CollectionCustomizer;
Example:
// books.authorId → persons.id
books.addManyToOneRelation('author', 'persons', {
foreignKey: 'authorId',
});
collection.addOneToManyRelation(name, foreignCollection, options)
Add a one-to-many relationship.
collection.addOneToManyRelation(
name: string,
foreignCollection: string,
options: {
originKey: string;
originKeyTarget?: string;
}
): CollectionCustomizer;
Example:
// persons.id ← books.authorId
persons.addOneToManyRelation('writtenBooks', 'books', {
originKey: 'authorId',
});
collection.addOneToOneRelation(name, foreignCollection, options)
Add a one-to-one relationship.
collection.addOneToOneRelation(
name: string,
foreignCollection: string,
options: {
originKey: string;
originKeyTarget?: string;
}
): CollectionCustomizer;
Example:
// persons.id ← profiles.personId (unique)
persons.addOneToOneRelation('profile', 'profiles', {
originKey: 'personId',
});
collection.addManyToManyRelation(name, foreignCollection, throughCollection, options)
Add a many-to-many relationship.
collection.addManyToManyRelation(
name: string,
foreignCollection: string,
throughCollection: string,
options: {
originKey: string;
foreignKey: string;
originKeyTarget?: string;
foreignKeyTarget?: string;
}
): CollectionCustomizer;
Example:
// students ↔ student_courses ↔ courses
students.addManyToManyRelation('enrolledCourses', 'courses', 'student_courses', {
originKey: 'studentId',
foreignKey: 'courseId',
});
collection.addExternalRelation(name, definition)
Add a virtual collection into the related data of a record.
collection.addExternalRelation(
name: string,
definition: ExternalRelationDefinition
): CollectionCustomizer;
Example:
collection.addExternalRelation('relatedProducts', {
schema: { id: 'Number', name: 'String', price: 'Number' },
listRecords: async ({ id }) => {
return await fetchRelatedProducts(id);
},
});
Segments
collection.addSegment(name, definition)
Add a segment (saved filter) to the collection.
collection.addSegment(
name: string,
definition: SegmentDefinition
): CollectionCustomizer;
Example - Static Segment:
collection.addSegment('Premium users', {
field: 'plan',
operator: 'Equal',
value: 'premium',
});
Example - Dynamic Segment:
collection.addSegment('Active this month', async (context) => {
const startOfMonth = new Date();
startOfMonth.setDate(1);
return {
field: 'lastActiveAt',
operator: 'After',
value: startOfMonth,
};
});
Hooks
collection.addHook(position, type, handler)
Add a hook to execute code before or after operations.
collection.addHook(
position: 'Before' | 'After',
type: HookType,
handler: HookHandler
): CollectionCustomizer;
Hook Types:
'List', 'Create', 'Update', 'Delete', 'Aggregate'
Example - Before Hook:
collection.addHook('Before', 'Create', async (context) => {
// Validate data before creation
if (!context.data.email) {
throw new Error('Email is required');
}
});
Example - After Hook:
collection.addHook('After', 'Update', async (context) => {
// Send notification after update
const records = await context.collection.list(context.filter, ['email']);
for (const record of records) {
await sendUpdateNotification(record.email);
}
});
Search
collection.replaceSearch(definition)
Replace the default search behavior.
collection.replaceSearch(
definition: SearchDefinition
): CollectionCustomizer;
Example:
collection.replaceSearch(async (searchString) => {
// Search in multiple fields
return {
aggregator: 'Or',
conditions: [
{ field: 'firstName', operator: 'Contains', value: searchString },
{ field: 'lastName', operator: 'Contains', value: searchString },
{ field: 'email', operator: 'Contains', value: searchString },
],
};
});
collection.disableSearch()
Disable search functionality on the collection.
collection.disableSearch(): CollectionCustomizer;
Sorting
collection.emulateFieldSorting(name)
Enable in-memory sorting on a field.
collection.emulateFieldSorting(name: string): CollectionCustomizer;
Example:
collection.emulateFieldSorting('fullName');
collection.replaceFieldSorting(name, equivalentSort)
Replace sorting implementation for a field.
collection.replaceFieldSorting(
name: string,
equivalentSort: SortClause[]
): CollectionCustomizer;
Example:
collection.replaceFieldSorting('fullName', [
{ field: 'lastName', ascending: true },
{ field: 'firstName', ascending: true },
]);
collection.disableFieldSorting(name)
Disable sorting on a specific field.
collection.disableFieldSorting(name: string): CollectionCustomizer;
Filtering
collection.emulateFieldFiltering(name)
Enable in-memory filtering on all operators for a field.
collection.emulateFieldFiltering(name: string): CollectionCustomizer;
collection.emulateFieldOperator(name, operator)
Enable in-memory filtering for a specific operator on a field.
collection.emulateFieldOperator(
name: string,
operator: Operator
): CollectionCustomizer;
Example:
collection.emulateFieldOperator('fullName', 'Contains');
collection.replaceFieldOperator(name, operator, replacer)
Replace the implementation of a filter operator.
collection.replaceFieldOperator(
name: string,
operator: Operator,
replacer: OperatorDefinition
): CollectionCustomizer;
Example:
collection.replaceFieldOperator('fullName', 'Contains', (value) => {
return {
aggregator: 'Or',
conditions: [
{ field: 'firstName', operator: 'Contains', value },
{ field: 'lastName', operator: 'Contains', value },
],
};
});
Charts
collection.addChart(name, definition)
Add a chart to the collection.
collection.addChart(
name: string,
definition: ChartDefinition
): CollectionCustomizer;
Example - Value Chart:
collection.addChart('totalRevenue', async (context, resultBuilder) => {
const total = await calculateTotalRevenue();
return resultBuilder.value(total);
});
Example - Distribution Chart:
collection.addChart('usersByPlan', async (context, resultBuilder) => {
const distribution = await getUserDistributionByPlan();
return resultBuilder.distribution(distribution);
});
Example - Time-based Chart:
collection.addChart('signupsOverTime', async (context, resultBuilder) => {
const data = await getSignupsOverTime(context.timezone);
return resultBuilder.timeBased(data);
});
Collection Overrides
collection.overrideCreate(handler)
Replace the default create operation.
collection.overrideCreate(
handler: CreateOverrideHandler
): CollectionCustomizer;
Example:
collection.overrideCreate(async (context) => {
const { data } = context;
// Custom creation logic
const record = await customCreateAPI(data);
return [record];
});
collection.overrideUpdate(handler)
Replace the default update operation.
collection.overrideUpdate(
handler: UpdateOverrideHandler
): CollectionCustomizer;
Example:
collection.overrideUpdate(async (context) => {
const { filter, patch } = context;
// Custom update logic
await customUpdateAPI(filter, patch);
});
collection.overrideDelete(handler)
Replace the default delete operation.
collection.overrideDelete(
handler: DeleteOverrideHandler
): CollectionCustomizer;
Example:
collection.overrideDelete(async (context) => {
const { filter } = context;
// Custom deletion logic (e.g., soft delete)
await customSoftDeleteAPI(filter);
});
Other Methods
collection.disableCount()
Disable count in list view pagination for improved performance.
collection.disableCount(): CollectionCustomizer;
collection.use(plugin, options?)
Load a plugin on a specific collection.
collection.use(
plugin: Plugin,
options?: any
): CollectionCustomizer;
Example:
import { createFileField } from '@forestadmin/plugin-s3';
collection.use(createFileField, {
fieldname: 'avatar',
bucket: 'my-bucket',
});
Chart Result Builders
When creating charts with collection.addChart() or agent.addChart(), the result builder provides methods to format chart data.
resultBuilder.value(value, previousValue?)
Create a Value chart (single number).
collection.addChart('totalRevenue', async (context, resultBuilder) => {
const total = await calculateRevenue();
const previous = await calculateRevenue(lastMonth);
return resultBuilder.value(total, previous);
});
resultBuilder.distribution(obj)
Create a Distribution/Pie chart.
collection.addChart('usersByPlan', async (context, resultBuilder) => {
return resultBuilder.distribution({
'Free': 1000,
'Pro': 500,
'Enterprise': 50,
});
});
resultBuilder.timeBased(timeRange, values)
Create a Time-based/Line chart.
collection.addChart('signupsOverTime', async (context, resultBuilder) => {
return resultBuilder.timeBased('Day', [
{ date: new Date('2024-01-01'), value: 10 },
{ date: new Date('2024-01-02'), value: 15 },
{ date: new Date('2024-01-03'), value: null }, // Missing data
]);
});
Time Ranges: 'Day', 'Week', 'Month', 'Quarter', 'Year'
resultBuilder.multipleTimeBased(timeRange, dates, lines)
Create a Multi-line Time-based chart.
collection.addChart('comparison', async (context, resultBuilder) => {
const dates = [new Date('2024-01-01'), new Date('2024-01-02'), new Date('2024-01-03')];
return resultBuilder.multipleTimeBased('Day', dates, [
{ label: 'Sales', values: [100, 150, 200] },
{ label: 'Returns', values: [10, 15, null] },
]);
});
resultBuilder.percentage(value)
Create a Percentage chart.
collection.addChart('completionRate', async (context, resultBuilder) => {
const rate = (completed / total) * 100;
return resultBuilder.percentage(rate);
});
resultBuilder.objective(value, objective)
Create an Objective chart (progress toward goal).
collection.addChart('salesGoal', async (context, resultBuilder) => {
const current = await getCurrentSales();
const target = 100000;
return resultBuilder.objective(current, target);
});
resultBuilder.leaderboard(obj)
Create a Leaderboard chart (sorted distribution).
collection.addChart('topSellers', async (context, resultBuilder) => {
return resultBuilder.leaderboard({
'John': 5000,
'Jane': 7500,
'Bob': 3000,
});
// Automatically sorted: Jane (7500), John (5000), Bob (3000)
});
resultBuilder.smart(data)
Create a Smart chart (custom format).
collection.addChart('custom', async (context, resultBuilder) => {
return resultBuilder.smart({
// Custom chart data structure
type: 'custom',
data: [/* your data */],
});
});
Action forms support various field types with different widgets.
Basic Field Types
collection.addAction('Example', {
scope: 'Single',
form: [
{
label: 'User Name',
type: 'String',
isRequired: true,
description: 'Enter the user name',
},
{
label: 'Age',
type: 'Number',
isRequired: false,
defaultValue: 18,
},
{
label: 'Is Active',
type: 'Boolean',
defaultValue: true,
},
{
label: 'Birth Date',
type: 'Date',
},
{
label: 'Metadata',
type: 'Json',
},
],
execute: async (context, resultBuilder) => {
const { userName, age, isActive, birthDate, metadata } = context.formValues;
// ... action logic
return resultBuilder.success();
},
});
Available Types:
'String' - Text input
'Number' - Numeric input
'Boolean' - Checkbox
'Date' - Date picker
'Dateonly' - Date without time
'Time' - Time picker
'Enum' - Single selection
'EnumList' - Multiple selection
'File' - File upload
'FileList' - Multiple file upload
'Json' - JSON editor
'Collection' - Record picker
'NumberList' - Array of numbers
'StringList' - Array of strings
Enum Fields
{
label: 'Status',
type: 'Enum',
enumValues: ['pending', 'approved', 'rejected'],
isRequired: true,
}
Collection Fields
Pick a record from another collection:
{
label: 'Assign to User',
type: 'Collection',
collectionName: 'users',
}
{
label: 'Country',
type: 'String',
widget: 'Dropdown',
options: [
{ label: 'United States', value: 'US' },
{ label: 'United Kingdom', value: 'UK' },
{ label: 'France', value: 'FR' },
],
placeholder: 'Select a country',
search: 'static', // or 'disabled'
}
Dynamic Dropdown
{
label: 'Product',
type: 'String',
widget: 'Dropdown',
search: 'dynamic',
options: async (context, searchValue) => {
const products = await searchProducts(searchValue);
return products.map(p => ({
label: p.name,
value: p.id,
}));
},
}
Conditional Fields
Show/hide fields based on other field values:
form: [
{
label: 'Notification Type',
type: 'Enum',
enumValues: ['email', 'sms', 'push'],
},
{
label: 'Email Address',
type: 'String',
if: (context) => context.formValues.notificationType === 'email',
},
{
label: 'Phone Number',
type: 'String',
if: (context) => context.formValues.notificationType === 'sms',
},
]
Dynamic Field Values
{
label: 'Department',
type: 'Enum',
enumValues: async (context) => {
// Fetch departments based on selected company
const companyId = context.formValues.companyId;
return await getDepartments(companyId);
},
}
Read-Only Fields
{
label: 'Created At',
type: 'Date',
isReadOnly: true,
value: async (context) => {
const record = await context.getRecord(['createdAt']);
return record.createdAt;
},
}
ConditionTree Utilities
Build complex filter conditions programmatically.
ConditionTreeLeaf
Simple condition on a single field:
import { ConditionTreeLeaf } from '@forestadmin/agent';
const condition = new ConditionTreeLeaf('status', 'Equal', 'active');
// Equivalent to: { field: 'status', operator: 'Equal', value: 'active' }
ConditionTreeBranch
Combine multiple conditions with AND/OR:
import { ConditionTreeBranch, ConditionTreeLeaf } from '@forestadmin/agent';
const condition = new ConditionTreeBranch('And', [
new ConditionTreeLeaf('status', 'Equal', 'active'),
new ConditionTreeLeaf('age', 'GreaterThan', 18),
]);
ConditionTree Factory
import { ConditionTreeFactory } from '@forestadmin/agent';
// From plain object
const condition = ConditionTreeFactory.fromPlainObject({
field: 'email',
operator: 'Contains',
value: '@example.com',
});
// Combine conditions
const combined = ConditionTreeFactory.intersect([
condition1,
condition2,
]);
// Union (OR)
const union = ConditionTreeFactory.union([
condition1,
condition2,
]);
Available Operators
Comparison:
'Equal', 'NotEqual'
'GreaterThan', 'LessThan'
'In', 'NotIn'
'Present', 'Blank'
String:
'Contains', 'NotContains'
'StartsWith', 'EndsWith'
'Like', 'ILike' (case-insensitive)
Date:
'Before', 'After'
'Today', 'Yesterday', 'PreviousWeek', 'PreviousMonth', 'PreviousQuarter', 'PreviousYear'
'Past', 'Future'
Array:
'IncludesAll', 'IncludesNone'
Caller
User information available in all contexts:
interface Caller {
id: number;
email: string;
firstName: string;
lastName: string;
team: string;
role: string;
tags: Record<string, string>;
timezone: string;
}
Filter
import { Filter, ConditionTreeLeaf } from '@forestadmin/agent';
const filter = new Filter({
conditionTree: new ConditionTreeLeaf('status', 'Equal', 'active'),
search: 'john',
searchExtended: false,
segment: 'premium-users',
});
Projection
Specify which fields to retrieve:
import { Projection } from '@forestadmin/agent';
const projection = new Projection('id', 'name', 'email', 'company:name');
Plugin: Flattener
@forestadmin/plugin-flattener flattens nested data structures (composite columns, relations, JSON columns) into individual top-level fields.
flattenColumn(dataSource, collection, options)
Decompose a column with a composite type into individual fields.
import { flattenColumn } from '@forestadmin/plugin-flattener';
agent.customizeCollection('orders', async (collection) => {
await collection.use(flattenColumn, {
columnName: 'address',
include: ['street', 'city', 'zipCode'],
readonly: false,
});
});
Options:
| Option | Type | Required | Description |
|---|
columnName | string | Yes | Column to flatten |
include | string[] | No | Fields to import (defaults to all) |
exclude | string[] | No | Fields to skip |
level | number | No | Maximum nesting depth |
readonly | boolean | No | Whether imported fields are read-only |
columnType | object | No | Custom type mapping for nested fields |
flattenRelation(dataSource, collection, options)
Import fields from a relation directly into the collection.
import { flattenRelation } from '@forestadmin/plugin-flattener';
agent.customizeCollection('books', async (collection) => {
await collection.use(flattenRelation, {
relationName: 'author',
include: ['firstName', 'lastName', 'email'],
readonly: true,
});
});
Options:
| Option | Type | Required | Description |
|---|
relationName | string | Yes | Relation to flatten |
include | string[] | No | Fields to import |
exclude | string[] | No | Fields to skip |
readonly | boolean | No | Whether imported fields are read-only |
flattenJsonColumn(dataSource, collection, options)
Expand a JSON column into individual typed fields.
import { flattenJsonColumn } from '@forestadmin/plugin-flattener';
agent.customizeCollection('products', (collection) => {
collection.use(flattenJsonColumn, {
columnName: 'metadata',
columnType: {
weight: 'Number',
dimensions: { width: 'Number', height: 'Number' },
tags: ['String'],
},
readonly: false,
keepOriginalColumn: false,
});
});
Options:
| Option | Type | Required | Description |
|---|
columnName | string | Yes | JSON column to flatten |
columnType | object | Yes | Type definition for nested fields |
level | number | No | Maximum nesting depth |
readonly | boolean | No | Whether flattened fields are read-only |
keepOriginalColumn | boolean | No | Whether to preserve the original JSON column |
Package: Forest Cloud
@forestadmin/forest-cloud is a dev-only package that provides CLI tooling for cloud-hosted customization projects.
Installation
npm install @forestadmin/forest-cloud --save-dev
CLI Commands
bootstrap
Initialize a cloud customization project. Authenticates with Forest, creates a cloud-customizer directory, configures credentials, and generates type definitions.
npx forest-cloud bootstrap --env-secret YOUR_FOREST_ENV_SECRET
update-typings
Regenerate TypeScript type definitions based on your current database structure and customization code.
npx forest-cloud update-typings
login
Refresh your Forest authentication token.
Exported Types
import type { Agent, SqlConnectionParams, MongoConnectionParams } from '@forestadmin/forest-cloud';
| Type | Description |
|---|
Agent | Re-export of the Agent class from @forestadmin/agent |
SqlConnectionParams | Connection options for SQL datasources |
MongoConnectionParams | Connection parameters for MongoDB datasources |
Package: Agent Testing
@forestadmin/agent-testing provides utilities to test agent customizations locally without connecting to Forest servers.
createForestServerSandbox(port)
Start a local sandbox that mimics Forest servers.
import { createForestServerSandbox } from '@forestadmin/agent-testing';
const sandbox = await createForestServerSandbox(3001);
// ... run your tests
await sandbox.close();
createAgentTestClient(options)
Connect a test client to a running agent to simulate frontend requests.
import { createAgentTestClient } from '@forestadmin/agent-testing';
const client = await createAgentTestClient({
agentForestEnvSecret: process.env.FOREST_ENV_SECRET,
agentForestAuthSecret: process.env.FOREST_AUTH_SECRET,
agentUrl: 'http://localhost:3000',
serverUrl: 'http://localhost:3001',
agentSchemaPath: '.forestadmin-schema.json',
});
Parameters:
| Option | Type | Description |
|---|
agentForestEnvSecret | string | Environment secret |
agentForestAuthSecret | string | Auth secret |
agentUrl | string | URL of your running agent |
serverUrl | string | URL of the sandbox server |
agentSchemaPath | string | Path to .forestadmin-schema.json |
Typical test workflow
import { createForestServerSandbox, createAgentTestClient } from '@forestadmin/agent-testing';
// 1. Start sandbox
const sandbox = await createForestServerSandbox(3001);
// 2. Start your agent (pointing at sandbox)
// FOREST_SERVER_URL=http://localhost:3001 node index.js
// 3. Connect test client
const client = await createAgentTestClient({
agentForestEnvSecret: 'test-env-secret',
agentForestAuthSecret: 'test-auth-secret',
agentUrl: 'http://localhost:3000',
serverUrl: 'http://localhost:3001',
agentSchemaPath: '.forestadmin-schema.json',
});
// 4. Assert
const users = await client.collection('users').list();
expect(users).toHaveLength(3);
// 5. Cleanup
await sandbox.close();