mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
feat(db): DbTimestampType keeps DATETIME columns as the CURRENT_TIMESTAMP text
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { Type, type EntityProperty, type Platform, type TransformContext } from '@mikro-orm/core';
|
||||
|
||||
/** The text every DATETIME column holds and every API response emits. */
|
||||
export const DB_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
|
||||
|
||||
/**
|
||||
* `CURRENT_TIMESTAMP` as SQLite renders it, produced in JS so it is the same on
|
||||
* every dialect: UTC, seconds precision, a space between date and time.
|
||||
*/
|
||||
export function dbNow(now: Date = new Date()): string {
|
||||
return now.toISOString().slice(0, 19).replace('T', ' ');
|
||||
}
|
||||
|
||||
type TimestampValue = string | null | undefined;
|
||||
|
||||
/**
|
||||
* A DATETIME column whose JS value is the stored text, not a `Date`.
|
||||
*
|
||||
* The stock `DateTimeType` maps to `Date`, and on SQLite the platform then
|
||||
* writes `+date` — an integer of milliseconds — next to the `'YYYY-MM-DD
|
||||
* HH:MM:SS'` text the schema's `DEFAULT CURRENT_TIMESTAMP` produces and every
|
||||
* consumer (the client, `ORDER BY created_at`, `date(created_at)`) expects.
|
||||
* This type keeps the wire format the app has always had and stays dialect
|
||||
* neutral: a driver that returns a `Date` (Postgres) is formatted to the same
|
||||
* text on the way in, and text is written as text on the way out.
|
||||
*/
|
||||
export class DbTimestampType extends Type<TimestampValue, TimestampValue> {
|
||||
override convertToDatabaseValue(value: TimestampValue, platform: Platform, context?: TransformContext): TimestampValue {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
// At runtime, the ORM may pass a Date; handle it
|
||||
if ((value as unknown) instanceof Date) {
|
||||
return dbNow(value as unknown as Date);
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
// Default: convert to string
|
||||
return String(value);
|
||||
}
|
||||
|
||||
override convertToJSValue(value: TimestampValue, platform: Platform, context?: TransformContext): TimestampValue {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
// At runtime, database drivers may return a Date; handle it
|
||||
if ((value as unknown) instanceof Date) {
|
||||
return dbNow(value as unknown as Date);
|
||||
}
|
||||
// Handle millisecond integers (legacy format)
|
||||
if (typeof value === 'number' || (value as unknown) instanceof Number) {
|
||||
const numValue = typeof value === 'number' ? value : (value as unknown as number);
|
||||
return dbNow(new Date(numValue));
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
// Default: convert to string
|
||||
return String(value);
|
||||
}
|
||||
|
||||
override getColumnType(prop: EntityProperty, platform: Platform): string {
|
||||
return platform.getDateTimeTypeDeclarationSQL({ length: prop.length });
|
||||
}
|
||||
|
||||
override compareAsType(): string {
|
||||
return 'string';
|
||||
}
|
||||
|
||||
override get runtimeType(): string {
|
||||
return 'string';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { DB_TIMESTAMP_RE, DbTimestampType, dbNow } from './db-timestamp.type';
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { DB_TIMESTAMP_RE, DbTimestampType, dbNow } from '../../../../src/db/types';
|
||||
import type { Platform } from '@mikro-orm/core';
|
||||
|
||||
// The type never touches the platform for value conversion; a stub is enough.
|
||||
const platform = {} as Platform;
|
||||
const type = new DbTimestampType();
|
||||
|
||||
describe('dbNow', () => {
|
||||
it('TS-001: formats a Date as the SQLite CURRENT_TIMESTAMP text, in UTC', () => {
|
||||
expect(dbNow(new Date(Date.UTC(2026, 8, 21, 13, 5, 9, 123)))).toBe('2026-09-21 13:05:09');
|
||||
});
|
||||
|
||||
it('TS-002: the default is now and matches the wire format', () => {
|
||||
expect(dbNow()).toMatch(DB_TIMESTAMP_RE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DbTimestampType', () => {
|
||||
it('TS-003: passes stored text through unchanged on read', () => {
|
||||
expect(type.convertToJSValue('2026-09-21 13:05:09', platform)).toBe('2026-09-21 13:05:09');
|
||||
});
|
||||
|
||||
it('TS-004: formats a Date a driver hands back (Postgres later) to the same text', () => {
|
||||
expect(type.convertToJSValue(new Date(Date.UTC(2026, 8, 21, 13, 5, 9)) as unknown as string, platform))
|
||||
.toBe('2026-09-21 13:05:09');
|
||||
});
|
||||
|
||||
it('TS-005: a millisecond integer (a Date written by the stock DateTimeType) reads back as text', () => {
|
||||
expect(type.convertToJSValue(Date.UTC(2026, 8, 21, 13, 5, 9) as unknown as string, platform))
|
||||
.toBe('2026-09-21 13:05:09');
|
||||
});
|
||||
|
||||
it('TS-006: writes text unchanged and a stray Date as text, never as a number', () => {
|
||||
expect(type.convertToDatabaseValue('2026-09-21 13:05:09', platform)).toBe('2026-09-21 13:05:09');
|
||||
expect(type.convertToDatabaseValue(new Date(Date.UTC(2026, 8, 21, 13, 5, 9)) as unknown as string, platform))
|
||||
.toBe('2026-09-21 13:05:09');
|
||||
});
|
||||
|
||||
it('TS-007: null and undefined survive both directions', () => {
|
||||
expect(type.convertToDatabaseValue(null, platform)).toBeNull();
|
||||
expect(type.convertToDatabaseValue(undefined, platform)).toBeUndefined();
|
||||
expect(type.convertToJSValue(null, platform)).toBeNull();
|
||||
});
|
||||
|
||||
it('TS-008: compares as a string and reports a string runtime type', () => {
|
||||
expect(type.compareAsType()).toBe('string');
|
||||
expect(type.runtimeType).toBe('string');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user