refactor(time): migrate runtime time handling to Temporal

This commit is contained in:
2026-03-09 07:18:09 +04:00
parent fa8fa7fe23
commit 29f6d788e7
25 changed files with 353 additions and 104 deletions

View File

@@ -1,6 +1,7 @@
import { and, eq, inArray, sql } from 'drizzle-orm'
import { createDbClient, schema } from '@household/db'
import { instantFromDatabaseValue, instantToDate } from '@household/domain'
import type {
AnonymousFeedbackModerationStatus,
AnonymousFeedbackRepository
@@ -49,11 +50,14 @@ export function createDbAnonymousFeedbackRepository(
},
async getRateLimitSnapshot(memberId, acceptedSince) {
const acceptedSinceIso = acceptedSince.toISOString()
const acceptedSinceIso = acceptedSince.toString()
const rows = await db
.select({
acceptedCountSince: sql<string>`count(*) filter (where ${schema.anonymousMessages.createdAt} >= ${acceptedSinceIso}::timestamptz)`,
earliestAcceptedAtSince: sql<
string | Date | null
>`min(${schema.anonymousMessages.createdAt}) filter (where ${schema.anonymousMessages.createdAt} >= ${acceptedSinceIso}::timestamptz)`,
lastAcceptedAt: sql<string | Date | null>`max(${schema.anonymousMessages.createdAt})`
})
.from(schema.anonymousMessages)
@@ -65,16 +69,13 @@ export function createDbAnonymousFeedbackRepository(
)
)
const earliestAcceptedAtSinceRaw = rows[0]?.earliestAcceptedAtSince ?? null
const lastAcceptedAtRaw = rows[0]?.lastAcceptedAt ?? null
return {
acceptedCountSince: Number(rows[0]?.acceptedCountSince ?? '0'),
lastAcceptedAt:
lastAcceptedAtRaw instanceof Date
? lastAcceptedAtRaw
: typeof lastAcceptedAtRaw === 'string'
? new Date(lastAcceptedAtRaw)
: null
earliestAcceptedAtSince: instantFromDatabaseValue(earliestAcceptedAtSinceRaw),
lastAcceptedAt: instantFromDatabaseValue(lastAcceptedAtRaw)
}
},
@@ -146,7 +147,7 @@ export function createDbAnonymousFeedbackRepository(
postedChatId: input.postedChatId,
postedThreadId: input.postedThreadId,
postedMessageId: input.postedMessageId,
postedAt: input.postedAt,
postedAt: instantToDate(input.postedAt),
failureReason: null
})
.where(eq(schema.anonymousMessages.id, input.submissionId))

View File

@@ -1,8 +1,13 @@
import { and, desc, eq, gte, isNotNull, isNull, lte, or, sql } from 'drizzle-orm'
import { and, desc, eq, gte, isNotNull, isNull, lt, lte, or, sql } from 'drizzle-orm'
import { createDbClient, schema } from '@household/db'
import type { FinanceRepository } from '@household/ports'
import type { CurrencyCode } from '@household/domain'
import {
instantFromDatabaseValue,
instantToDate,
nowInstant,
type CurrencyCode
} from '@household/domain'
function toCurrencyCode(raw: string): CurrencyCode {
const normalized = raw.trim().toUpperCase()
@@ -171,7 +176,7 @@ export function createDbFinanceRepository(
await db
.update(schema.billingCycles)
.set({
closedAt
closedAt: instantToDate(closedAt)
})
.where(eq(schema.billingCycles.id, cycleId))
},
@@ -265,7 +270,8 @@ export function createDbFinanceRepository(
return rows.map((row) => ({
...row,
currency: toCurrencyCode(row.currency)
currency: toCurrencyCode(row.currency),
createdAt: instantFromDatabaseValue(row.createdAt)!
}))
},
@@ -284,8 +290,8 @@ export function createDbFinanceRepository(
eq(schema.purchaseMessages.householdId, householdId),
isNotNull(schema.purchaseMessages.senderMemberId),
isNotNull(schema.purchaseMessages.parsedAmountMinor),
gte(schema.purchaseMessages.messageSentAt, start),
lte(schema.purchaseMessages.messageSentAt, end)
gte(schema.purchaseMessages.messageSentAt, instantToDate(start)),
lt(schema.purchaseMessages.messageSentAt, instantToDate(end))
)
)
@@ -294,7 +300,7 @@ export function createDbFinanceRepository(
payerMemberId: row.payerMemberId!,
amountMinor: row.amountMinor!,
description: row.description,
occurredAt: row.occurredAt
occurredAt: instantFromDatabaseValue(row.occurredAt)
}))
},
@@ -316,7 +322,7 @@ export function createDbFinanceRepository(
inputHash: snapshot.inputHash,
totalDueMinor: snapshot.totalDueMinor,
currency: snapshot.currency,
computedAt: new Date(),
computedAt: instantToDate(nowInstant()),
metadata: snapshot.metadata
}
})

