Skip to content

Commit

Permalink
Convert timestamp fields to Date (#305)
Browse files Browse the repository at this point in the history
* add utils for converting timestamp types to Date/number

* fix typos

* extend toExternalJSON to convert timestamps to Date objects

* transform payload for recordings in Room

* fix casing

* add missing id

* add changeset

* use toExternalJSON for last return

* remove id

* update changeset

* update new type mappers with more descriptive names and the option to define a custom type mapper
  • Loading branch information
framini authored Sep 23, 2021
1 parent f2b14e7 commit cec54bd
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 18 deletions.
7 changes: 7 additions & 0 deletions .changeset/real-goats-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@signalwire/core': patch
'@signalwire/js': patch
'@signalwire/realtime-api': patch
---

Convert timestamp properties to Date objects.
32 changes: 31 additions & 1 deletion packages/core/src/types/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,37 @@ export type OnlyStateProperties<T> = Pick<T, OnlyStatePropertyNames<T>>
* compile-time error. If they are equal, `RoomSession` will refer to the
* documented version of the methods.
*/
export type AssertSameType<ExpectedType, Output> = ExpectedType extends Output ? Output extends ExpectedType ? Output : never : never
export type AssertSameType<ExpectedType, Output> = ExpectedType extends Output
? Output extends ExpectedType
? Output
: never
: never

export type IsTimestampProperty<Property> = Property extends `${string}At`
? Property
: never

export interface DefaultPublicToInternalTypeMapping {
startedAt?: number
endedAt?: number
}

/**
* For user convenience, sometimes we expose properties with
* a different type than the one used by the server. A good
* example of this are the `startedAt` and `endedAt` fields
* where we give a `Date` object to the user while the
* server treat them as timestamps (`number`).
*/
export type ConvertToInternalTypes<
Property extends string,
DefaultType,
TypesMap extends Partial<
Record<string, any>
> = DefaultPublicToInternalTypeMapping
> = Property extends IsTimestampProperty<Property>
? TypesMap[Property]
: DefaultType

export interface ConstructableType<T> {
new (o?: any): T
Expand Down
20 changes: 15 additions & 5 deletions packages/core/src/types/videoRecording.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { SwEvent } from '.'
import type {
CamelToSnakeCase,
ConvertToInternalTypes,
ToInternalVideoEvent,
OnlyStateProperties,
OnlyFunctionProperties,
Expand Down Expand Up @@ -45,10 +46,10 @@ export interface VideoRecordingContract {
duration?: number

/** Start time, if available */
startedAt?: number
startedAt?: Date

/** End time, if available */
endedAt?: number
endedAt?: Date

/** Pauses the recording. */
pause(): Promise<void>
Expand All @@ -72,13 +73,22 @@ export type VideoRecordingMethods =
OnlyFunctionProperties<VideoRecordingContract>

/**
* VideoRecordingEntity entity for internal usage (converted to snake_case)
* VideoRecordingEntity entity for internal usage (converted
* to snake_case and mapped to internal types.)
* @internal
*/
export type InternalVideoRecordingEntity = {
[K in NonNullable<
[Property in NonNullable<
keyof VideoRecordingEntity
> as CamelToSnakeCase<K>]: VideoRecordingEntity[K]
> as CamelToSnakeCase<Property>]: ConvertToInternalTypes<
Property,
/**
* Default type to be applied to `Property` in case
* `ConvertToInternalTypes` doesn't have an explicit
* conversion for the property.
*/
VideoRecordingEntity[Property]
>
}

/**
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/utils/toExternalJSON.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,30 @@ describe('toExternalJSON', () => {

expect(toExternalJSON(input)).toStrictEqual(output)
})

it('converts timestamp properties to Date objects', () => {
const input = {
started_at: 1632305086964,
ended_at: 1632305100130,
completed_at: 'an invalid date',
started: 1632305086964,
ended: 1632305100130,
prop_one: 'one',
prop_two: 'two',
prop_three: 1,
}

const output = {
startedAt: new Date(1632305086964),
endedAt: new Date(1632305100130),
completedAt: 'an invalid date',
started: 1632305086964,
ended: 1632305100130,
propOne: 'one',
propTwo: 'two',
propThree: 1,
}

expect(toExternalJSON(input)).toStrictEqual(output)
})
})
33 changes: 31 additions & 2 deletions packages/core/src/utils/toExternalJSON.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
const toDateObject = (timestamp?: number) => {
if (typeof timestamp === 'undefined') {
return timestamp
}

const date = new Date(timestamp)

/**
* If for some reason we can't convert to a valid date
* we'll return the original value
*/
if (isNaN(date.getTime())) {
return timestamp
}

return date
}

const DEFAULT_OPTIONS = {
/**
* Properties coming from the server where their value will be
Expand All @@ -6,6 +24,13 @@ const DEFAULT_OPTIONS = {
propsToUpdateValue: ['updated', 'layers'],
}

/**
* Follows the same convention as `src/types/utils/IsTimestamp`
*/
const isTimestampProperty = (prop: string) => {
return prop.endsWith('At')
}

/**
* Converts a record (a JSON coming from the server) to a JSON meant
* to be consumed by our users. This mostly mean converting properties
Expand All @@ -30,7 +55,7 @@ export const toExternalJSON = <T>(
const propType = typeof value

/**
* While this check won't be enough to detect all possible object
* While this check won't be enough to detect all possible objects
* it would cover our needs here since we just need to detect that
* it's not a primitive value
*/
Expand All @@ -46,7 +71,11 @@ export const toExternalJSON = <T>(
reducer[prop] = toExternalJSON(value as T)
}
} else {
reducer[prop] = value
if (isTimestampProperty(prop)) {
reducer[prop] = toDateObject(value)
} else {
reducer[prop] = value
}
}

return reducer
Expand Down
16 changes: 9 additions & 7 deletions packages/js/src/Room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
BaseComponentOptions,
BaseConnectionContract,
toLocalEvent,
toExternalJSON,
} from '@signalwire/core'
import {
getDisplayMedia,
Expand Down Expand Up @@ -77,15 +78,16 @@ export class RoomConnection
},
payloadTransform: (payload: any) => {
if (payload?.recording) {
return {
...payload?.recording,
roomSessionId: this.roomSessionId,
}
return toExternalJSON({
...payload.recording,
room_session_id: this.roomSessionId,
})
}
return {

return toExternalJSON({
id: payload.recording_id,
roomSessionId: this.roomSessionId,
}
room_session_id: this.roomSessionId,
})
},
},
],
Expand Down
7 changes: 4 additions & 3 deletions packages/realtime-api/src/video/RoomSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,10 +515,11 @@ class RoomSessionConsumer extends BaseConsumer<RealTimeRoomApiEvents> {
room_session_id: payload.room_session_id,
})
}
return {

return toExternalJSON({
id: payload.recording_id,
roomSessionId: payload.room_session_id,
}
room_session_id: payload.room_session_id,
})
},
},
],
Expand Down

0 comments on commit cec54bd

Please sign in to comment.