fix(OpenAI Node): Replace hardcoded models with RLC (#28226)

This commit is contained in:
Michael Kret 2026-04-20 11:13:47 +03:00 committed by GitHub
parent e848230947
commit 4070930e4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 796 additions and 209 deletions

View file

@ -15,7 +15,7 @@ export class OpenAi extends VersionedNodeType {
name: 'openAi',
icon: { light: 'file:openAi.svg', dark: 'file:openAi.dark.svg' },
group: ['transform'],
defaultVersion: 2.1,
defaultVersion: 2.2,
subtitle: `={{(${prettifyOperation})($parameter.resource, $parameter.operation)}}`,
description: 'Message an assistant or GPT, analyze images, generate audio, etc.',
codex: {
@ -70,6 +70,7 @@ export class OpenAi extends VersionedNodeType {
1.8: new OpenAiV1(baseDescription),
2: new OpenAiV2(baseDescription),
2.1: new OpenAiV2(baseDescription),
2.2: new OpenAiV2(baseDescription),
};
super(nodeVersions, baseDescription);

View file

@ -1,7 +1,7 @@
import type { ILoadOptionsFunctions } from 'n8n-workflow';
import * as transport from '../../transport';
import { modelSearch } from '../listSearch';
import { imageGenerateModelSearch, modelSearch } from '../listSearch';
jest.mock('../../transport');
@ -84,3 +84,48 @@ describe('modelSearch', () => {
});
});
});
describe('imageGenerateModelSearch', () => {
let mockContext: jest.Mocked<ILoadOptionsFunctions>;
beforeEach(() => {
mockContext = {} as unknown as jest.Mocked<ILoadOptionsFunctions>;
jest.clearAllMocks();
});
it('should return only image generation models (dall-e and gpt-image)', async () => {
(transport.apiRequest as jest.Mock).mockResolvedValue({
data: [
{ id: 'dall-e-2' },
{ id: 'dall-e-3' },
{ id: 'gpt-image-1' },
{ id: 'gpt-image-1-mini' },
{ id: 'gpt-4o' },
{ id: 'whisper-1' },
{ id: 'tts-1' },
],
});
const result = await imageGenerateModelSearch.call(mockContext);
expect(result.results).toEqual([
{ name: 'DALL-E-2', value: 'dall-e-2' },
{ name: 'DALL-E-3', value: 'dall-e-3' },
{ name: 'GPT-IMAGE-1', value: 'gpt-image-1' },
{ name: 'GPT-IMAGE-1-MINI', value: 'gpt-image-1-mini' },
]);
});
it('should filter results by search term', async () => {
(transport.apiRequest as jest.Mock).mockResolvedValue({
data: [{ id: 'dall-e-2' }, { id: 'dall-e-3' }, { id: 'gpt-image-1' }],
});
const result = await imageGenerateModelSearch.call(mockContext, 'dall');
expect(result.results).toEqual([
{ name: 'DALL-E-2', value: 'dall-e-2' },
{ name: 'DALL-E-3', value: 'dall-e-3' },
]);
});
});

View file

