Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bal 3330 #2949

Merged
merged 3 commits into from
Jan 8, 2025
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
3 changes: 2 additions & 1 deletion apps/kyb-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
"dependencies": {
"@ballerine/blocks": "0.2.30",
"@ballerine/common": "^0.9.63",
"@ballerine/workflow-browser-sdk": "0.6.82",
"@ballerine/ui": "0.5.59",
"@ballerine/workflow-browser-sdk": "0.6.82",
"@lukemorales/query-key-factory": "^1.0.3",
"@radix-ui/react-icons": "^1.3.0",
"@rjsf/core": "^5.9.0",
Expand All @@ -46,6 +46,7 @@
"i18next-http-backend": "^2.1.1",
"jmespath": "^0.16.0",
"json-logic-js": "^2.0.2",
"jsonata": "^2.0.6",
"ky": "^0.33.3",
"lodash": "^4.17.21",
"lucide-react": "^0.144.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export const CollectionFlowUI: FunctionComponent<ICollectionFlowUIProps> = ({
[handleEvent, sync, helpers],
);

console.log('context', context);

return (
<DynamicFormV2
fieldExtends={formElementsExtends}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { EVENT_PLUGIN_NAME, eventPlugin } from './plugins/event.plugin';
import { OCR_PLUGIN_NAME, ocrPlugin } from './plugins/ocr.plugin';
import { TRANSFORMER_PLUGIN_NAME, transformerPlugin } from './plugins/transformer.plugin';

export const pluginsRepository = {
[EVENT_PLUGIN_NAME]: eventPlugin,
[OCR_PLUGIN_NAME]: ocrPlugin,
[TRANSFORMER_PLUGIN_NAME]: transformerPlugin,
};

export const getPlugin = (pluginName: string) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { CollectionFlowContext } from '@/domains/collection-flow/types/flow-context.types';
import { describe, expect, it, vi } from 'vitest';
import { OCR_PLUGIN_NAME, ocrPlugin } from './ocr.plugin';

describe('ocrPlugin', () => {
it('should invoke fetch_company_information plugin and return updated context', async () => {
// Mock context and API
const mockContext = {} as CollectionFlowContext;
const mockUpdatedContext = {} as CollectionFlowContext;

const mockApi = {
invokePlugin: vi.fn().mockResolvedValue(undefined),
getContext: vi.fn().mockReturnValue(mockUpdatedContext),
};

// Execute plugin
const result = await ocrPlugin(mockContext, { api: mockApi as any });

// Verify plugin was invoked
expect(mockApi.invokePlugin).toHaveBeenCalledWith('fetch_company_information');
expect(mockApi.invokePlugin).toHaveBeenCalledTimes(1);

// Verify context was retrieved
expect(mockApi.getContext).toHaveBeenCalled();
expect(mockApi.getContext).toHaveBeenCalledTimes(1);

// Verify returned context matches
expect(result).toBe(mockUpdatedContext);
});

it('should throw error if fetch_company_information plugin fails', async () => {
// Mock context and API with error
const mockContext = {} as CollectionFlowContext;
const mockError = new Error('Plugin failed');

const mockApi = {
invokePlugin: vi.fn().mockRejectedValue(mockError),
getContext: vi.fn(),
};

// Verify plugin throws error
await expect(ocrPlugin(mockContext, { api: mockApi as any })).rejects.toThrow(mockError);

// Verify plugin was invoked
expect(mockApi.invokePlugin).toHaveBeenCalledWith('fetch_company_information');
expect(mockApi.invokePlugin).toHaveBeenCalledTimes(1);

// Verify context was not retrieved
expect(mockApi.getContext).not.toHaveBeenCalled();
});

it('should export correct plugin name constant', () => {
expect(OCR_PLUGIN_NAME).toBe('ocr');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { StateMachineAPI } from '@/components/organisms/DynamicUI/StateManager/hooks/useMachineLogic';
import { CollectionFlowContext } from '@/domains/collection-flow/types/flow-context.types';
import jsonata from 'jsonata';
import get from 'lodash/get';
import set from 'lodash/set';

export const TRANSFORMER_PLUGIN_NAME = 'transformer';

export interface ITransformerPluginParams {
expression: string;
input?: string;
output: string;
}

export const transformerPlugin = async (
context: CollectionFlowContext,
_: { api: StateMachineAPI },
params: ITransformerPluginParams,
) => {
const { expression, input, output } = params;

const inputData = input ? get(context, input) : context;

const jsonataExpression = jsonata(expression);
const expressionResult = await jsonataExpression.evaluate(inputData);

const updateResult = set(context, output, expressionResult);

return updateResult;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { CollectionFlowContext } from '@/domains/collection-flow/types/flow-context.types';
import jsonata from 'jsonata';
import get from 'lodash/get';
import set from 'lodash/set';
import { describe, expect, it, vi } from 'vitest';
import { TRANSFORMER_PLUGIN_NAME, transformerPlugin } from './transformer.plugin';

vi.mock('jsonata');
vi.mock('lodash/get');
vi.mock('lodash/set');

describe('transformerPlugin', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should transform data according to provided expression and paths', async () => {
// Mock context and params
const mockContext = {
sourceData: { name: 'Test Company' },
} as unknown as CollectionFlowContext;

const mockParams = {
expression: 'name',
input: 'sourceData',
output: 'targetData',
};

const mockExpressionResult = 'Test Company';

// Mock dependencies
const mockJsonataInstance = {
evaluate: vi.fn().mockResolvedValue(mockExpressionResult),
};

vi.mocked(jsonata).mockReturnValue(mockJsonataInstance as any);
vi.mocked(get).mockReturnValue({ name: 'Test Company' });
vi.mocked(set).mockReturnValue({ ...mockContext, targetData: mockExpressionResult });

// Execute plugin
const result = await transformerPlugin(mockContext, { api: {} as any }, mockParams);

// Verify jsonata expression was created and evaluated
expect(jsonata).toHaveBeenCalledWith(mockParams.expression);
expect(mockJsonataInstance.evaluate).toHaveBeenCalledWith({ name: 'Test Company' });

// Verify lodash get/set were called correctly
expect(get).toHaveBeenCalledWith(mockContext, mockParams.input);
expect(set).toHaveBeenCalledWith(mockContext, mockParams.output, mockExpressionResult);

// Verify result contains transformed data
expect(result).toEqual({
sourceData: { name: 'Test Company' },
targetData: 'Test Company',
});
});

it('should use full context when input path is not provided', async () => {
const mockContext = {
name: 'Test Company',
} as unknown as CollectionFlowContext;

const mockParams = {
expression: 'name',
output: 'targetData',
};

const mockExpressionResult = 'Test Company';

const mockJsonataInstance = {
evaluate: vi.fn().mockResolvedValue(mockExpressionResult),
};

vi.mocked(jsonata).mockReturnValue(mockJsonataInstance as any);
vi.mocked(set).mockReturnValue({ ...mockContext, targetData: mockExpressionResult });

const result = await transformerPlugin(mockContext, { api: {} as any }, mockParams);

expect(jsonata).toHaveBeenCalledWith(mockParams.expression);
expect(mockJsonataInstance.evaluate).toHaveBeenCalledWith(mockContext);
expect(get).not.toHaveBeenCalled();
expect(set).toHaveBeenCalledWith(mockContext, mockParams.output, mockExpressionResult);

expect(result).toEqual({
name: 'Test Company',
targetData: 'Test Company',
});
});

it('should handle jsonata evaluation errors', async () => {
const mockContext = {} as CollectionFlowContext;
const mockParams = {
expression: 'invalid[expression',
input: 'source',
output: 'target',
};

const mockError = new Error('Invalid expression');
const mockJsonataInstance = {
evaluate: vi.fn().mockRejectedValue(mockError),
};

vi.mocked(jsonata).mockReturnValue(mockJsonataInstance as any);
vi.mocked(get).mockReturnValue({});

await expect(transformerPlugin(mockContext, { api: {} as any }, mockParams)).rejects.toThrow(
mockError,
);
});

it('should export correct plugin name constant', () => {
expect(TRANSFORMER_PLUGIN_NAME).toBe('transformer');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ export const usePluginRunners = (plugins: IPlugin[] = []) => {
const getPluginRunner = useCallback(
(eventName: TElementEvent, element?: IFormEventElement<any, any>) => {
if (eventName && element) {
return runners.find(runner =>
return runners.filter(runner =>
runner.runOn?.some(runOn => runOn.type === eventName && runOn.elementId === element.id),
);
}

return runners.find(runner => runner.runOn?.some(runOn => runOn.type === eventName));
return runners.filter(runner => runner.runOn?.some(runOn => runOn.type === eventName));
},
[runners],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ describe('usePluginRunners', () => {
const { result } = renderHook(() => usePluginRunners(plugins));

expect(result.current.runners).toHaveLength(1);
expect(result.current.runners?.[0]?.name).toBe('test-plugin');
expect(typeof result.current.runners?.[0]?.run).toBe('function');
expect(result.current.runners[0]?.name).toBe('test-plugin');
expect(typeof result.current.runners[0]?.run).toBe('function');
expect(typeof result.current.getPluginRunner).toBe('function');
});

Expand All @@ -48,13 +48,13 @@ describe('usePluginRunners', () => {

const { result } = renderHook(() => usePluginRunners(plugins));

const runner = result.current.getPluginRunner('onChange', {
const runners = result.current.getPluginRunner('onChange', {
id: 'test-id',
element: {
id: 'test-id',
},
} as any);
expect(runner?.name).toBe('test-plugin');
expect(runners[0]?.name).toBe('test-plugin');
});

it('should find plugin runner by event name only', () => {
Expand All @@ -67,11 +67,11 @@ describe('usePluginRunners', () => {

const { result } = renderHook(() => usePluginRunners(plugins));

const runner = result.current.getPluginRunner('onSubmit');
expect(runner?.name).toBe('test-plugin');
const runners = result.current.getPluginRunner('onSubmit');
expect(runners[0]?.name).toBe('test-plugin');
});

it('should return undefined when no matching plugin runner found', () => {
it('should return empty array when no matching plugin runner found', () => {
const plugins = [
{
name: 'test-plugin',
Expand All @@ -81,8 +81,8 @@ describe('usePluginRunners', () => {

const { result } = renderHook(() => usePluginRunners(plugins));

const runner = result.current.getPluginRunner('onSubmit');
expect(runner).toBeUndefined();
const runners = result.current.getPluginRunner('onSubmit');
expect(runners).toHaveLength(0);
});

it('should debounce plugin execution', () => {
Expand All @@ -97,8 +97,8 @@ describe('usePluginRunners', () => {
const { result } = renderHook(() => usePluginRunners(plugins));

const context = { testData: 'test' };
result.current.runners?.[0]?.run?.(context);
result.current.runners?.[0]?.run?.(context);
result.current.runners[0]?.run?.(context);
result.current.runners[0]?.run?.(context);

expect(mockRunPlugin).not.toHaveBeenCalled();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@ export const usePluginsHandler = () => {

const handleEvent = useCallback(
(eventName: TElementEvent, element?: IFormEventElement<any, any>) => {
console.log('handleEvent', eventName, element);
const runner = getPluginRunner(eventName, element);
const runners = getPluginRunner(eventName, element);
const context = stateApi.getContext();

if (!runner) return;
if (!runners?.length) return;

console.log(`Found plugin ${runner.name} for event ${eventName}`);
console.log(`Found plugins ${JSON.stringify(runners)} for event ${eventName}`);

if (!checkIfPluginCanRun(runner.runOn, eventName, context)) {
console.log(`Plugin ${runner.name} cannot run for event ${eventName}`);
runners.forEach(runner => {
if (!checkIfPluginCanRun(runner.runOn, eventName, context)) {
console.log(`Plugin ${runner.name} cannot run for event ${eventName}`);

return;
}
return;
}

console.log(`Plugin ${runner.name} can run for event ${eventName}`);

runner.run(context);
console.log(`Plugin ${runner.name} can run for event ${eventName}`);
runner.run(context);
});
},
[getPluginRunner, stateApi],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ describe('usePluginsHandler', () => {
mockGetContext.mockReturnValue({ someContext: 'value' });
});

it('should not run plugin when no matching runner found', () => {
it('should not run plugin when no matching runners found', () => {
const { result } = renderHook(() => usePluginsHandler());
mockGetPluginRunner.mockReturnValue(undefined);
mockGetPluginRunner.mockReturnValue([]);

result.current.handleEvent('onChange', { id: 'test' } as IFormEventElement<any, any>);

Expand All @@ -67,7 +67,7 @@ describe('usePluginsHandler', () => {
};

const { result } = renderHook(() => usePluginsHandler());
mockGetPluginRunner.mockReturnValue(mockRunner);
mockGetPluginRunner.mockReturnValue([mockRunner]);
mockedCheckIfPluginCanRun.mockReturnValue(false);

result.current.handleEvent('onChange', { id: 'test' } as IFormEventElement<any, any>);
Expand All @@ -84,7 +84,7 @@ describe('usePluginsHandler', () => {
const context = { someContext: 'value' };

const { result } = renderHook(() => usePluginsHandler());
mockGetPluginRunner.mockReturnValue(mockRunner);
mockGetPluginRunner.mockReturnValue([mockRunner]);
mockedCheckIfPluginCanRun.mockReturnValue(true);

result.current.handleEvent('onChange', { id: 'test' } as IFormEventElement<any, any>);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './useElement';
export * from './useElementId';
export * from './useField';
export * from './useRules';
export * from './useSubmit';
export * from './useValueDestination';
Loading
Loading