Skip to content

Commit 5f1e04a

Browse files
committed
fix(tables): reject rolled-over TTL dates
1 parent 0ac33b8 commit 5f1e04a

2 files changed

Lines changed: 23 additions & 8 deletions

File tree

apps/sim/lib/table/__tests__/column-type-registry.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,26 @@ describe('ttl columns', () => {
145145
expect(COLUMN_TYPE_REGISTRY.ttl.coerce(1_700_000_000.5, column)).toEqual({ ok: false })
146146
})
147147

148+
it.each(['2023-02-29', '2023-02-29T12:00:00', '2023-02-29T12:00:00-05:00'])(
149+
'rejects a nonexistent ISO calendar input: %s',
150+
(value) => {
151+
expect(COLUMN_TYPE_REGISTRY.ttl.coerce(value, column, { timezone: 'UTC' })).toEqual({
152+
ok: false,
153+
})
154+
}
155+
)
156+
157+
it.each([
158+
['2024-02-29', '2024-02-29T00:00:00Z'],
159+
['2024-02-29T12:00:00', '2024-02-29T12:00:00Z'],
160+
['2024-02-29T12:00:00-05:00', '2024-02-29T17:00:00Z'],
161+
])('accepts a valid leap-day ISO calendar input: %s', (value, expectedInstant) => {
162+
expect(COLUMN_TYPE_REGISTRY.ttl.coerce(value, column, { timezone: 'UTC' })).toEqual({
163+
ok: true,
164+
value: Math.floor(Date.parse(expectedInstant) / 1000),
165+
})
166+
})
167+
148168
it('renders and edits epoch seconds as a date', () => {
149169
expect(COLUMN_TYPE_REGISTRY.ttl.formatForDisplay(1_700_000_000, column)).toBe(
150170
'11/14/2023 10:13:20 PM'

apps/sim/lib/table/column-types/ttl.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import type { ColumnDefinition } from '@/lib/table/types'
1010

1111
const NUMERIC_VALUE_PATTERN = /^-?\d+(?:\.\d+)?$/
12-
const EXPLICIT_OFFSET_PATTERN = /(?:Z|[+-]\d{2}:?\d{2})$/i
12+
const ISO_DATE_PREFIX_PATTERN = /^(\d{4}-\d{2}-\d{2})(?:$|[T ])/i
1313

1414
function isRepresentableEpochSeconds(value: number): boolean {
1515
return Number.isSafeInteger(value) && !Number.isNaN(new Date(value * 1000).getTime())
@@ -36,19 +36,14 @@ export function parseTtlEpochSeconds(
3636
return isRepresentableEpochSeconds(numeric) ? numeric : null
3737
}
3838

39-
if (EXPLICIT_OFFSET_PATTERN.test(trimmed)) {
40-
const milliseconds = Date.parse(trimmed)
41-
if (Number.isNaN(milliseconds)) return null
42-
const seconds = Math.floor(milliseconds / 1000)
43-
return isRepresentableEpochSeconds(seconds) ? seconds : null
44-
}
45-
4639
const normalized = normalizeDateCellValue(trimmed, options)
4740
if (normalized === null) return null
4841
const instant = /^\d{4}-\d{2}-\d{2}$/.test(normalized)
4942
? normalizeDateCellValue(`${normalized}T00:00:00`, options)
5043
: normalized
5144
if (instant === null) return null
45+
const inputIsoDate = trimmed.match(ISO_DATE_PREFIX_PATTERN)?.[1]
46+
if (inputIsoDate && instant.slice(0, 10) !== inputIsoDate) return null
5247
const milliseconds = Date.parse(instant)
5348
if (Number.isNaN(milliseconds)) return null
5449
const seconds = Math.floor(milliseconds / 1000)

0 commit comments

Comments
 (0)