@ -98,6 +98,15 @@ export async function imageModelSearch(
)(this, filter);
}
export async function imageGenerateModelSearch(
this: ILoadOptionsFunctions,
filter?: string,
): Promise<INodeListSearchResult> {
return await getModelSearch(
(model) => model.id.includes('dall-e') || model.id.includes('gpt-image'),
)(this, filter);
}
export async function assistantSearch(
this: ILoadOptionsFunctions,
filter?: string,

View file

@ -0,0 +1,237 @@
import { mock, mockDeep } from 'jest-mock-extended';
import type { IExecuteFunctions, INode } from 'n8n-workflow';
import * as transport from '../../../../transport';
import { execute } from '../../../../v2/actions/image/generate.operation';
jest.mock('../../../../transport');
describe('Image Generate Operation', () => {
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
let mockNode: INode;
const apiRequestSpy = jest.spyOn(transport, 'apiRequest');
const makeNode = (typeVersion: number): INode =>
mock<INode>({
id: 'test-node',
name: 'OpenAI Image Generate',
type: 'n8n-nodes-base.openAi',
typeVersion,
position: [0, 0],
parameters: {},
});
const mockBinaryData = {
data: 'base64-encoded-image',
mimeType: 'image/png',
fileName: 'data',
};
const b64Response = {
data: [{ b64_json: Buffer.from('fake-image').toString('base64') }],
};
beforeEach(() => {
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
mockExecuteFunctions.helpers.prepareBinaryData = jest.fn().mockResolvedValue(mockBinaryData);
});
afterEach(() => {
jest.resetAllMocks();
});
describe('v2.1 (static model field)', () => {
beforeEach(() => {
mockNode = makeNode(2.1);
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
});
it('should read model as plain string and include response_format for dall-e-3', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params = { model: 'dall-e-3', prompt: 'a cute cat', options: {} };
return params[paramName as keyof typeof params];
});
apiRequestSpy.mockResolvedValue(b64Response);
await execute.call(mockExecuteFunctions, 0);
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
body: expect.objectContaining({ model: 'dall-e-3', response_format: 'b64_json' }),
});
});
it('should not send response_format for gpt-image-1', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params = { model: 'gpt-image-1', prompt: 'a cute cat', options: {} };
return params[paramName as keyof typeof params];
});
apiRequestSpy.mockResolvedValue(b64Response);
await execute.call(mockExecuteFunctions, 0);
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
body: expect.objectContaining({ model: 'gpt-image-1', response_format: undefined }),
});
});
});
describe('v2.2 (RLC model field)', () => {
beforeEach(() => {
mockNode = makeNode(2.2);
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
});
const makeRlcMock =
(modelValue: string, extraOptions = {}) =>
(paramName: string, _i: unknown, _default: unknown, opts?: { extractValue?: boolean }) => {
if (paramName === 'modelId' && opts?.extractValue) return modelValue;
const params = { prompt: 'a cute cat', options: extraOptions };
return params[paramName as keyof typeof params];
};
it('should extract model value from RLC and include response_format for dall-e-3', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation(makeRlcMock('dall-e-3'));
apiRequestSpy.mockResolvedValue(b64Response);
await execute.call(mockExecuteFunctions, 0);
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
body: expect.objectContaining({ model: 'dall-e-3', response_format: 'b64_json' }),
});
});
it('should not send response_format for gpt-image-1', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation(makeRlcMock('gpt-image-1'));
apiRequestSpy.mockResolvedValue(b64Response);
await execute.call(mockExecuteFunctions, 0);
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
body: expect.objectContaining({ model: 'gpt-image-1', response_format: undefined }),
});
});
it('should not send response_format for gpt-image-1-mini', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation(makeRlcMock('gpt-image-1-mini'));
apiRequestSpy.mockResolvedValue(b64Response);
await execute.call(mockExecuteFunctions, 0);
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
body: expect.objectContaining({ model: 'gpt-image-1-mini', response_format: undefined }),
});
});
});
describe('response handling', () => {
beforeEach(() => {
mockNode = makeNode(2.2);
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
});
const makeRlcMock =
(modelValue: string, options = {}) =>
(paramName: string, _i: unknown, _default: unknown, opts?: { extractValue?: boolean }) => {
if (paramName === 'modelId' && opts?.extractValue) return modelValue;
const params = { prompt: 'a cute cat', options };
return params[paramName as keyof typeof params];
};
it('should return binary data by default', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation(makeRlcMock('dall-e-3'));
apiRequestSpy.mockResolvedValue(b64Response);
const result = await execute.call(mockExecuteFunctions, 0);
expect(result).toEqual([
{
json: expect.objectContaining({ data: undefined }),
binary: { data: mockBinaryData },
pairedItem: { item: 0 },
},
]);
});
it('should return URLs when returnImageUrls is true and model supports it', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation(
makeRlcMock('dall-e-3', { returnImageUrls: true }),
);
apiRequestSpy.mockResolvedValue({ data: [{ url: 'https://example.com/image.png' }] });
const result = await execute.call(mockExecuteFunctions, 0);
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
body: expect.objectContaining({ response_format: 'url' }),
});
expect(result).toEqual([
{ json: { url: 'https://example.com/image.png' }, pairedItem: { item: 0 } },
]);
});
it('should return binary even when returnImageUrls is true for gpt-image models', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation(
makeRlcMock('gpt-image-1-mini', { returnImageUrls: true }),
);
apiRequestSpy.mockResolvedValue(b64Response);
const result = await execute.call(mockExecuteFunctions, 0);
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
body: expect.objectContaining({ response_format: undefined }),
});
expect(result[0].binary).toBeDefined();
});
it('should use custom binaryPropertyOutput field name', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation(
makeRlcMock('dall-e-3', { binaryPropertyOutput: 'myImage' }),
);
apiRequestSpy.mockResolvedValue(b64Response);
const result = await execute.call(mockExecuteFunctions, 0);
expect(result[0].binary).toHaveProperty('myImage');
});
});
describe('options processing', () => {
beforeEach(() => {
mockNode = makeNode(2.2);
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
});
it('should rename dalleQuality to quality before sending', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation(
(paramName: string, _i: unknown, _default: unknown, opts?: { extractValue?: boolean }) => {
if (paramName === 'modelId' && opts?.extractValue) return 'dall-e-3';
const params = { prompt: 'a cute cat', options: { dalleQuality: 'hd' } };
return params[paramName as keyof typeof params];
},
);
apiRequestSpy.mockResolvedValue(b64Response);
await execute.call(mockExecuteFunctions, 0);
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
body: expect.objectContaining({ quality: 'hd' }),
});
expect(apiRequestSpy).toHaveBeenCalledWith(
'POST',
'/images/generations',
expect.objectContaining({
body: expect.not.objectContaining({ dalleQuality: expect.anything() }),
}),
);
});
});
});

