|
1 | 1 | import type { ValidationNames } from '../types'
|
2 |
| -import { TimestampValidator } from './timestamps' |
| 2 | +import { BaseValidator } from './base' |
3 | 3 |
|
4 |
| -export class TimestampTzValidator extends TimestampValidator { |
| 4 | +export class TimestampTzValidator extends BaseValidator<number | string> { |
5 | 5 | public name: ValidationNames = 'timestampTz'
|
6 | 6 |
|
7 | 7 | constructor() {
|
8 | 8 | super()
|
9 | 9 | this.addRule({
|
10 | 10 | name: 'timestampTz',
|
11 |
| - test: (value: number | string) => { |
12 |
| - // Check if it's a valid number |
13 |
| - const num = Number(value) |
14 |
| - if (Number.isNaN(num)) { |
| 11 | + test: (value: number | string | null | undefined) => { |
| 12 | + if (value === null || value === undefined) { |
15 | 13 | return false
|
16 | 14 | }
|
17 | 15 |
|
18 |
| - // MySQL TIMESTAMP range: 1970-01-01 to 2038-01-19 |
19 |
| - const minTimestamp = 0 // 1970-01-01 00:00:00 UTC |
20 |
| - const maxTimestamp = 2147483647 // 2038-01-19 03:14:07 UTC |
| 16 | + // For numeric values, validate as Unix timestamp |
| 17 | + if (typeof value === 'number') { |
| 18 | + const num = Number(value) |
| 19 | + if (Number.isNaN(num)) { |
| 20 | + return false |
| 21 | + } |
21 | 22 |
|
22 |
| - // First check if it's within the valid range |
23 |
| - if (num < minTimestamp || num > maxTimestamp) { |
24 |
| - return false |
| 23 | + // MySQL TIMESTAMP range: 1970-01-01 to 2038-01-19 |
| 24 | + const minTimestamp = 0 // 1970-01-01 00:00:00 UTC |
| 25 | + const maxTimestamp = 2147483647 // 2038-01-19 03:14:07 UTC |
| 26 | + |
| 27 | + return num >= minTimestamp && num <= maxTimestamp |
25 | 28 | }
|
26 | 29 |
|
27 |
| - // For string inputs, check if the length is valid (10-13 digits) |
| 30 | + // For string values, check for timezone information |
28 | 31 | if (typeof value === 'string') {
|
29 |
| - const timestampStr = value.toString() |
30 |
| - const length = timestampStr.length |
31 |
| - if (length < 10 || length > 13) { |
32 |
| - return false |
| 32 | + const str = value.trim() |
| 33 | + |
| 34 | + // Check if it's a numeric string (Unix timestamp) |
| 35 | + const num = Number(str) |
| 36 | + if (!Number.isNaN(num)) { |
| 37 | + // If it's a numeric string, validate as Unix timestamp |
| 38 | + const minTimestamp = 0 |
| 39 | + const maxTimestamp = 2147483647 |
| 40 | + |
| 41 | + // Check length (10-13 digits) for Unix timestamps |
| 42 | + if (str.length < 10 || str.length > 13) { |
| 43 | + return false |
| 44 | + } |
| 45 | + |
| 46 | + return num >= minTimestamp && num <= maxTimestamp |
| 47 | + } |
| 48 | + |
| 49 | + // Check for ISO 8601 format with timezone |
| 50 | + // Examples: 2023-12-25T10:30:00Z, 2023-12-25T10:30:00+05:00, 2023-12-25T10:30:00-08:00 |
| 51 | + const isoWithTzRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|[+-]\d{2}:\d{2})$/ |
| 52 | + if (isoWithTzRegex.test(str)) { |
| 53 | + const date = new Date(str) |
| 54 | + return !Number.isNaN(date.getTime()) |
33 | 55 | }
|
| 56 | + |
| 57 | + // Check for RFC 3339 format with timezone |
| 58 | + // Examples: 2023-12-25 10:30:00Z, 2023-12-25 10:30:00+05:00 |
| 59 | + const rfc3339WithTzRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d{3})?(Z|[+-]\d{2}:\d{2})$/ |
| 60 | + if (rfc3339WithTzRegex.test(str)) { |
| 61 | + const date = new Date(str) |
| 62 | + return !Number.isNaN(date.getTime()) |
| 63 | + } |
| 64 | + |
| 65 | + // Check for other common timezone formats |
| 66 | + // Examples: 2023-12-25T10:30:00.000Z, 2023-12-25 10:30:00 UTC |
| 67 | + const otherTzFormats = [ |
| 68 | + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, // ISO with milliseconds |
| 69 | + /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC$/, // UTC suffix |
| 70 | + /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} GMT$/, // GMT suffix |
| 71 | + /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [A-Z]{3,4}$/, // Timezone abbreviations |
| 72 | + ] |
| 73 | + |
| 74 | + for (const regex of otherTzFormats) { |
| 75 | + if (regex.test(str)) { |
| 76 | + const date = new Date(str) |
| 77 | + return !Number.isNaN(date.getTime()) |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + return false |
34 | 82 | }
|
35 | 83 |
|
36 |
| - return true |
| 84 | + return false |
37 | 85 | },
|
38 |
| - message: 'Must be a valid timestamp between 1970-01-01 and 2038-01-19', |
| 86 | + message: 'Must be a valid timestamp with timezone information (ISO 8601, RFC 3339, or Unix timestamp)', |
39 | 87 | })
|
40 | 88 | }
|
| 89 | + |
| 90 | + test(value: any): boolean { |
| 91 | + // Override the base test method to handle null/undefined properlyc |
| 92 | + if (value === null || value === undefined) { |
| 93 | + return !this.isRequired |
| 94 | + } |
| 95 | + |
| 96 | + return this.validate(value).valid |
| 97 | + } |
41 | 98 | }
|
42 | 99 |
|
43 | 100 | export function timestampTz(): TimestampTzValidator {
|
|
0 commit comments