mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
fix(core)!: Type coercion of $fromAI default values (#19128)
This commit is contained in:
@@ -115,6 +115,73 @@ describe('createNodeAsTool', () => {
|
|||||||
expect(tool.schema.shape.booleanWithDefault.description).toBe('Boolean with default');
|
expect(tool.schema.shape.booleanWithDefault.description).toBe('Boolean with default');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow omitting parameters with default values', () => {
|
||||||
|
node.parameters = {
|
||||||
|
requiredParam: "={{ $fromAI('requiredParam', 'Required parameter', 'string') }}",
|
||||||
|
optionalParam:
|
||||||
|
"={{ $fromAI('optionalParam', 'Optional parameter', 'string', 'default value') }}",
|
||||||
|
optionalNumber: "={{ $fromAI('optionalNumber', 'Optional number', 'number', 42) }}",
|
||||||
|
};
|
||||||
|
|
||||||
|
const tool = createNodeAsTool(options).response;
|
||||||
|
|
||||||
|
// Test that the schema accepts an object with only the required field
|
||||||
|
// This should NOT throw an error if fields with defaults are truly optional
|
||||||
|
const parseResult = tool.schema.safeParse({ requiredParam: 'test' });
|
||||||
|
|
||||||
|
expect(parseResult.success).toBe(true);
|
||||||
|
if (parseResult.success) {
|
||||||
|
expect(parseResult.data.requiredParam).toBe('test');
|
||||||
|
expect(parseResult.data.optionalParam).toBe('default value');
|
||||||
|
expect(parseResult.data.optionalNumber).toBe(42);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that all fields can still be provided
|
||||||
|
const fullParseResult = tool.schema.safeParse({
|
||||||
|
requiredParam: 'test',
|
||||||
|
optionalParam: 'custom value',
|
||||||
|
optionalNumber: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fullParseResult.success).toBe(true);
|
||||||
|
if (fullParseResult.success) {
|
||||||
|
expect(fullParseResult.data.requiredParam).toBe('test');
|
||||||
|
expect(fullParseResult.data.optionalParam).toBe('custom value');
|
||||||
|
expect(fullParseResult.data.optionalNumber).toBe(100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow omitting parameters with default values = empty string', () => {
|
||||||
|
node.parameters = {
|
||||||
|
requiredParam: "={{ $fromAI('requiredParam', 'Required parameter', 'string') }}",
|
||||||
|
optionalParam: "={{ $fromAI('optionalParam', 'Optional parameter', 'string', '') }}",
|
||||||
|
};
|
||||||
|
|
||||||
|
const tool = createNodeAsTool(options).response;
|
||||||
|
|
||||||
|
// Test that the schema accepts an object with only the required field
|
||||||
|
// This should NOT throw an error if fields with defaults are truly optional
|
||||||
|
const parseResult = tool.schema.safeParse({ requiredParam: 'test' });
|
||||||
|
|
||||||
|
expect(parseResult.success).toBe(true);
|
||||||
|
if (parseResult.success) {
|
||||||
|
expect(parseResult.data.requiredParam).toBe('test');
|
||||||
|
expect(parseResult.data.optionalParam).toBe('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that all fields can still be provided
|
||||||
|
const fullParseResult = tool.schema.safeParse({
|
||||||
|
requiredParam: 'test',
|
||||||
|
optionalParam: 'custom value',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fullParseResult.success).toBe(true);
|
||||||
|
if (fullParseResult.success) {
|
||||||
|
expect(fullParseResult.data.requiredParam).toBe('test');
|
||||||
|
expect(fullParseResult.data.optionalParam).toBe('custom value');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle nested parameters correctly', () => {
|
it('should handle nested parameters correctly', () => {
|
||||||
node.parameters = {
|
node.parameters = {
|
||||||
topLevel: "={{ $fromAI('topLevel', 'Top level parameter', 'string') }}",
|
topLevel: "={{ $fromAI('topLevel', 'Top level parameter', 'string') }}",
|
||||||
|
|||||||
@@ -104,19 +104,32 @@ export function generateZodSchema(placeholder: FromAIArgument): z.ZodTypeAny {
|
|||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFromAIArgumentType(value: string): value is FromAIArgumentType {
|
||||||
|
return ['string', 'number', 'boolean', 'json'].includes(value.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the default value, preserving its original type.
|
* Parses the default value, preserving its original type.
|
||||||
* @param value The default value as a string.
|
* @param value The default value as a string.
|
||||||
|
* @param type The expected type of the default value.
|
||||||
* @returns The parsed default value in its appropriate type.
|
* @returns The parsed default value in its appropriate type.
|
||||||
*/
|
*/
|
||||||
function parseDefaultValue(
|
function parseDefaultValue(
|
||||||
value: string | undefined,
|
value: string | undefined,
|
||||||
|
type: FromAIArgumentType = 'string',
|
||||||
): string | number | boolean | Record<string, unknown> | undefined {
|
): string | number | boolean | Record<string, unknown> | undefined {
|
||||||
if (value === undefined || value === '') return undefined;
|
if (value === undefined) return value;
|
||||||
|
|
||||||
const lowerValue = value.toLowerCase();
|
const lowerValue = value.toLowerCase();
|
||||||
if (lowerValue === 'true') return true;
|
if (type === 'string') {
|
||||||
if (lowerValue === 'false') return false;
|
return value.toString();
|
||||||
if (!isNaN(Number(value))) return Number(value);
|
}
|
||||||
|
|
||||||
|
if (type === 'boolean' && (lowerValue === 'true' || lowerValue === 'false'))
|
||||||
|
return lowerValue === 'true';
|
||||||
|
if (type === 'number' && !isNaN(Number(value))) return Number(value);
|
||||||
|
|
||||||
|
// For type 'json' or any other case, attempt to parse as JSON
|
||||||
try {
|
try {
|
||||||
return jsonParse(value);
|
return jsonParse(value);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -197,17 +210,17 @@ function parseArguments(argsString: string): FromAIArgument {
|
|||||||
return trimmed;
|
return trimmed;
|
||||||
});
|
});
|
||||||
|
|
||||||
const type = cleanArgs?.[2] || 'string';
|
const type = cleanArgs?.[2] ?? 'string';
|
||||||
|
|
||||||
if (!['string', 'number', 'boolean', 'json'].includes(type.toLowerCase())) {
|
if (!isFromAIArgumentType(type)) {
|
||||||
throw new ParseError(`Invalid type: ${type}`);
|
throw new ParseError(`Invalid type: ${type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: cleanArgs[0] || '',
|
key: cleanArgs[0] || '',
|
||||||
description: cleanArgs[1],
|
description: cleanArgs[1],
|
||||||
type: (cleanArgs?.[2] ?? 'string') as FromAIArgumentType,
|
type,
|
||||||
defaultValue: parseDefaultValue(cleanArgs[3]),
|
defaultValue: parseDefaultValue(cleanArgs[3], type),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,19 @@ describe('extractFromAICalls', () => {
|
|||||||
test.each<[string, [unknown, unknown, unknown, unknown]]>([
|
test.each<[string, [unknown, unknown, unknown, unknown]]>([
|
||||||
['$fromAI("a", "b", "string")', ['a', 'b', 'string', undefined]],
|
['$fromAI("a", "b", "string")', ['a', 'b', 'string', undefined]],
|
||||||
['$fromAI("a", "b", "number", 5)', ['a', 'b', 'number', 5]],
|
['$fromAI("a", "b", "number", 5)', ['a', 'b', 'number', 5]],
|
||||||
|
['$fromAI("a", "b", "number", "5")', ['a', 'b', 'number', 5]],
|
||||||
['$fromAI("a", "`", "number", 5)', ['a', '`', 'number', 5]],
|
['$fromAI("a", "`", "number", 5)', ['a', '`', 'number', 5]],
|
||||||
['$fromAI("a", "\\`", "number", 5)', ['a', '`', 'number', 5]], // this is a bit surprising, but intended
|
['$fromAI("a", "\\`", "number", 5)', ['a', '`', 'number', 5]], // this is a bit surprising, but intended
|
||||||
['$fromAI("a", "\\n", "number", 5)', ['a', 'n', 'number', 5]], // this is a bit surprising, but intended
|
['$fromAI("a", "\\n", "number", 5)', ['a', 'n', 'number', 5]], // this is a bit surprising, but intended
|
||||||
['{{ $fromAI("a", "b", "boolean") }}', ['a', 'b', 'boolean', undefined]],
|
['{{ $fromAI("a", "b", "boolean") }}', ['a', 'b', 'boolean', undefined]],
|
||||||
|
['{{ $fromAI("a", "b", "boolean", "true") }}', ['a', 'b', 'boolean', true]],
|
||||||
|
['{{ $fromAI("a", "b", "boolean", "false") }}', ['a', 'b', 'boolean', false]],
|
||||||
|
['{{ $fromAI("a", "b", "boolean", true) }}', ['a', 'b', 'boolean', true]],
|
||||||
|
['{{ $fromAI("a", "b", "string", "") }}', ['a', 'b', 'string', '']],
|
||||||
|
['{{ $fromAI("a", "b", "string", "null") }}', ['a', 'b', 'string', 'null']],
|
||||||
|
['{{ $fromAI("a", "b", "string", "5") }}', ['a', 'b', 'string', '5']],
|
||||||
|
['{{ $fromAI("a", "b", "string", "true") }}', ['a', 'b', 'string', 'true']],
|
||||||
|
['{{ $fromAI("a", "b", "string", "{}") }}', ['a', 'b', 'string', '{}']],
|
||||||
])('should parse args as expected for %s', (formula, [key, description, type, defaultValue]) => {
|
])('should parse args as expected for %s', (formula, [key, description, type, defaultValue]) => {
|
||||||
expect(extractFromAICalls(formula)).toEqual([
|
expect(extractFromAICalls(formula)).toEqual([
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user