View File

@@ -1,6 +1,7 @@
import { and, eq } from 'drizzle-orm'
import { createDbClient, schema } from '@household/db'
import { instantToDate, nowInstant } from '@household/domain'
import {
HOUSEHOLD_TOPIC_ROLES,
type HouseholdConfigurationRepository,
@@ -138,7 +139,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
.set({
telegramChatType: input.telegramChatType,
title: nextTitle,
updatedAt: new Date()
updatedAt: instantToDate(nowInstant())
})
.where(eq(schema.householdTelegramChats.telegramChatId, input.telegramChatId))
@@ -256,7 +257,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
set: {
telegramThreadId: input.telegramThreadId,
topicName: input.topicName?.trim() || null,
updatedAt: new Date()
updatedAt: instantToDate(nowInstant())
}
})
.returning({
@@ -348,7 +349,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
set: {
token: input.token,
createdByTelegramUserId: input.createdByTelegramUserId ?? null,
updatedAt: new Date()
updatedAt: instantToDate(nowInstant())
}
})
.returning({
@@ -448,7 +449,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
displayName: input.displayName,
username: input.username?.trim() || null,
languageCode: input.languageCode?.trim() || null,
updatedAt: new Date()
updatedAt: instantToDate(nowInstant())
}
})
.returning({

View File

@@ -1,6 +1,7 @@
import { and, eq } from 'drizzle-orm'
import { createDbClient, schema } from '@household/db'
import { instantFromDatabaseValue, instantToDate, nowInstant, Temporal } from '@household/domain'
import type {
TelegramPendingActionRecord,
TelegramPendingActionRepository,
@@ -20,7 +21,7 @@ function mapPendingAction(row: {
telegramChatId: string
action: string
payload: unknown
expiresAt: Date | null
expiresAt: Date | string | null
}): TelegramPendingActionRecord {
return {
telegramUserId: row.telegramUserId,
@@ -30,7 +31,7 @@ function mapPendingAction(row: {
row.payload && typeof row.payload === 'object' && !Array.isArray(row.payload)
? (row.payload as Record<string, unknown>)
: {},
expiresAt: row.expiresAt
expiresAt: instantFromDatabaseValue(row.expiresAt)
}
}
@@ -52,8 +53,8 @@ export function createDbTelegramPendingActionRepository(databaseUrl: string): {
telegramChatId: input.telegramChatId,
action: input.action,
payload: input.payload,
expiresAt: input.expiresAt,
updatedAt: new Date()
expiresAt: input.expiresAt ? instantToDate(input.expiresAt) : null,
updatedAt: instantToDate(nowInstant())
})
.onConflictDoUpdate({
target: [
@@ -63,8 +64,8 @@ export function createDbTelegramPendingActionRepository(databaseUrl: string): {
set: {
action: input.action,
payload: input.payload,
expiresAt: input.expiresAt,
updatedAt: new Date()
expiresAt: input.expiresAt ? instantToDate(input.expiresAt) : null,
updatedAt: instantToDate(nowInstant())
}
})
.returning({
@@ -84,7 +85,7 @@ export function createDbTelegramPendingActionRepository(databaseUrl: string): {
},
async getPendingAction(telegramChatId, telegramUserId) {
const now = new Date()
const now = nowInstant()
const rows = await db
.select({
telegramUserId: schema.telegramPendingActions.telegramUserId,
@@ -107,7 +108,8 @@ export function createDbTelegramPendingActionRepository(databaseUrl: string): {
return null
}
if (row.expiresAt && row.expiresAt.getTime() <= now.getTime()) {
const expiresAt = instantFromDatabaseValue(row.expiresAt)
if (expiresAt && Temporal.Instant.compare(expiresAt, now) <= 0) {
await db
.delete(schema.telegramPendingActions)
.where(
@@ -120,7 +122,16 @@ export function createDbTelegramPendingActionRepository(databaseUrl: string): {
return null
}
return mapPendingAction(row)
return {
telegramUserId: row.telegramUserId,
telegramChatId: row.telegramChatId,
action: parsePendingActionType(row.action),
payload:
row.payload && typeof row.payload === 'object' && !Array.isArray(row.payload)
? (row.payload as Record<string, unknown>)
: {},
expiresAt
}
},
async clearPendingAction(telegramChatId, telegramUserId) {