View file

@ -23,7 +23,7 @@ export class OpenAiV2 implements INodeType {
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
version: [2, 2.1],
version: [2, 2.1, 2.2],
defaults: {
name: 'OpenAI',
},

View file

@ -253,3 +253,412 @@ export const messageOptions: INodePropertyCollection[] = [
],
},
];
export const imageGenerateOptions: INodeProperties = {
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
type: 'collection',
default: {},
displayOptions: {
show: {
'@version': [{ _cnd: { lt: 2.2 } }],
},
},
options: [
{
displayName: 'Number of Images',
name: 'n',
default: 1,
description: 'Number of images to generate',
type: 'number',
typeOptions: {
minValue: 1,
maxValue: 10,
},
displayOptions: {
show: {
'/model': ['dall-e-2'],
},
},
},
{
displayName: 'Quality',
name: 'dalleQuality',
type: 'options',
description:
'The quality of the image that will be generated, HD creates images with finer details and greater consistency across the image',
options: [
{
name: 'HD',
value: 'hd',
},
{
name: 'Standard',
value: 'standard',
},
],
displayOptions: {
show: {
'/model': ['dall-e-3'],
},
},
default: 'standard',
},
{
displayName: 'Quality',
name: 'quality',
type: 'options',
description:
'The quality of the image that will be generated, High creates images with finer details and greater consistency across the image',
options: [
{
name: 'High',
value: 'high',
},
{
name: 'Medium',
value: 'medium',
},
{
name: 'Low',
value: 'low',
},
],
displayOptions: {
show: {
'/model': [{ _cnd: { includes: 'gpt-image' } }],
},
},
default: 'medium',
},
{
displayName: 'Resolution',
name: 'size',
type: 'options',
options: [
{
name: '256x256',
value: '256x256',
},
{
name: '512x512',
value: '512x512',
},
{
name: '1024x1024',
value: '1024x1024',
},
],
displayOptions: {
show: {
'/model': ['dall-e-2'],
},
},
default: '1024x1024',
},
{
displayName: 'Resolution',
name: 'size',
type: 'options',
options: [
{
name: '1024x1024',
value: '1024x1024',
},
{
name: '1792x1024',
value: '1792x1024',
},
{
name: '1024x1792',
value: '1024x1792',
},
],
displayOptions: {
show: {
'/model': ['dall-e-3'],
},
},
default: '1024x1024',
},
{
displayName: 'Resolution',
name: 'size',
type: 'options',
options: [
{
name: '1024x1024',
value: '1024x1024',
},
{
name: '1024x1536',
value: '1024x1536',
},
{
name: '1536x1024',
value: '1536x1024',
},
],
displayOptions: {
show: {
'/model': [{ _cnd: { includes: 'gpt-image' } }],
},
},
default: '1024x1024',
},
{
displayName: 'Style',
name: 'style',
type: 'options',
options: [
{
name: 'Natural',
value: 'natural',
description: 'Produce more natural looking images',
},
{
name: 'Vivid',
value: 'vivid',
description: 'Lean towards generating hyper-real and dramatic images',
},
],
displayOptions: {
show: {
'/model': ['dall-e-3'],
},
},
default: 'vivid',
},
{
displayName: 'Respond with Image URL(s)',
name: 'returnImageUrls',
type: 'boolean',
default: false,
description: 'Whether to return image URL(s) instead of binary file(s)',
displayOptions: {
hide: {
'/model': [{ _cnd: { includes: 'gpt-image' } }],
},
},
},
{
displayName: 'Put Output in Field',
name: 'binaryPropertyOutput',
type: 'string',
default: 'data',
hint: 'The name of the output field to put the binary file data in',
displayOptions: {
show: {
returnImageUrls: [false],
},
},
},
],
};
export const imageGenerateOptionsRLC: INodeProperties = {
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
type: 'collection',
default: {},
displayOptions: {
show: {
'@version': [{ _cnd: { gte: 2.2 } }],
},
},
options: [
{
displayName: 'Number of Images',
name: 'n',
default: 1,
description: 'Number of images to generate',
type: 'number',
typeOptions: {
minValue: 1,
maxValue: 10,
},
displayOptions: {
show: {
'/modelId': ['dall-e-2'],
},
},
},
{
displayName: 'Quality',
name: 'dalleQuality',
type: 'options',
description:
'The quality of the image that will be generated, HD creates images with finer details and greater consistency across the image',
options: [
{
name: 'HD',
value: 'hd',
},
{
name: 'Standard',
value: 'standard',
},
],
displayOptions: {
show: {
'/modelId': ['dall-e-3'],
},
},
default: 'standard',
},
{
displayName: 'Quality',
name: 'quality',
type: 'options',
description:
'The quality of the image that will be generated, High creates images with finer details and greater consistency across the image',
options: [
{
name: 'High',
value: 'high',
},
{
name: 'Medium',
value: 'medium',
},
{
name: 'Low',
value: 'low',
},
],
displayOptions: {
show: {
'/modelId': [{ _cnd: { includes: 'gpt-image' } }],
},
},
default: 'medium',
},
{
displayName: 'Resolution',
name: 'size',
type: 'options',
options: [
{
name: '256x256',
value: '256x256',
},
{
name: '512x512',
value: '512x512',
},
{
name: '1024x1024',
value: '1024x1024',
},
],
displayOptions: {
show: {
'/modelId': ['dall-e-2'],
},
},
default: '1024x1024',
},
{
displayName: 'Resolution',
name: 'size',
type: 'options',
options: [
{
name: '1024x1024',
value: '1024x1024',
},
{
name: '1792x1024',
value: '1792x1024',
},
{
name: '1024x1792',
value: '1024x1792',
},
],
displayOptions: {
show: {
'/modelId': ['dall-e-3'],
},
},
default: '1024x1024',
},
{
displayName: 'Resolution',
name: 'size',
type: 'options',
options: [
{
name: '1024x1024',
value: '1024x1024',
},
{
name: '1024x1536',
value: '1024x1536',
},
{
name: '1536x1024',
value: '1536x1024',
},
],
displayOptions: {
show: {
'/modelId': [{ _cnd: { includes: 'gpt-image' } }],
},
},
default: '1024x1024',
},
{
displayName: 'Style',
name: 'style',
type: 'options',
options: [
{
name: 'Natural',
value: 'natural',
description: 'Produce more natural looking images',
},
{
name: 'Vivid',
value: 'vivid',
description: 'Lean towards generating hyper-real and dramatic images',
},
],
displayOptions: {
show: {
'/modelId': ['dall-e-3'],
},
},
default: 'vivid',
},
{
displayName: 'Respond with Image URL(s)',
name: 'returnImageUrls',
type: 'boolean',
default: false,
description: 'Whether to return image URL(s) instead of binary file(s)',
displayOptions: {
hide: {
'/modelId': [{ _cnd: { includes: 'gpt-image' } }],
},
},
},
{
displayName: 'Put Output in Field',
name: 'binaryPropertyOutput',
type: 'string',
default: 'data',
hint: 'The name of the output field to put the binary file data in',
displayOptions: {
show: {
returnImageUrls: [false],
},
},
},
],
};

