|
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