Use moment-timezone in parseEventDatetime so naive America/Asuncion wall-clock times convert correctly regardless of the host Node tz database. Also normalize user registration date filters with toDbDate for Postgres. Co-authored-by: Cursor <cursoragent@cursor.com>
131 lines
3.9 KiB
TypeScript
131 lines
3.9 KiB
TypeScript
import { nanoid } from 'nanoid';
|
|
import { randomUUID } from 'crypto';
|
|
import moment from 'moment-timezone';
|
|
|
|
/**
|
|
* Get database type (reads env var each time to handle module loading order)
|
|
*/
|
|
function getDbType(): string {
|
|
return process.env.DB_TYPE || 'sqlite';
|
|
}
|
|
|
|
/**
|
|
* Generate a unique ID appropriate for the database type.
|
|
* - SQLite: returns nanoid (21-char alphanumeric)
|
|
* - PostgreSQL: returns UUID v4
|
|
*/
|
|
export function generateId(): string {
|
|
return getDbType() === 'postgres' ? randomUUID() : nanoid(21);
|
|
}
|
|
|
|
export function generateTicketCode(): string {
|
|
return `TKT-${nanoid(8).toUpperCase()}`;
|
|
}
|
|
|
|
/**
|
|
* Get current timestamp in the format appropriate for the database type.
|
|
* - SQLite: returns ISO string
|
|
* - PostgreSQL: returns Date object
|
|
*/
|
|
export function getNow(): string | Date {
|
|
const now = new Date();
|
|
return getDbType() === 'postgres' ? now : now.toISOString();
|
|
}
|
|
|
|
/**
|
|
* Convert a date value to the appropriate format for the database type.
|
|
* - SQLite: returns ISO string
|
|
* - PostgreSQL: returns Date object
|
|
*/
|
|
export function toDbDate(date: Date | string): string | Date {
|
|
const d = typeof date === 'string' ? new Date(date) : date;
|
|
return getDbType() === 'postgres' ? d : d.toISOString();
|
|
}
|
|
|
|
const NAIVE_DATETIME_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/;
|
|
|
|
/**
|
|
* Parse a datetime string that represents wall-clock time in a given timezone
|
|
* and return the corresponding UTC Date.
|
|
*
|
|
* Naive strings (no "Z" or offset) are interpreted as wall-clock time in
|
|
* `timezone`. Strings that already carry a timezone indicator are parsed
|
|
* directly so existing UTC ISO values still work.
|
|
*/
|
|
export function parseEventDatetime(
|
|
datetime: string,
|
|
timezone: string = 'America/Asuncion',
|
|
): Date {
|
|
if (!NAIVE_DATETIME_RE.test(datetime)) {
|
|
return new Date(datetime);
|
|
}
|
|
|
|
// Interpret the wall-clock digits as local time in `timezone` using
|
|
// moment-timezone's bundled IANA data. This keeps the conversion correct
|
|
// regardless of the host Node runtime's (possibly stale) tz database.
|
|
return moment.tz(datetime, timezone).toDate();
|
|
}
|
|
|
|
/**
|
|
* Convert a datetime string to the appropriate DB format, interpreting naive
|
|
* strings as wall-clock time in `timezone` (defaults to America/Asuncion).
|
|
*/
|
|
export function toDbDateTz(
|
|
datetime: string,
|
|
timezone: string = 'America/Asuncion',
|
|
): string | Date {
|
|
const d = parseEventDatetime(datetime, timezone);
|
|
return getDbType() === 'postgres' ? d : d.toISOString();
|
|
}
|
|
|
|
/**
|
|
* Convert a boolean value to the appropriate format for the database type.
|
|
* - SQLite: returns boolean (true/false) for mode: 'boolean'
|
|
* - PostgreSQL: returns integer (1/0) for pgInteger columns
|
|
*/
|
|
export function toDbBool(value: boolean): boolean | number {
|
|
return getDbType() === 'postgres' ? (value ? 1 : 0) : value;
|
|
}
|
|
|
|
/**
|
|
* Convert all boolean values in an object to the appropriate database format.
|
|
* Useful for converting request data before database insert/update.
|
|
*/
|
|
export function convertBooleansForDb<T extends Record<string, any>>(obj: T): T {
|
|
if (getDbType() !== 'postgres') {
|
|
return obj; // SQLite handles booleans automatically
|
|
}
|
|
|
|
const result = { ...obj };
|
|
for (const key in result) {
|
|
if (typeof result[key] === 'boolean') {
|
|
(result as any)[key] = result[key] ? 1 : 0;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function formatCurrency(amount: number, currency: string = 'PYG'): string {
|
|
return new Intl.NumberFormat('es-PY', {
|
|
style: 'currency',
|
|
currency,
|
|
}).format(amount);
|
|
}
|
|
|
|
export function calculateAvailableSeats(capacity: number, bookedCount: number): number {
|
|
return Math.max(0, capacity - bookedCount);
|
|
}
|
|
|
|
export function isEventSoldOut(capacity: number, bookedCount: number): boolean {
|
|
return bookedCount >= capacity;
|
|
}
|
|
|
|
export function sanitizeHtml(str: string): string {
|
|
return str
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|