fix(webhook-trigger): request array type adjustment (#25005)

This commit is contained in:
cathy
2025-09-02 23:20:12 +08:00
committed by GitHub
parent 1d1bb9451e
commit d522350c99
17 changed files with 426 additions and 105 deletions

View File

@ -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]')
})
})
})

View File

@ -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,
}))
}