Skip to content

Commit

Permalink
fix(Call n8n Sub-Workflow Tool Node): Fix json type when using $fromAI
Browse files Browse the repository at this point in the history
  • Loading branch information
OlegIvaniv committed Feb 6, 2025
1 parent 00e3ebc commit a9d6797
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,61 @@ export class AIParametersParser {
case 'boolean':
schema = z.boolean();
break;
case 'json':
schema = z.record(z.any());
case 'json': {
interface CustomSchemaDef extends z.ZodTypeDef {
jsonSchema?: {
anyOf: [
{
type: 'object';
minProperties: number;
additionalProperties: boolean;
},
{
type: 'array';
minItems: number;
},
];
};
}

// Create a custom schema to validate that the incoming data is either a non-empty object or a non-empty array.
const customSchema = z.custom<Record<string, unknown> | unknown[]>(
(data: unknown) => {
if (data === null || typeof data !== 'object') return false;
if (Array.isArray(data)) {
return data.length > 0;
}
return Object.keys(data).length > 0;
},
{
message: 'Value must be a non-empty object or a non-empty array',
},
);

// Cast the custom schema to a type that includes our JSON metadata.
const typedSchema = customSchema as z.ZodType<
Record<string, unknown> | unknown[],
CustomSchemaDef
>;

// Attach the updated `jsonSchema` metadata to the internal definition.
typedSchema._def.jsonSchema = {
anyOf: [
{
type: 'object',
minProperties: 1,
additionalProperties: true,
},
{
type: 'array',
minItems: 1,
},
],
};

schema = typedSchema;
break;
}
default:
schema = z.string();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ describe('createNodeAsTool', () => {

const tool = createNodeAsTool(options).response;

expect(tool.schema.shape.complexJson._def.innerType).toBeInstanceOf(z.ZodRecord);
expect(tool.schema.shape.complexJson._def.innerType).toBeInstanceOf(z.ZodEffects);
expect(tool.schema.shape.complexJson.description).toBe('Param with complex JSON default');
expect(tool.schema.shape.complexJson._def.defaultValue()).toEqual({
nested: { key: 'value' },
Expand Down Expand Up @@ -478,4 +478,68 @@ describe('createNodeAsTool', () => {
expect(tool.schema.shape.unicodeParam.description).toBe('🌈 Unicode parameter 你好');
});
});

// Additional tests for JSON type parsing based on recent changes
describe('JSON Type Parsing and Metadata', () => {
it('should correctly parse a JSON parameter without default', () => {
node.parameters = {
jsonWithoutDefault:
"={{ $fromAI('jsonWithoutDefault', 'JSON parameter without default', 'json') }}",
};

const tool = createNodeAsTool(options).response;
// Valid JSON objects (with at least one property) should pass.
const validJson = { key: 'value' };
expect(() => tool.schema.shape.jsonWithoutDefault.parse(validJson)).not.toThrow();

// Parsing an empty object should throw a validation error.
expect(() => tool.schema.shape.jsonWithoutDefault.parse({})).toThrow(
'Value must be a non-empty object or a non-empty array',
);
});

it('should correctly parse a JSON parameter with a valid default', () => {
node.parameters = {
jsonWithValidDefault:
"={{ $fromAI('jsonWithValidDefault', 'JSON parameter with valid default', 'json', '{\"key\": \"defaultValue\"}') }}",
};

const tool = createNodeAsTool(options).response;
// The default value should be parsed from the provided JSON string.
expect(tool.schema.shape.jsonWithValidDefault._def.defaultValue()).toEqual({
key: 'defaultValue',
});
});

it('should throw an error if provided JSON default is an empty object', () => {
node.parameters = {
jsonEmptyDefault:
"={{ $fromAI('jsonEmptyDefault', 'JSON parameter with empty default', 'json', '{}') }}",
};

const tool = createNodeAsTool(options).response;
// The default value is an empty object.
expect(tool.schema.shape.jsonEmptyDefault._def.defaultValue()).toEqual({});
// Parsing an empty object should fail.
expect(() => tool.schema.shape.jsonEmptyDefault.parse({})).toThrow(
'Value must be a non-empty object or a non-empty array',
);
});

it('should use provided JSON value over the default value', () => {
node.parameters = {
jsonParamCustom:
"={{ $fromAI('jsonParamCustom', 'JSON parameter with custom default', 'json', '{\"initial\": \"value\"}') }}",
};

const tool = createNodeAsTool(options).response;
// Check that the default value is correctly set.
expect(tool.schema.shape.jsonParamCustom._def.defaultValue()).toEqual({ initial: 'value' });

// When a new valid value is provided, the schema should use it.
const newValue = { newKey: 'newValue' };
const parsedResult = tool.schema.shape.jsonParamCustom.parse(newValue);
expect(parsedResult).toEqual(newValue);
});
});
});
56 changes: 54 additions & 2 deletions packages/workflow/src/FromAIParseUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,61 @@ export function generateZodSchema(placeholder: FromAIArgument): z.ZodTypeAny {
case 'boolean':
schema = z.boolean();
break;
case 'json':
schema = z.record(z.any());
case 'json': {
interface CustomSchemaDef extends z.ZodTypeDef {
jsonSchema?: {
anyOf: [
{
type: 'object';
minProperties: number;
additionalProperties: boolean;
},
{
type: 'array';
minItems: number;
},
];
};
}

// Create a custom schema to validate that the incoming data is either a non-empty object or a non-empty array.
const customSchema = z.custom<Record<string, unknown> | unknown[]>(
(data: unknown) => {
if (data === null || typeof data !== 'object') return false;
if (Array.isArray(data)) {
return data.length > 0;
}
return Object.keys(data).length > 0;
},
{
message: 'Value must be a non-empty object or a non-empty array',
},
);

// Cast the custom schema to a type that includes our JSON metadata.
const typedSchema = customSchema as z.ZodType<
Record<string, unknown> | unknown[],
CustomSchemaDef
>;

// Attach the updated `jsonSchema` metadata to the internal definition.
typedSchema._def.jsonSchema = {
anyOf: [
{
type: 'object',
minProperties: 1,
additionalProperties: true,
},
{
type: 'array',
minItems: 1,
},
],
};

schema = typedSchema;
break;
}
default:
schema = z.string();
}
Expand Down

0 comments on commit a9d6797

Please sign in to comment.