Skip to content

Трансформери ​

Трансформер — це чиста функція серіалізації: вона бере рядок, який повернув репозиторій (з bigint id, об'єктами Date і внутрішніми полями), і перетворює його на публічний DTO, який можна безпечно віддати клієнту в JSON.

Навіщо потрібен цей шар ​

  • Безпека даних. Рядок users містить хеш пароля і sessionToken. Трансформер явно відкидає такі поля, тож вони не потраплять у відповідь навіть якщо сервіс поверне «весь користувач». Без цього шару легко випадково віддати клієнту зайве.
  • JSON-сумісність. bigint не серіалізується JSON.stringify, а Date треба віддавати в єдиному форматі. Трансформер приводить id до string, а дати — до ISO-рядка UTC (toISOString()). Схема відповіді описує вже цей формат — див. Перевірка відповідей.
  • Стабільний публічний контракт. Внутрішні назви колонок (createdAt) можуть змінюватися, а клієнт очікує created_at. Трансформер — одне місце, де визначається, як сутність виглядає зовні; похідні поля (emailVerified: boolean замість emailVerifiedAt) теж додаються тут.
  • Повторне використання. Один і той самий користувач повертається з логіну, реєстрації, OAuth — усі вони викликають userTransformer.serialize, тож форма відповіді однакова.
  • Легко тестувати. Трансформер не має залежностей і побічних ефектів: вхідний рядок → вихідний об'єкт.

Як влаштовані ​

Трансформери в app/transformers/ — це об'єкти з методами serialize / serializeArray (у push-підписок ще serializeWithLogs / serializeArrayWithLogs, у логів push — serializeSummary). Є userTransformer, pushSubscriptionTransformer, pushNotificationLogTransformer і promptTransformer. Вони:

  • приводять bigint id до string;
  • переводять Date у ISO-рядок UTC через toISOString() (формат 2026-10-25T10:00:00.000Z не залежить від часового поясу процесу); userTransformer і pushSubscriptionTransformer ще перейменовують createdAt/updatedAt → created_at/updated_at;
  • прибирають чутливі поля: user-transformer.ts відкидає password і sessionToken (простір імен сесій у Redis, див. Автентифікація) і додає похідне emailVerified: boolean.
ts
serialize(user: UserRow): SerializedUser {
    const { id, password: _password, sessionToken: _sessionToken, createdAt, updatedAt, emailVerifiedAt, ...rest } = user
    return { ...rest, id: String(id), created_at: createdAt.toISOString(), /* ... */ }
}

Тип результату (SerializedUser, SerializedPushSubscription) виводиться з типу рядка через Omit<...> & {...}, тож він живе в бекенді й не імпортується з packages/shared. Схеми відповідей у packages/shared/src/responses описані окремо (ArkType). Відповідність DTO і схеми перевіряє компілятор — defineRoute звіряє результат контролера зі схемою (Тип відповіді хендлера) — і перевірка відповіді під час виконання (Перевірка відповідей).

Де використовуються ​

Трансформери викликаються в сервісах: userTransformer — у auth-service.ts; pushSubscriptionTransformer — у push-subscription-service.ts і push-subscription-statistics-service.ts.

pushNotificationLogTransformer — у push-subscription-service.ts і всередині pushSubscriptionTransformer.serializeWithLogs; promptTransformer — у prompt-controller.ts (сервіс prompts кешує рядки БД).

Обхід: mainService.init збирає об'єкт користувача вручну (див. «Архітектура»); адмінські списки, knowledge base, support- та online-історія серіалізують рядки у своїх сервісах без окремого трансформера.

Тестування ​

Трансформери — чисті функції, тож їх тестують без моків: user-transformer.spec.ts перевіряє, що password і sessionToken не потрапляють у DTO. У тестах сервісів трансформери мокають через barrel:

ts
vi.mock('#app/transformers/index.js', () => ({
    pushSubscriptionTransformer: pushSubscriptionTransformerMock,
}))

Суміжні шари ​

Загальна схема — Архітектура, покроковий приклад — Додавання фічі.

Автор: Олександр Блажейко · blazheiko@gmail.com