PostgreSQL и жизненный цикл пула
До сих пор серверный граф можно было понимать без конкретного хранения. Теперь введём PostgreSQL 18 и node-postgres pg. Database владеет одним Pool, а SQL задаёт таблицы tickets и ticket_events. Результат главы — определённая схема и lifecycle соединений, которые repository сможет использовать для чтения и атомарной записи. Мы не создаём базу автоматически и не выполняем SQL: архив описывает самостоятельные действия читателя для отдельной локальной среды.
Pool управляет набором подключений к PostgreSQL. Приложение берёт доступный client для операции и освобождает его после использования. Это отличается от создания соединения на каждый HTTP-запрос и от хранения одного вечного client для всех транзакций. Настройки max5, connectionTimeoutMillis3000 и idleTimeoutMillis10000 выбраны как понятный профиль лаборатории. Они не являются рассчитанной production-ёмкостью: нагрузка, число процессов и серверные лимиты в этой сессии не измерялись.
Владелец ресурса
Ниже полный src/database.ts. ConfigService получает DATABASE_URL из проверенного профиля, а Database создаёт Pool в constructor. OnModuleDestroy завершает его через end. DatabaseModule экспортирует этот provider для TicketsModule. Обработчик pool error сообщает об неожиданном отказе свободного подключения без вывода строки соединения. Ошибки конкретного query всё ещё поступают вызывающему repository и далее в error filter; отдельный callback их не превращает в успех.
import { Injectable, Module, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import pg from 'pg';
@Injectable()
export class Database implements OnModuleDestroy {
readonly pool: pg.Pool;
constructor(config: ConfigService) {
this.pool = new pg.Pool({connectionString:config.getOrThrow<string>('DATABASE_URL'), max:5, connectionTimeoutMillis:3000, idleTimeoutMillis:10000});
this.pool.on('error', () => console.error('Неожиданная ошибка свободного подключения PostgreSQL'));
}
async onModuleDestroy() { await this.pool.end(); }
}
@Module({providers:[Database],exports:[Database]})
export class DatabaseModule {}
Ожидается один pool на созданный Database-provider, а не отдельный pool на каждом вызове get. EnableShutdownHooks из main позволяет lifecycle завершиться при предусмотренной остановке Nest. Если процесс аварийно завершён без обычного lifecycle, нельзя обещать выполнение любого finally или hook. Сервер базы также освобождает разорванные подключения по своим правилам. Лаборатория показывает корректное намерение управления ресурсом, но не моделирует все аварийные сценарии ОС, сети и контейнера.
DATABASE_URL содержит адрес, пользователя, пароль и базу. README предлагает подготовить отдельную dispatcher_lab, не рабочий набор данных. .env.example показывает вымышленные credentials, которые читатель заменяет своей локальной настройкой. Psql не читает .env автоматически, поэтому команды в README объясняют необходимость явно передать или экспортировать строку подключения. Такая подробность полезнее, чем команда, которая выглядит воспроизводимой, но незаметно рассчитывает на неизвестное окружение shell.
Ограничения схемы
Tickets имеет primary key id, обязательный title, допустимые status, положительный integer version и timestamptz updated_at. Check constraints защищают данные независимо от Nest DTO: если другой SQL-клиент пишет неверный статус, база также отклонит его. Это не заменяет transport validation, которая даёт ранний понятный 400. У двух уровней разные задачи: DTO проверяет внешнее намерение, схема удерживает состояние таблицы, даже когда запись приходит другим путём.
Ticket_events содержит generated identity event_id, ссылку на ticket_id, actor_id, новую version, title/status и created_at. Unique(ticket_id,version) запрещает два события одной подтверждённой версии. Seed создаёт исходные tickets, но не выдаёт их появление за историю реального изменения пользователя. Даты вымышлены и совпадают с клиентским fixture. После PATCH одна транзакция добавит событие следующей версии; историю можно читать SQL самостоятельно, однако отдельного HTTP-route её выдачи в архиве нет.
Primary key не разрешает два rq-101 даже с разными titles. Foreign key не разрешает журналу ссылаться на отсутствующую заявку. Check status запрещает archived, хотя внешняя страница могла бы содержать такое слово в query. Эти ограничения удерживают конкретные инварианты и помогают понять ошибку состояния. Но база не знает, какой публичный demo token разрешён: actor уже определён guard. Не приписывайте constraints способность заменить всю серверную авторизацию и предметный договор доступа.
Выполнение SQL читателем
001_schema.sql обёрнут в BEGIN/COMMIT и создаёт две таблицы. 002_seed.sql отдельно вводит три записи. Они не идемпотентны: повтор schema или seed не является безопасным «обновить всё». README предлагает применять их один раз к подготовленной лабораторной базе. Reset-скрипт не предоставлен, поскольку удаление данных должно быть отдельным осознанным действием. В этой сессии ни psql, ни создание роли/базы, ни migration tool не запускались, а сам архив не называется применённой миграцией.
Timestamptz хранит временной момент с правилами PostgreSQL, а pg обычно возвращает его как Date. Repository преобразует его в toISOString для wire-строки. Поэтому формат JSON не зависит от того, как psql показывает локальный часовой пояс сессии. Исходные данные заданы UTC. При настоящей работе разные представления одного момента не должны создавать новую version или ошибку сравнения. Клиентская модель времени остаётся строковой, а UI лаборатории не добавляет локализованный календарный редактор.
Положительная version ограничена SQL integer. DTO допускает вход до 2147483646, чтобы увеличение поместилось в следующий integer. На практике исчерпание такого счётчика требует отдельной долгосрочной политики, а не автоматического обнуления. Обнуление нарушило бы смысл старой основы. Лаборатория не моделирует миллиарды изменений, но ограничение полезно увидеть: тип хранения определяет техническую границу, которую нельзя забыть при проектировании mutation-протокола и проверке JSON number.
Для самостоятельного наблюдения читатель может применить schema/seed и запросить SELECT id,status,version FROM tickets ORDER BY id. Ожидаются new v1, active v2 и done v3 у соответствующих идентификаторов. Затем неверный status должен быть отклонён check constraint. Эти результаты здесь не получены. Они задают конкретный способ проверить инварианты до подключения HTTP и помогают отличить проблему подготовки базы от неверного token или повреждённого response JSON.
Следующая глава напишет repository mapping и параметризованные SELECT. Database останется владельцем ресурса и не получит предметных методов «взять заявку». Такое направление позволяет развивать SQL отдельно от pool lifecycle. Код, схема и ожидаемые состояния согласованы в одной конечной версии, а реальное разрешение pg-пакета, соединение PostgreSQL 18 и shutdown требуют самостоятельного воспроизведения по README.
Статус примера — not_run. Зависимости, приложение, SQL, HTTP и тесты не запускались. Описанные ответы и изменения интерфейса ожидаемые; чтение документации не является проверкой выполнением. Полная конечная версия находится в архиве исходников, а её ограничения и действия читателя — в README и CONTRACT.md.
Перейдите к оглавлению серии или общему маршруту интерактивной веб-разработки. Механизм этой главы сверяется с официальным первичным источником; конкретные версии и непроверенные части перечислены в договоре курса.