Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/forestadmin-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export { default as ForestAdminClientWithCache } from './forest-admin-client-wit
export { default as buildApplicationServices } from './build-application-services';
export { HttpOptions } from './utils/http-options';
export { default as ForestHttpApi } from './permissions/forest-http-api';
export { default as ServerUtils } from './utils/server';
// export is necessary for the agent-generator package
export { default as SchemaService, SchemaServiceOptions } from './schema';
export { default as ActivityLogsService, ActivityLogsOptions } from './activity-logs';
Expand Down
1 change: 1 addition & 0 deletions packages/workflow-executor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"dependencies": {
"@forestadmin/agent-client": "1.4.13",
"@forestadmin/forestadmin-client": "1.37.17",
"@langchain/core": "1.1.33",
"zod": "4.3.6"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { McpConfiguration, WorkflowPort } from '../ports/workflow-port';
import type { PendingStepExecution } from '../types/execution';
import type { CollectionRef } from '../types/record';
import type { StepHistory } from '../types/step-history';
import type { HttpOptions } from '@forestadmin/forestadmin-client';

import { ServerUtils } from '@forestadmin/forestadmin-client';

// TODO: finalize route paths with the team — these are placeholders
const ROUTES = {
pendingStepExecutions: '/liana/v1/workflow-step-executions/pending',
updateStepExecution: (runId: string) => `/liana/v1/workflow-step-executions/${runId}/complete`,
collectionRef: (collectionName: string) => `/liana/v1/collections/${collectionName}`,
mcpServerConfigs: '/liana/mcp-server-configs-with-details',
};

export default class ForestServerWorkflowPort implements WorkflowPort {
private readonly options: HttpOptions;

constructor(params: { envSecret: string; forestServerUrl: string }) {
this.options = { envSecret: params.envSecret, forestServerUrl: params.forestServerUrl };
}

async getPendingStepExecutions(): Promise<PendingStepExecution[]> {
return ServerUtils.query<PendingStepExecution[]>(
this.options,
'get',
ROUTES.pendingStepExecutions,
);
}

async updateStepExecution(runId: string, stepHistory: StepHistory): Promise<void> {
await ServerUtils.query(
this.options,
'post',
ROUTES.updateStepExecution(runId),
{},
stepHistory,
);
}

async getCollectionRef(collectionName: string): Promise<CollectionRef> {
return ServerUtils.query<CollectionRef>(
this.options,
'get',
ROUTES.collectionRef(collectionName),
);
}

async getMcpServerConfigs(): Promise<McpConfiguration[]> {
return ServerUtils.query<McpConfiguration[]>(this.options, 'get', ROUTES.mcpServerConfigs);
}
}
1 change: 1 addition & 0 deletions packages/workflow-executor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ export {
export { default as BaseStepExecutor } from './executors/base-step-executor';
export { default as ConditionStepExecutor } from './executors/condition-step-executor';
export { default as AgentClientAgentPort } from './adapters/agent-client-agent-port';
export { default as ForestServerWorkflowPort } from './adapters/forest-server-workflow-port';
2 changes: 1 addition & 1 deletion packages/workflow-executor/src/ports/workflow-port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type McpConfiguration = unknown;

export interface WorkflowPort {
getPendingStepExecutions(): Promise<PendingStepExecution[]>;
completeStepExecution(runId: string, stepHistory: StepHistory): Promise<void>;
updateStepExecution(runId: string, stepHistory: StepHistory): Promise<void>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3

getCollectionRef(collectionName: string): Promise<CollectionRef>;
getMcpServerConfigs(): Promise<McpConfiguration[]>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { PendingStepExecution } from '../../src/types/execution';
import type { CollectionRef } from '../../src/types/record';
import type { StepHistory } from '../../src/types/step-history';

import { ServerUtils } from '@forestadmin/forestadmin-client';

import ForestServerWorkflowPort from '../../src/adapters/forest-server-workflow-port';

jest.mock('@forestadmin/forestadmin-client', () => ({
ServerUtils: { query: jest.fn() },
}));

const mockQuery = ServerUtils.query as jest.Mock;

const options = { envSecret: 'env-secret-123', forestServerUrl: 'https://api.forestadmin.com' };

describe('ForestServerWorkflowPort', () => {
let port: ForestServerWorkflowPort;

beforeEach(() => {
jest.clearAllMocks();
port = new ForestServerWorkflowPort(options);
});

describe('getPendingStepExecutions', () => {
it('should call the pending step executions route', async () => {
const pending: PendingStepExecution[] = [];
mockQuery.mockResolvedValue(pending);

const result = await port.getPendingStepExecutions();

expect(mockQuery).toHaveBeenCalledWith(
options,
'get',
'/liana/v1/workflow-step-executions/pending',
);
expect(result).toBe(pending);
});
});

describe('updateStepExecution', () => {
it('should post step history to the complete route', async () => {
mockQuery.mockResolvedValue(undefined);
const stepHistory: StepHistory = {
type: 'condition',
stepId: 'step-1',
stepIndex: 0,
status: 'success',
selectedOption: 'optionA',
};

await port.updateStepExecution('run-42', stepHistory);

expect(mockQuery).toHaveBeenCalledWith(
options,
'post',
'/liana/v1/workflow-step-executions/run-42/complete',
{},
stepHistory,
);
});
});

describe('getCollectionRef', () => {
it('should fetch the collection ref by name', async () => {
const collectionRef: CollectionRef = {
collectionName: 'users',
collectionDisplayName: 'Users',
primaryKeyFields: ['id'],
fields: [],
actions: [],
};
mockQuery.mockResolvedValue(collectionRef);

const result = await port.getCollectionRef('users');

expect(mockQuery).toHaveBeenCalledWith(options, 'get', '/liana/v1/collections/users');
expect(result).toEqual(collectionRef);
});
});

describe('getMcpServerConfigs', () => {
it('should fetch mcp server configs', async () => {
const configs = [{ name: 'mcp-1' }];
mockQuery.mockResolvedValue(configs);

const result = await port.getMcpServerConfigs();

expect(mockQuery).toHaveBeenCalledWith(
options,
'get',
'/liana/mcp-server-configs-with-details',
);
expect(result).toEqual(configs);
});
});

describe('error propagation', () => {
it('should propagate errors from ServerUtils.query', async () => {
mockQuery.mockRejectedValue(new Error('Network error'));

await expect(port.getPendingStepExecutions()).rejects.toThrow('Network error');
});
});
});
Loading