diff --git a/internal/playground-realtime-api/src/voice/index.ts b/internal/playground-realtime-api/src/voice/index.ts index 89c4697d2d..eee9c5f535 100644 --- a/internal/playground-realtime-api/src/voice/index.ts +++ b/internal/playground-realtime-api/src/voice/index.ts @@ -139,7 +139,7 @@ async function run() { volume: 1.0, digits: { max: 4, - digit_timeout: 10, + digitTimeout: 10, terminators: '#', }, }) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e13eee555f..55cc342096 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,6 +4,7 @@ import { getLogger, isGlobalEvent, toExternalJSON, + toSnakeCaseKeys, toLocalEvent, toSyntheticEvent, extendComponent, @@ -44,6 +45,7 @@ export { getEventEmitter, isGlobalEvent, toExternalJSON, + toSnakeCaseKeys, toLocalEvent, toInternalEventName, serializeableProxy, diff --git a/packages/core/src/types/voiceCall.ts b/packages/core/src/types/voiceCall.ts index 3739f5b941..5594bf210c 100644 --- a/packages/core/src/types/voiceCall.ts +++ b/packages/core/src/types/voiceCall.ts @@ -138,7 +138,7 @@ export interface VoiceCallSipParams { timeout?: number headers?: SipHeader[] codecs?: SipCodec[] - webrtc_media?: boolean + webrtcMedia?: boolean } export interface NestedArray extends Array> {} @@ -207,8 +207,8 @@ export interface VoiceCallRecordMethodParams { format?: 'mp3' | 'wav' stereo?: boolean direction?: 'listen' | 'speak' | 'both' - initial_timeout?: number - end_silence_timeout?: number + initialTimeout?: number + endSilenceTimeout?: number terminators?: string } } @@ -217,7 +217,7 @@ type SpeechOrDigits = | { digits: { max: number - digit_timeout?: number + digitTimeout?: number terminators?: string } speech?: never @@ -225,8 +225,8 @@ type SpeechOrDigits = | { digits?: never speech: { - end_silence_timeout: number - speech_timeout: number + endSilenceTimeout: number + speechTimeout: number language: number hints: string[] } @@ -234,26 +234,26 @@ type SpeechOrDigits = export type VoiceCallPromptMethodParams = SpeechOrDigits & { media: NestedArray volume?: number - initial_timeout?: number - partial_results?: boolean + initialTimeout?: number + partialResults?: boolean } export type VoiceCallPromptAudioMethodParams = SpeechOrDigits & Omit & { volume?: number - initial_timeout?: number - partial_results?: boolean + initialTimeout?: number + partialResults?: boolean } export type VoiceCallPromptRingtoneMethodParams = SpeechOrDigits & Omit & { volume?: number - initial_timeout?: number - partial_results?: boolean + initialTimeout?: number + partialResults?: boolean } export type VoiceCallPromptTTSMethodParams = SpeechOrDigits & Omit & { volume?: number - initial_timeout?: number - partial_results?: boolean + initialTimeout?: number + partialResults?: boolean } type TapCodec = 'OPUS' | 'PCMA' | 'PCMU' interface TapDeviceWS { diff --git a/packages/core/src/utils/common.ts b/packages/core/src/utils/common.ts new file mode 100644 index 0000000000..5823e1550b --- /dev/null +++ b/packages/core/src/utils/common.ts @@ -0,0 +1,11 @@ +const UPPERCASE_REGEX = /[A-Z]/g +/** + * Converts values from camelCase to snake_case + * @internal + */ +export const fromCamelToSnakeCase = (event: T): T => { + // @ts-ignore + return event.replace(UPPERCASE_REGEX, (letter) => { + return `_${letter.toLowerCase()}` + }) as T +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 073c4e90d3..f5990dcb88 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -14,6 +14,7 @@ export * from './parseRPCResponse' export * from './toExternalJSON' export * from './toInternalEventName' export * from './toInternalAction' +export * from './toSnakeCaseKeys' export * from './extendComponent' export * from './eventTransformUtils' export * from './proxyUtils' diff --git a/packages/core/src/utils/toInternalEventName.ts b/packages/core/src/utils/toInternalEventName.ts index e002953e44..1677a0470c 100644 --- a/packages/core/src/utils/toInternalEventName.ts +++ b/packages/core/src/utils/toInternalEventName.ts @@ -1,3 +1,4 @@ +import { fromCamelToSnakeCase } from './common' import { EVENT_NAMESPACE_DIVIDER } from './constants' import { EventEmitter } from './EventEmitter' @@ -27,18 +28,6 @@ export const toInternalEventName = < return event } -const UPPERCASE_REGEX = /[A-Z]/g -/** - * Converts values from camelCase to snake_case - * @internal - */ -const fromCamelToSnakeCase = (event: T): T => { - // @ts-ignore - return event.replace(UPPERCASE_REGEX, (letter) => { - return `_${letter.toLowerCase()}` - }) as T -} - const getNamespacedEvent = ({ namespace, event, diff --git a/packages/core/src/utils/toSnakeCaseKeys.test.ts b/packages/core/src/utils/toSnakeCaseKeys.test.ts new file mode 100644 index 0000000000..3c893c0e6f --- /dev/null +++ b/packages/core/src/utils/toSnakeCaseKeys.test.ts @@ -0,0 +1,114 @@ +import { toSnakeCaseKeys } from './toSnakeCaseKeys' + +describe('toSnakeCaseKeys', () => { + it('should convert properties from camelCase to snake_case', () => { + expect( + toSnakeCaseKeys({ + someProperty: 'someValue', + someOtherProperty: 'someOtherValue', + nestedProperty: { + nestedProperty1: 'nestedValue', + nestedProperty2: 'nestedValue2', + nestedProperty3: { + nestedProperty4: 'nestedValue4', + nestedProperty5: { + nestedProperty6: 'nestedValue6', + nestedProperty7: { + nestedProperty8: 'nestedValue8', + }, + }, + }, + nestedProperty4: [ + { + somePropertyKey: 'value_in_key', + somePropertyNested: { + somePropertyNested1: 'value_in_key_nested1', + somePropertyNested2: [ + { + somePropertyNested21: { + somePropertyNested211: 'value', + }, + }, + ], + }, + }, + ], + }, + nestedPropertyWithLongName: 'nestedValueWithLongName', + }) + ).toEqual({ + some_property: 'someValue', + some_other_property: 'someOtherValue', + nested_property: { + nested_property1: 'nestedValue', + nested_property2: 'nestedValue2', + nested_property3: { + nested_property4: 'nestedValue4', + nested_property5: { + nested_property6: 'nestedValue6', + nested_property7: { + nested_property8: 'nestedValue8', + }, + }, + }, + nested_property4: [ + { + some_property_key: 'value_in_key', + some_property_nested: { + some_property_nested1: 'value_in_key_nested1', + some_property_nested2: [ + { + some_property_nested21: { + some_property_nested211: 'value', + }, + }, + ], + }, + }, + ], + }, + nested_property_with_long_name: 'nestedValueWithLongName', + }) + }) + + it('should allow passing a function for transforming values', () => { + expect( + toSnakeCaseKeys( + { + someProperty: 'someValue', + someOtherProperty: 'someOtherValue', + nestedProperty: { + nestedProperty1: 'nestedValue', + nestedProperty2: 'nestedValue2', + nestedProperty3: { + nestedProperty4: 'nestedValue4', + nestedProperty5: { + nestedProperty6: 'nestedValue6', + nestedProperty7: { + nestedProperty8: 'nestedValue8', + }, + }, + }, + }, + }, + (value: string) => value.toUpperCase() + ) + ).toEqual({ + some_property: 'SOMEVALUE', + some_other_property: 'SOMEOTHERVALUE', + nested_property: { + nested_property1: 'NESTEDVALUE', + nested_property2: 'NESTEDVALUE2', + nested_property3: { + nested_property4: 'NESTEDVALUE4', + nested_property5: { + nested_property6: 'NESTEDVALUE6', + nested_property7: { + nested_property8: 'NESTEDVALUE8', + }, + }, + }, + }, + }) + }) +}) diff --git a/packages/core/src/utils/toSnakeCaseKeys.ts b/packages/core/src/utils/toSnakeCaseKeys.ts new file mode 100644 index 0000000000..c7e64bbfba --- /dev/null +++ b/packages/core/src/utils/toSnakeCaseKeys.ts @@ -0,0 +1,32 @@ +import { CamelToSnakeCase } from '../types/utils' +import { fromCamelToSnakeCase } from './common' + +type ToSnakeCaseKeys = { + [Property in NonNullable as CamelToSnakeCase< + Extract + >]: T[Property] +} + +export const toSnakeCaseKeys = >( + obj: T, + transform: (value: string) => any = (value: string) => value, + result: Record = {} +) => { + if (Array.isArray(obj)) { + result = obj.map((item: any, index: number) => + toSnakeCaseKeys(item, transform, result[index]) + ) + } else { + Object.keys(obj).forEach((key) => { + const newKey = fromCamelToSnakeCase(key) + // Both 'object's and arrays will enter this branch + if (obj[key] && typeof obj[key] === 'object') { + result[newKey] = toSnakeCaseKeys(obj[key], transform, result[newKey]) + } else { + result[newKey] = transform(obj[key]) + } + }) + } + + return result as ToSnakeCaseKeys +} diff --git a/packages/realtime-api/src/voice/Call.ts b/packages/realtime-api/src/voice/Call.ts index 152aabba44..b998f0c5ec 100644 --- a/packages/realtime-api/src/voice/Call.ts +++ b/packages/realtime-api/src/voice/Call.ts @@ -24,6 +24,7 @@ import { EventTransform, toLocalEvent, toExternalJSON, + toSnakeCaseKeys, CallingCallPlayEventParams, VoiceCallTapMethodParams, VoiceCallTapAudioMethodParams, @@ -498,7 +499,7 @@ export class CallConsumer extends AutoApplyTransformsConsumer { @@ -11,18 +12,18 @@ const toInternalDevice = (device: VoiceCallDeviceParams) => { const { type, ...params } = device return { type, - params, + params: toSnakeCaseKeys(params), } } case 'phone': { const { to, from, type, ...rest } = device return { type, - params: { + params: toSnakeCaseKeys({ ...rest, to_number: to, from_number: from, - }, + }), } }