mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
refactor(Structured Output Parser Node): Support schema via expression (#16671)
This commit is contained in:
120
packages/@n8n/nodes-langchain/utils/descriptions.test.ts
Normal file
120
packages/@n8n/nodes-langchain/utils/descriptions.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { buildInputSchemaField } from './descriptions';
|
||||
|
||||
describe('buildInputSchemaField', () => {
|
||||
it('should create input schema field with noDataExpression set to false', () => {
|
||||
const result = buildInputSchemaField();
|
||||
|
||||
expect(result.noDataExpression).toBe(false);
|
||||
expect(result.displayName).toBe('Input Schema');
|
||||
expect(result.name).toBe('inputSchema');
|
||||
expect(result.type).toBe('json');
|
||||
});
|
||||
|
||||
it('should include typeOptions with rows set to 10', () => {
|
||||
const result = buildInputSchemaField();
|
||||
|
||||
expect(result.typeOptions).toEqual({ rows: 10 });
|
||||
});
|
||||
|
||||
it('should have correct default JSON schema', () => {
|
||||
const result = buildInputSchemaField();
|
||||
|
||||
const expectedDefault = `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"some_input": {
|
||||
"type": "string",
|
||||
"description": "Some input to the function"
|
||||
}
|
||||
}
|
||||
}`;
|
||||
expect(result.default).toBe(expectedDefault);
|
||||
});
|
||||
|
||||
it('should include display options with schemaType manual', () => {
|
||||
const result = buildInputSchemaField();
|
||||
|
||||
expect(result.displayOptions).toEqual({
|
||||
show: {
|
||||
schemaType: ['manual'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge showExtraProps when provided', () => {
|
||||
const result = buildInputSchemaField({
|
||||
showExtraProps: {
|
||||
mode: ['advanced'],
|
||||
authentication: ['oauth2'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.displayOptions).toEqual({
|
||||
show: {
|
||||
mode: ['advanced'],
|
||||
authentication: ['oauth2'],
|
||||
schemaType: ['manual'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should include description and hint', () => {
|
||||
const result = buildInputSchemaField();
|
||||
|
||||
expect(result.description).toBe('Schema to use for the function');
|
||||
expect(result.hint).toContain('JSON Schema');
|
||||
expect(result.hint).toContain('json-schema.org');
|
||||
});
|
||||
|
||||
it('should allow data expressions in the schema field', () => {
|
||||
const result = buildInputSchemaField();
|
||||
|
||||
// noDataExpression is false, which means expressions are allowed
|
||||
expect(result.noDataExpression).toBe(false);
|
||||
|
||||
// Since noDataExpression is false, this should be valid
|
||||
expect(typeof result.default).toBe('string');
|
||||
expect(result.noDataExpression).toBe(false);
|
||||
});
|
||||
|
||||
it('should be a valid INodeProperties object', () => {
|
||||
const result = buildInputSchemaField();
|
||||
|
||||
// Check all required fields for INodeProperties
|
||||
expect(result).toHaveProperty('displayName');
|
||||
expect(result).toHaveProperty('name');
|
||||
expect(result).toHaveProperty('type');
|
||||
expect(result).toHaveProperty('default');
|
||||
|
||||
// Verify types
|
||||
expect(typeof result.displayName).toBe('string');
|
||||
expect(typeof result.name).toBe('string');
|
||||
expect(typeof result.type).toBe('string');
|
||||
expect(typeof result.default).toBe('string');
|
||||
});
|
||||
|
||||
it('should properly handle edge cases with showExtraProps', () => {
|
||||
// Empty showExtraProps
|
||||
const result1 = buildInputSchemaField({ showExtraProps: {} });
|
||||
expect(result1.displayOptions).toEqual({
|
||||
show: {
|
||||
schemaType: ['manual'],
|
||||
},
|
||||
});
|
||||
|
||||
// showExtraProps with undefined values
|
||||
const result2 = buildInputSchemaField({
|
||||
showExtraProps: {
|
||||
field1: undefined,
|
||||
field2: ['value2'],
|
||||
},
|
||||
});
|
||||
expect(result2.displayOptions).toEqual({
|
||||
show: {
|
||||
field1: undefined,
|
||||
field2: ['value2'],
|
||||
schemaType: ['manual'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -84,7 +84,7 @@ export const buildInputSchemaField = (props?: {
|
||||
}
|
||||
}
|
||||
}`,
|
||||
noDataExpression: true,
|
||||
noDataExpression: false,
|
||||
typeOptions: {
|
||||
rows: 10,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
import { getOptionalOutputParser } from './N8nOutputParser';
|
||||
import type { N8nStructuredOutputParser } from './N8nStructuredOutputParser';
|
||||
|
||||
describe('getOptionalOutputParser', () => {
|
||||
let mockContext: jest.Mocked<IExecuteFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = mock<IExecuteFunctions>();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return undefined when hasOutputParser is false', async () => {
|
||||
mockContext.getNodeParameter.mockReturnValue(false);
|
||||
|
||||
const result = await getOptionalOutputParser(mockContext);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockContext.getNodeParameter).toHaveBeenCalledWith('hasOutputParser', 0, true);
|
||||
expect(mockContext.getInputConnectionData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return output parser when hasOutputParser is true with default index', async () => {
|
||||
const mockParser = mock<N8nStructuredOutputParser>();
|
||||
mockContext.getNodeParameter.mockReturnValue(true);
|
||||
mockContext.getInputConnectionData.mockResolvedValue(mockParser);
|
||||
|
||||
const result = await getOptionalOutputParser(mockContext);
|
||||
|
||||
expect(result).toBe(mockParser);
|
||||
expect(mockContext.getNodeParameter).toHaveBeenCalledWith('hasOutputParser', 0, true);
|
||||
expect(mockContext.getInputConnectionData).toHaveBeenCalledWith(
|
||||
NodeConnectionTypes.AiOutputParser,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
it('should use provided index when fetching output parser', async () => {
|
||||
const mockParser = mock<N8nStructuredOutputParser>();
|
||||
mockContext.getNodeParameter.mockReturnValue(true);
|
||||
mockContext.getInputConnectionData.mockResolvedValue(mockParser);
|
||||
|
||||
const result = await getOptionalOutputParser(mockContext, 2);
|
||||
|
||||
expect(result).toBe(mockParser);
|
||||
expect(mockContext.getNodeParameter).toHaveBeenCalledWith('hasOutputParser', 0, true);
|
||||
expect(mockContext.getInputConnectionData).toHaveBeenCalledWith(
|
||||
NodeConnectionTypes.AiOutputParser,
|
||||
2,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle different index values correctly', async () => {
|
||||
const mockParser1 = mock<N8nStructuredOutputParser>();
|
||||
const mockParser2 = mock<N8nStructuredOutputParser>();
|
||||
const mockParser3 = mock<N8nStructuredOutputParser>();
|
||||
|
||||
mockContext.getNodeParameter.mockReturnValue(true);
|
||||
mockContext.getInputConnectionData
|
||||
.mockResolvedValueOnce(mockParser1)
|
||||
.mockResolvedValueOnce(mockParser2)
|
||||
.mockResolvedValueOnce(mockParser3);
|
||||
|
||||
const result1 = await getOptionalOutputParser(mockContext, 0);
|
||||
const result2 = await getOptionalOutputParser(mockContext, 1);
|
||||
const result3 = await getOptionalOutputParser(mockContext, 5);
|
||||
|
||||
expect(result1).toBe(mockParser1);
|
||||
expect(result2).toBe(mockParser2);
|
||||
expect(result3).toBe(mockParser3);
|
||||
|
||||
expect(mockContext.getInputConnectionData).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
NodeConnectionTypes.AiOutputParser,
|
||||
0,
|
||||
);
|
||||
expect(mockContext.getInputConnectionData).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
NodeConnectionTypes.AiOutputParser,
|
||||
1,
|
||||
);
|
||||
expect(mockContext.getInputConnectionData).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
NodeConnectionTypes.AiOutputParser,
|
||||
5,
|
||||
);
|
||||
});
|
||||
|
||||
it('should always check hasOutputParser at index 0', async () => {
|
||||
mockContext.getNodeParameter.mockReturnValue(false);
|
||||
|
||||
await getOptionalOutputParser(mockContext, 3);
|
||||
|
||||
// Even when called with index 3, hasOutputParser is checked at index 0
|
||||
expect(mockContext.getNodeParameter).toHaveBeenCalledWith('hasOutputParser', 0, true);
|
||||
expect(mockContext.getInputConnectionData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -14,13 +14,14 @@ export { N8nOutputFixingParser, N8nItemListOutputParser, N8nStructuredOutputPars
|
||||
|
||||
export async function getOptionalOutputParser(
|
||||
ctx: IExecuteFunctions,
|
||||
index: number = 0,
|
||||
): Promise<N8nOutputParser | undefined> {
|
||||
let outputParser: N8nOutputParser | undefined;
|
||||
|
||||
if (ctx.getNodeParameter('hasOutputParser', 0, true) === true) {
|
||||
outputParser = (await ctx.getInputConnectionData(
|
||||
NodeConnectionTypes.AiOutputParser,
|
||||
0,
|
||||
index,
|
||||
)) as N8nOutputParser;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user