View file

@ -7,6 +7,7 @@ import type {
import { updateDisplayOptions } from 'n8n-workflow';
import { apiRequest } from '../../../transport';
import { imageGenerateOptions, imageGenerateOptionsRLC, modelRLC } from '../descriptions';
const properties: INodeProperties[] = [
{
@ -29,6 +30,20 @@ const properties: INodeProperties[] = [
value: 'gpt-image-1',
},
],
displayOptions: {
show: {
'@version': [{ _cnd: { lt: 2.2 } }],
},
},
},
{
...modelRLC('imageGenerateModelSearch'),
default: { mode: 'list', value: 'gpt-image-1-mini' },
displayOptions: {
show: {
'@version': [{ _cnd: { gte: 2.2 } }],
},
},
},
{
displayName: 'Prompt',
@ -42,205 +57,8 @@ const properties: INodeProperties[] = [
rows: 2,
},
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
type: 'collection',
default: {},
options: [
{
displayName: 'Number of Images',
name: 'n',
default: 1,
description: 'Number of images to generate',
type: 'number',
typeOptions: {
minValue: 1,
maxValue: 10,
},
displayOptions: {
show: {
'/model': ['dall-e-2'],
},
},
},
{
displayName: 'Quality',
name: 'dalleQuality',
type: 'options',
description:
'The quality of the image that will be generated, HD creates images with finer details and greater consistency across the image',
options: [
{
name: 'HD',
value: 'hd',
},
{
name: 'Standard',
value: 'standard',
},
],
displayOptions: {
show: {
'/model': ['dall-e-3'],
},
},
default: 'standard',
},
{
displayName: 'Quality',
name: 'quality',
type: 'options',
description:
'The quality of the image that will be generated, High creates images with finer details and greater consistency across the image',
options: [
{
name: 'High',
value: 'high',
},
{
name: 'Medium',
value: 'medium',
},
{
name: 'Low',
value: 'low',
},
],
displayOptions: {
show: {
'/model': ['gpt-image-1'],
},
},
default: 'medium',
},
{
displayName: 'Resolution',
name: 'size',
type: 'options',
options: [
{
name: '256x256',
value: '256x256',
},
{
name: '512x512',
value: '512x512',
},
{
name: '1024x1024',
value: '1024x1024',
},
],
displayOptions: {
show: {
'/model': ['dall-e-2'],
},
},
default: '1024x1024',
},
{
displayName: 'Resolution',
name: 'size',
type: 'options',
options: [
{
name: '1024x1024',
value: '1024x1024',
},
{
name: '1792x1024',
value: '1792x1024',
},
{
name: '1024x1792',
value: '1024x1792',
},
],
displayOptions: {
show: {
'/model': ['dall-e-3'],
},
},
default: '1024x1024',
},
{
displayName: 'Resolution',
name: 'size',
type: 'options',
options: [
{
name: '1024x1024',
value: '1024x1024',
},
{
name: '1024x1536',
value: '1024x1536',
},
{
name: '1536x1024',
value: '1536x1024',
},
],
displayOptions: {
show: {
'/model': ['gpt-image-1'],
},
},
default: '1024x1024',
},
{
displayName: 'Style',
name: 'style',
type: 'options',
options: [
{
name: 'Natural',
value: 'natural',
description: 'Produce more natural looking images',
},
{
name: 'Vivid',
value: 'vivid',
description: 'Lean towards generating hyper-real and dramatic images',
},
],
displayOptions: {
show: {
'/model': ['dall-e-3'],
},
},
default: 'vivid',
},
{
displayName: 'Respond with Image URL(s)',
name: 'returnImageUrls',
type: 'boolean',
default: false,
description: 'Whether to return image URL(s) instead of binary file(s)',
displayOptions: {
hide: {
'/model': ['gpt-image-1'],
},
},
},
{
displayName: 'Put Output in Field',
name: 'binaryPropertyOutput',
type: 'string',
default: 'data',
hint: 'The name of the output field to put the binary file data in',
displayOptions: {
show: {
returnImageUrls: [false],
},
},
},
],
},
imageGenerateOptions,
imageGenerateOptionsRLC,
];
const displayOptions = {
@ -253,13 +71,22 @@ const displayOptions = {
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const model = this.getNodeParameter('model', i) as string;
const nodeVersion = this.getNode().typeVersion;
let model = '';
if (nodeVersion >= 2.2) {
model = this.getNodeParameter('modelId', i, '', { extractValue: true }) as string;
} else {
model = this.getNodeParameter('model', i) as string;
}
const prompt = this.getNodeParameter('prompt', i) as string;
const options = this.getNodeParameter('options', i, {});
const supportsResponseFormat = !model.startsWith('gpt-image');
let response_format = 'b64_json';
let binaryPropertyOutput = 'data';
if (options.returnImageUrls) {
if (options.returnImageUrls && supportsResponseFormat) {
response_format = 'url';
}
@ -277,7 +104,7 @@ export async function execute(this: IExecuteFunctions, i: number): Promise<INode
const body: IDataObject = {
prompt,
model,
response_format: model !== 'gpt-image-1' ? response_format : undefined, // gpt-image-1 does not support response_format
response_format: supportsResponseFormat ? response_format : undefined,
...options,
};

View file

@ -374,16 +374,21 @@ export const checkConditions = (
return (propertyValue as number) >= from && (propertyValue as number) <= to;
}
if (key === 'includes') {
return (propertyValue as string).includes(targetValue);
return typeof propertyValue === 'string' && propertyValue.includes(targetValue as string);
}
if (key === 'startsWith') {
return (propertyValue as string).startsWith(targetValue);
return (
typeof propertyValue === 'string' && propertyValue.startsWith(targetValue as string)
);
}
if (key === 'endsWith') {
return (propertyValue as string).endsWith(targetValue);
return typeof propertyValue === 'string' && propertyValue.endsWith(targetValue as string);
}
if (key === 'regex') {
return new RegExp(targetValue as string).test(propertyValue as string);
return (
typeof propertyValue === 'string' &&
new RegExp(targetValue as string).test(propertyValue)
);
}
if (key === 'exists') {
return propertyValue !== null && propertyValue !== undefined && propertyValue !== '';

View file

@ -5275,6 +5275,60 @@ describe('NodeHelpers', () => {
expect(result).toEqual(expected);
});
}
describe('_cnd string operators with undefined property value', () => {
const node: INode = { ...testNode };
const nodeTypeDescription: INodeTypeDescription = { ...testNodeType };
const baseParam: INodeProperties = {
displayName: 'Test',
name: 'test',
type: 'string',
default: '',
};
test('includes returns false when property is undefined', () => {
const param: INodeProperties = {
...baseParam,
displayOptions: { show: { '/missingParam': [{ _cnd: { includes: 'foo' } }] } },
};
expect(displayParameter({}, param, node, nodeTypeDescription)).toBe(false);
});
test('startsWith returns false when property is undefined', () => {
const param: INodeProperties = {
...baseParam,
displayOptions: { show: { '/missingParam': [{ _cnd: { startsWith: 'foo' } }] } },
};
expect(displayParameter({}, param, node, nodeTypeDescription)).toBe(false);
});
test('endsWith returns false when property is undefined', () => {
const param: INodeProperties = {
...baseParam,
displayOptions: { show: { '/missingParam': [{ _cnd: { endsWith: 'foo' } }] } },
};
expect(displayParameter({}, param, node, nodeTypeDescription)).toBe(false);
});
test('regex returns false when property is undefined', () => {
const param: INodeProperties = {
...baseParam,
displayOptions: { show: { '/missingParam': [{ _cnd: { regex: 'foo' } }] } },
};
expect(displayParameter({}, param, node, nodeTypeDescription)).toBe(false);
});
test('includes returns true when property matches', () => {
const param: INodeProperties = {
...baseParam,
displayOptions: { show: { '/missingParam': [{ _cnd: { includes: 'foo' } }] } },
};
expect(displayParameter({ missingParam: 'foobar' }, param, node, nodeTypeDescription)).toBe(
true,
);
});
});
});
describe('makeDescription', () => {