mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 00:48:04 +08:00
fix(webhook-trigger): request array type adjustment (#25005)
This commit is contained in:
@ -0,0 +1,149 @@
|
||||
import {
|
||||
createParameterTypeOptions,
|
||||
getAvailableParameterTypes,
|
||||
getParameterTypeDisplayName,
|
||||
isValidParameterType,
|
||||
normalizeParameterType,
|
||||
validateParameterValue,
|
||||
} from './parameter-type-utils'
|
||||
|
||||
describe('Parameter Type Utils', () => {
|
||||
describe('isValidParameterType', () => {
|
||||
it('should validate specific array types', () => {
|
||||
expect(isValidParameterType('array[string]')).toBe(true)
|
||||
expect(isValidParameterType('array[number]')).toBe(true)
|
||||
expect(isValidParameterType('array[boolean]')).toBe(true)
|
||||
expect(isValidParameterType('array[object]')).toBe(true)
|
||||
})
|
||||
|
||||
it('should validate basic types', () => {
|
||||
expect(isValidParameterType('string')).toBe(true)
|
||||
expect(isValidParameterType('number')).toBe(true)
|
||||
expect(isValidParameterType('boolean')).toBe(true)
|
||||
expect(isValidParameterType('object')).toBe(true)
|
||||
expect(isValidParameterType('file')).toBe(true)
|
||||
})
|
||||
|
||||
it('should reject invalid types', () => {
|
||||
expect(isValidParameterType('array')).toBe(false)
|
||||
expect(isValidParameterType('invalid')).toBe(false)
|
||||
expect(isValidParameterType('array[invalid]')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeParameterType', () => {
|
||||
it('should normalize valid types', () => {
|
||||
expect(normalizeParameterType('string')).toBe('string')
|
||||
expect(normalizeParameterType('array[string]')).toBe('array[string]')
|
||||
})
|
||||
|
||||
it('should migrate legacy array type', () => {
|
||||
expect(normalizeParameterType('array')).toBe('array[string]')
|
||||
})
|
||||
|
||||
it('should default to string for invalid types', () => {
|
||||
expect(normalizeParameterType('invalid')).toBe('string')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getParameterTypeDisplayName', () => {
|
||||
it('should return correct display names for array types', () => {
|
||||
expect(getParameterTypeDisplayName('array[string]')).toBe('Array[String]')
|
||||
expect(getParameterTypeDisplayName('array[number]')).toBe('Array[Number]')
|
||||
expect(getParameterTypeDisplayName('array[boolean]')).toBe('Array[Boolean]')
|
||||
expect(getParameterTypeDisplayName('array[object]')).toBe('Array[Object]')
|
||||
})
|
||||
|
||||
it('should return correct display names for basic types', () => {
|
||||
expect(getParameterTypeDisplayName('string')).toBe('String')
|
||||
expect(getParameterTypeDisplayName('number')).toBe('Number')
|
||||
expect(getParameterTypeDisplayName('boolean')).toBe('Boolean')
|
||||
expect(getParameterTypeDisplayName('object')).toBe('Object')
|
||||
expect(getParameterTypeDisplayName('file')).toBe('File')
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateParameterValue', () => {
|
||||
it('should validate string values', () => {
|
||||
expect(validateParameterValue('test', 'string')).toBe(true)
|
||||
expect(validateParameterValue('', 'string')).toBe(true)
|
||||
expect(validateParameterValue(123, 'string')).toBe(false)
|
||||
})
|
||||
|
||||
it('should validate number values', () => {
|
||||
expect(validateParameterValue(123, 'number')).toBe(true)
|
||||
expect(validateParameterValue(123.45, 'number')).toBe(true)
|
||||
expect(validateParameterValue('abc', 'number')).toBe(false)
|
||||
expect(validateParameterValue(Number.NaN, 'number')).toBe(false)
|
||||
})
|
||||
|
||||
it('should validate boolean values', () => {
|
||||
expect(validateParameterValue(true, 'boolean')).toBe(true)
|
||||
expect(validateParameterValue(false, 'boolean')).toBe(true)
|
||||
expect(validateParameterValue('true', 'boolean')).toBe(false)
|
||||
})
|
||||
|
||||
it('should validate array values', () => {
|
||||
expect(validateParameterValue(['a', 'b'], 'array[string]')).toBe(true)
|
||||
expect(validateParameterValue([1, 2, 3], 'array[number]')).toBe(true)
|
||||
expect(validateParameterValue([true, false], 'array[boolean]')).toBe(true)
|
||||
expect(validateParameterValue([{ key: 'value' }], 'array[object]')).toBe(true)
|
||||
expect(validateParameterValue(['a', 1], 'array[string]')).toBe(false)
|
||||
expect(validateParameterValue('not an array', 'array[string]')).toBe(false)
|
||||
})
|
||||
|
||||
it('should validate object values', () => {
|
||||
expect(validateParameterValue({ key: 'value' }, 'object')).toBe(true)
|
||||
expect(validateParameterValue({}, 'object')).toBe(true)
|
||||
expect(validateParameterValue(null, 'object')).toBe(false)
|
||||
expect(validateParameterValue([], 'object')).toBe(false)
|
||||
expect(validateParameterValue('string', 'object')).toBe(false)
|
||||
})
|
||||
|
||||
it('should validate file values', () => {
|
||||
const mockFile = new File(['content'], 'test.txt', { type: 'text/plain' })
|
||||
expect(validateParameterValue(mockFile, 'file')).toBe(true)
|
||||
expect(validateParameterValue({ name: 'file.txt' }, 'file')).toBe(true)
|
||||
expect(validateParameterValue('not a file', 'file')).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for invalid types', () => {
|
||||
expect(validateParameterValue('test', 'invalid' as any)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAvailableParameterTypes', () => {
|
||||
it('should return only string for non-request body', () => {
|
||||
const types = getAvailableParameterTypes('application/json', false)
|
||||
expect(types).toEqual(['string'])
|
||||
})
|
||||
|
||||
it('should return all types for application/json', () => {
|
||||
const types = getAvailableParameterTypes('application/json', true)
|
||||
expect(types).toContain('string')
|
||||
expect(types).toContain('number')
|
||||
expect(types).toContain('boolean')
|
||||
expect(types).toContain('array[string]')
|
||||
expect(types).toContain('array[number]')
|
||||
expect(types).toContain('array[boolean]')
|
||||
expect(types).toContain('array[object]')
|
||||
expect(types).toContain('object')
|
||||
})
|
||||
|
||||
it('should include file type for multipart/form-data', () => {
|
||||
const types = getAvailableParameterTypes('multipart/form-data', true)
|
||||
expect(types).toContain('file')
|
||||
})
|
||||
})
|
||||
|
||||
describe('createParameterTypeOptions', () => {
|
||||
it('should create options with display names', () => {
|
||||
const options = createParameterTypeOptions('application/json', true)
|
||||
const stringOption = options.find(opt => opt.value === 'string')
|
||||
const arrayStringOption = options.find(opt => opt.value === 'array[string]')
|
||||
|
||||
expect(stringOption?.name).toBe('String')
|
||||
expect(arrayStringOption?.name).toBe('Array[String]')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,181 @@
|
||||
import type { ArrayElementType, ParameterType } from '../types'
|
||||
|
||||
// Constants for better maintainability and reusability
|
||||
const BASIC_TYPES = ['string', 'number', 'boolean', 'object', 'file'] as const
|
||||
const ARRAY_ELEMENT_TYPES = ['string', 'number', 'boolean', 'object'] as const
|
||||
|
||||
// Generate all valid parameter types programmatically
|
||||
const VALID_PARAMETER_TYPES: readonly ParameterType[] = [
|
||||
...BASIC_TYPES,
|
||||
...ARRAY_ELEMENT_TYPES.map(type => `array[${type}]` as const),
|
||||
] as const
|
||||
|
||||
// Type display name mappings
|
||||
const TYPE_DISPLAY_NAMES: Record<ParameterType, string> = {
|
||||
'string': 'String',
|
||||
'number': 'Number',
|
||||
'boolean': 'Boolean',
|
||||
'object': 'Object',
|
||||
'file': 'File',
|
||||
'array[string]': 'Array[String]',
|
||||
'array[number]': 'Array[Number]',
|
||||
'array[boolean]': 'Array[Boolean]',
|
||||
'array[object]': 'Array[Object]',
|
||||
} as const
|
||||
|
||||
// Content type configurations
|
||||
const CONTENT_TYPE_CONFIGS = {
|
||||
'application/json': {
|
||||
supportedTypes: [...BASIC_TYPES.filter(t => t !== 'file'), ...ARRAY_ELEMENT_TYPES.map(t => `array[${t}]` as const)],
|
||||
description: 'JSON supports all types including arrays',
|
||||
},
|
||||
'text/plain': {
|
||||
supportedTypes: ['string'] as const,
|
||||
description: 'Plain text only supports string',
|
||||
},
|
||||
'application/x-www-form-urlencoded': {
|
||||
supportedTypes: ['string', 'number', 'boolean'] as const,
|
||||
description: 'Form data supports basic types',
|
||||
},
|
||||
'forms': {
|
||||
supportedTypes: ['string', 'number', 'boolean'] as const,
|
||||
description: 'Form data supports basic types',
|
||||
},
|
||||
'multipart/form-data': {
|
||||
supportedTypes: ['string', 'number', 'boolean', 'file'] as const,
|
||||
description: 'Multipart supports basic types plus files',
|
||||
},
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Type guard to check if a string is a valid parameter type
|
||||
*/
|
||||
export const isValidParameterType = (type: string): type is ParameterType => {
|
||||
return (VALID_PARAMETER_TYPES as readonly string[]).includes(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe helper to check if a string is a valid array element type
|
||||
*/
|
||||
const isValidArrayElementType = (type: string): type is ArrayElementType => {
|
||||
return (ARRAY_ELEMENT_TYPES as readonly string[]).includes(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe helper to check if a string is a valid basic type
|
||||
*/
|
||||
const isValidBasicType = (type: string): type is Exclude<ParameterType, `array[${ArrayElementType}]`> => {
|
||||
return (BASIC_TYPES as readonly string[]).includes(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes parameter type from various input formats to the new type system
|
||||
* Handles legacy 'array' type and malformed inputs gracefully
|
||||
*/
|
||||
export const normalizeParameterType = (input: string | undefined | null): ParameterType => {
|
||||
if (!input || typeof input !== 'string')
|
||||
return 'string'
|
||||
|
||||
const trimmed = input.trim().toLowerCase()
|
||||
|
||||
// Handle legacy array type
|
||||
if (trimmed === 'array')
|
||||
return 'array[string]' // Default to string array for backward compatibility
|
||||
|
||||
// Handle specific array types
|
||||
if (trimmed.startsWith('array[') && trimmed.endsWith(']')) {
|
||||
const elementType = trimmed.slice(6, -1) // Extract content between 'array[' and ']'
|
||||
|
||||
if (isValidArrayElementType(elementType))
|
||||
return `array[${elementType}]`
|
||||
|
||||
// Invalid array element type, default to string array
|
||||
return 'array[string]'
|
||||
}
|
||||
|
||||
// Handle basic types
|
||||
if (isValidBasicType(trimmed))
|
||||
return trimmed
|
||||
|
||||
// Fallback to string for unknown types
|
||||
return 'string'
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets display name for parameter types in UI components
|
||||
*/
|
||||
export const getParameterTypeDisplayName = (type: ParameterType): string => {
|
||||
return TYPE_DISPLAY_NAMES[type]
|
||||
}
|
||||
|
||||
// Type validation functions for better reusability
|
||||
const validators = {
|
||||
string: (value: unknown): value is string => typeof value === 'string',
|
||||
number: (value: unknown): value is number => typeof value === 'number' && !isNaN(value),
|
||||
boolean: (value: unknown): value is boolean => typeof value === 'boolean',
|
||||
object: (value: unknown): value is object =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value),
|
||||
file: (value: unknown): value is File =>
|
||||
value instanceof File || (typeof value === 'object' && value !== null),
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Validates array elements based on element type
|
||||
*/
|
||||
const validateArrayElements = (value: unknown[], elementType: ArrayElementType): boolean => {
|
||||
const validator = validators[elementType]
|
||||
return value.every(item => validator(item))
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates parameter value against its declared type
|
||||
* Provides runtime type checking for webhook parameters
|
||||
*/
|
||||
export const validateParameterValue = (value: unknown, type: ParameterType): boolean => {
|
||||
// Handle basic types
|
||||
if (type in validators) {
|
||||
const validator = validators[type as keyof typeof validators]
|
||||
return validator(value)
|
||||
}
|
||||
|
||||
// Handle array types
|
||||
if (type.startsWith('array[') && type.endsWith(']')) {
|
||||
if (!Array.isArray(value)) return false
|
||||
|
||||
const elementType = type.slice(6, -1)
|
||||
return isValidArrayElementType(elementType) && validateArrayElements(value, elementType)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets available parameter types based on content type
|
||||
* Provides context-aware type filtering for different webhook content types
|
||||
*/
|
||||
export const getAvailableParameterTypes = (contentType?: string, isRequestBody = false): ParameterType[] => {
|
||||
if (!isRequestBody) {
|
||||
// Query parameters and headers are always strings
|
||||
return ['string']
|
||||
}
|
||||
|
||||
const normalizedContentType = (contentType || '').toLowerCase()
|
||||
const configKey = normalizedContentType in CONTENT_TYPE_CONFIGS
|
||||
? normalizedContentType as keyof typeof CONTENT_TYPE_CONFIGS
|
||||
: 'application/json'
|
||||
|
||||
const config = CONTENT_TYPE_CONFIGS[configKey]
|
||||
return [...config.supportedTypes]
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates type options for UI select components
|
||||
*/
|
||||
export const createParameterTypeOptions = (contentType?: string, isRequestBody = false) => {
|
||||
const availableTypes = getAvailableParameterTypes(contentType, isRequestBody)
|
||||
|
||||
return availableTypes.map(type => ({
|
||||
name: getParameterTypeDisplayName(type),
|
||||
value: type,
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user