Validate date time fields when adding/editing photos

This commit is contained in:
Sam Becker 2025-01-22 18:05:33 -06:00
parent ad11ce32b0
commit d6e5aa012e
4 changed files with 74 additions and 25 deletions

View File

@ -1,6 +1,9 @@
/* eslint-disable max-len */
import { import {
convertTimestampToNaivePostgresString, convertTimestampToNaivePostgresString,
convertTimestampWithOffsetToPostgresString, convertTimestampWithOffsetToPostgresString,
validatePostgresDateString,
validateNaivePostgresDateString,
} from '../src/utility/date'; } from '../src/utility/date';
describe('Date utility', () => { describe('Date utility', () => {
@ -29,7 +32,6 @@ describe('Date utility', () => {
expect(convertTimestampToNaivePostgresString(timestamp)) expect(convertTimestampToNaivePostgresString(timestamp))
.toBe('2023-12-02 16:38:36'); .toBe('2023-12-02 16:38:36');
}); });
});
it('Malformed date string', () => { it('Malformed date string', () => {
const timestamp = '2024/01a/01 Z'; const timestamp = '2024/01a/01 Z';
expect(convertTimestampWithOffsetToPostgresString(timestamp)) expect(convertTimestampWithOffsetToPostgresString(timestamp))
@ -44,4 +46,20 @@ describe('Date utility', () => {
new Date().toISOString(), new Date().toISOString(),
)); ));
}); });
});
describe('validates date strings', () => {
it('Correct', () => {
expect(validatePostgresDateString('2025-01-03T21:00:44.000Z')).toBe(true);
expect(validateNaivePostgresDateString('2025-01-03 16:00:44')).toBe(true);
});
it('Incorrect', () => {
expect(validatePostgresDateString('2024-01-01')).toBe(false);
expect(validatePostgresDateString('2025-01-03 16:00:44')).toBe(false);
expect(validateNaivePostgresDateString('2024-01-01')).toBe(false);
expect(validatePostgresDateString('2025-01-03T21:00:44.000')).toBe(false);
expect(validateNaivePostgresDateString('2025-01-03T16:00:44')).toBe(false);
expect(validatePostgresDateString('2025-01-03T21:00:44.000ZZ')).toBe(false);
expect(validateNaivePostgresDateString('2025-01-03 16:00:44Z')).toBe(false);
});
});
}); });

View File

@ -58,7 +58,7 @@ export default function FieldSetWithStatus({
{!hideLabel && label && {!hideLabel && label &&
<label <label
className={clsx( className={clsx(
'flex gap-2 items-center select-none', 'flex flex-wrap gap-x-2 items-center select-none',
type === 'checkbox' && 'order-2 pt-[3px]', type === 'checkbox' && 'order-2 pt-[3px]',
)} )}
htmlFor={id} htmlFor={id}

View File

@ -5,6 +5,8 @@ import {
convertTimestampWithOffsetToPostgresString, convertTimestampWithOffsetToPostgresString,
generateLocalNaivePostgresString, generateLocalNaivePostgresString,
generateLocalPostgresString, generateLocalPostgresString,
validationMessageNaivePostgresDateString,
validationMessagePostgresDateString,
} from '@/utility/date'; } from '@/utility/date';
import { import {
convertApertureValueToFNumber, convertApertureValueToFNumber,
@ -116,8 +118,14 @@ const FORM_METADATA = (
locationName: { label: 'location name', hide: true }, locationName: { label: 'location name', hide: true },
latitude: { label: 'latitude' }, latitude: { label: 'latitude' },
longitude: { label: 'longitude' }, longitude: { label: 'longitude' },
takenAt: { label: 'taken at' }, takenAt: {
takenAtNaive: { label: 'taken at (naive)' }, label: 'taken at',
validate: validationMessagePostgresDateString,
},
takenAtNaive: {
label: 'taken at (naive)',
validate: validationMessageNaivePostgresDateString,
},
priorityOrder: { label: 'priority order' }, priorityOrder: { label: 'priority order' },
favorite: { label: 'favorite', type: 'checkbox', excludeFromInsert: true }, favorite: { label: 'favorite', type: 'checkbox', excludeFromInsert: true },
hidden: { label: 'hidden', type: 'checkbox' }, hidden: { label: 'hidden', type: 'checkbox' },

View File

@ -16,6 +16,9 @@ const DATE_STRING_FORMAT_LONG_PLACEHOLDER = '00 000 0000 00:0000';
const DATE_STRING_FORMAT_POSTGRES = 'yyyy-MM-dd HH:mm:ss'; const DATE_STRING_FORMAT_POSTGRES = 'yyyy-MM-dd HH:mm:ss';
export const VALIDATION_EXAMPLE_POSTGRES = '2025-01-03T21:00:44.000Z';
export const VALIDATION_EXAMPLE_POSTGRES_NAIVE = '2025-01-03 16:00:44';
type AmbiguousTimestamp = number | string; type AmbiguousTimestamp = number | string;
type Length = 'tiny' | 'short' | 'medium' | 'long'; type Length = 'tiny' | 'short' | 'medium' | 'long';
@ -103,3 +106,23 @@ export const generateLocalPostgresString = () =>
export const generateLocalNaivePostgresString = () => export const generateLocalNaivePostgresString = () =>
format(new Date(), DATE_STRING_FORMAT_POSTGRES); format(new Date(), DATE_STRING_FORMAT_POSTGRES);
// Form validation to prevent Postgres runtime errors
// POSTGRES: 2025-01-03T21:00:44.000Z
export const validatePostgresDateString = (date = ''): boolean =>
/^(\d{4}-\d{2}-\d{2})T\d{2}:\d{2}:\d{2}(.[\d]+)*Z$/.test(date);
export const validationMessagePostgresDateString = (date = '') =>
validatePostgresDateString(date)
? undefined
: `Invalid format (${VALIDATION_EXAMPLE_POSTGRES})`;
// NAIVE: 2025-01-03 16:00:44
export const validateNaivePostgresDateString = (date = ''): boolean =>
/^(\d{4}-\d{2}-\d{2}) \d{2}:\d{2}:\d{2}$/.test(date);
export const validationMessageNaivePostgresDateString = (date = '') =>
validateNaivePostgresDateString(date)
? undefined
: `Invalid format (${VALIDATION_EXAMPLE_POSTGRES_NAIVE})`;