@application-kit/typeorm
v0.5.0
Published
Курсорный листинг на TypeORM: контракты листинга, пагинатор, фильтры, ORM-база сущности, конфиг и корневой модуль соединения Postgres, SQL-логгер с маской bind-параметров
Maintainers
Readme
@application-kit/typeorm
Курсорный листинг на TypeORM: контракты листинга (пагинатор, сортировка, фильтры), курсорный
пагинатор поверх SelectQueryBuilder, перевод фильтров в where, ORM-база сущности и конфиг
соединения с Postgres из окружения.
Установка
yarn add @application-kit/typeormPeer-зависимости: typeorm, @nestjs/common; для входа /nest — @nestjs/cqrs, @nestjs/typeorm, nestjs-pino,
class-transformer, class-validator, reflect-metadata; для DatabaseLogger входа /logging — nestjs-pino
(объявлены опциональными: корню и /listing они не нужны).
Точки входа
| Вход | Что внутри | Зависимости |
|---|---|---|
| @application-kit/typeorm/listing | FilterInterface, FiltersMatchMode, SortingInterface, SortOrder, PaginatorInterface, ListingRepositoryInterface | нет — годится для браузерного кода |
| @application-kit/typeorm | getListByCursor, CursorPaginator, getWhereByFilters, isUniqueViolation, estimateRowCount, withIdTiebreaker, AbstractOrmEntity, PackageOrmInterface | typeorm, @nestjs/common |
| @application-kit/typeorm/nest | TypeormRootModule, ListingQuery, ListingHandler, DatabaseConfig, DatabaseEnvironment, createCliDataSource | @nestjs/common, @nestjs/cqrs, @nestjs/typeorm, nestjs-pino, class-transformer, class-validator, reflect-metadata |
| @application-kit/typeorm/logging | createTypeormLogger, TypeormQueryLogger, createSqlParameterMask, runAsInfrastructureQuery, isInfrastructureQuery, SqlLoggerInterface, DatabaseLogger | nestjs-pino — createTypeormLogger и DatabaseLogger |
Клиент (Vue, GraphQL-типы) импортирует только /listing: корень тянет TypeORM и Nest. Nest-интеграции (DI-модули, CQRS,
конфиг) — только во входе /nest, корень их не реэкспортирует и @nestjs/typeorm/nestjs-pino не тянет.
Листинг в репозитории
import { getListByCursor } from '@application-kit/typeorm'
import type { ListingRepositoryInterface } from '@application-kit/typeorm/listing'
export class UserRepository implements ListingRepositoryInterface<User> {
public async list(paginator, sorting, filters) {
return getListByCursor(UserEntity, this.repository.createQueryBuilder('user'), toDomain, paginator, filters, sorting)
}
}Неверный курсор страницы — InvalidPageCursorError (BadRequestException, HTTP 400).
Обработчик query
import { ListingHandler, ListingQuery } from '@application-kit/typeorm/nest'
export class GetUsersQuery extends ListingQuery {}
@QueryHandler(GetUsersQuery)
export class GetUsersHandler extends ListingHandler<User> {
public constructor(@Inject(USER_REPOSITORY) repository: UserRepository) {
super(repository)
}
}ORM-база
AbstractOrmEntity — колонки id (uuid), createdAt, updatedAt; OrmEntityProps<E> — свойства
сущности без них. Имя отличается от доменного AbstractEntity из DDD-библиотек намеренно.
Логгер SQL
TypeormQueryLogger — адаптер Logger TypeORM поверх любого pino-совместимого логгера (SqlLoggerInterface:
debug/info/warn/error). SQL приложения пишется на info с bind-параметрами, медленный запрос — warn,
упавший — error с ошибкой под ключом err; SQL из области runAsInfrastructureQuery (пробы health, которые
иначе дают до 97 % потока логов сервиса) уходит на debug.
createTypeormLogger(pinoParams, maskSqlParameters?) — сборка на pino-логгере: TypeormQueryLogger поверх
DatabaseLogger с маской по умолчанию. Одна сборка на корень соединения (TypeormRootModule) и CLI миграций — калибровка
маски у них не разъедется. Маска bind-параметров — аргумент:
import { createSqlParameterMask, createTypeormLogger } from '@application-kit/typeorm/logging'
const maskSqlParameters = createSqlParameterMask({
// Порог усечения строки; по умолчанию 256 — user-agent сессии доезжает целиком, дамп обрезается.
maxValueLength: 256,
// Формы потребителя проверяются раньше форм по умолчанию.
forms: [{ label: 'session-token', pattern: /^[\da-f]{64}$/u }],
})
const logger = createTypeormLogger(pinoParams, maskSqlParameters)Маска смотрит на форму значения, а не на имя колонки (параметры TypeORM позиционные): по умолчанию скрываются
bcrypt-хеши, JWT, hex-хеши от 32 символов, шифротекст AES-GCM из трёх base64-кусков, base32 от 16 символов и
base64 от 40 — на месте значения метка [masked:<label>]; значение длиннее порога — префикс с пометкой
…[truncated:<длина>]. Остальные значения (email, идентификаторы, числа) остаются в логе: по ним разбирают заявки.
Массив параметром обходится поэлементно, объекты (JSONB) — нет. Секрет с формой обычного текста маска не ловит.
DatabaseLogger — отдельный PinoLogger с контекстом DatabaseLogger: контекст живёт в экземпляре, и общий логгер
приложения под SQL занимать нельзя. Область пробы открывает вызывающий:
return runAsInfrastructureQuery(async () => this.health.check(indicators))Соединение с Postgres
DatabaseConfig собирает DataSourceOptions из окружения DB_* (DatabaseEnvironment — класс валидации
class-validator): соединение всегда с верификацией сертификата сервера, statement_timeout сессии, стратегия имён
TypeormPluralNamingStrategy (зависимость пакета) — под неё пишутся сущности и миграции потребителя. entities,
migrations и logger в опциях нет — их добавляет вызывающий.
| Переменная | Назначение |
|---|---|
| DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD | адрес и учётные данные |
| DB_SSL_CA_FILE | путь к CA сервера БД — обязателен во всех окружениях; нечитаемый файл роняет процесс на буте, а не отключает проверку |
| DB_STATEMENT_TIMEOUT_MS | потолок времени SQL-запроса, мс; не задан — без потолка. Задаётся процессам, обслуживающим пользовательские запросы; миграциям, CLI и консьюмерам — нет: у них легитимно долгие запросы |
Регистрацию и валидацию окружения делает приложение — пакет отдаёт классы и корень соединения:
import { DatabaseConfig, DatabaseEnvironment, TypeormRootModule } from '@application-kit/typeorm/nest'
// @libs-for-dev/nestjs-config-module: конфиг и env-класс — провайдеры, валидация на старте
const databaseConfigsModule = ConfigsModule.forFeature([DatabaseConfig], [DatabaseEnvironment])
TypeormRootModule.forRoot({ imports: [databaseConfigsModule] })TypeormRootModule.forRoot({ imports, maskSqlParameters? }) — TypeOrmModule.forRootAsync с опциями DatabaseConfig,
autoLoadEntities (entities пакетов kit регистрирует их собственный TypeOrmModule.forFeature) и логгером
createTypeormLogger. Логгер собирается из параметров глобального LoggerModule nestjs-pino (PARAMS_PROVIDER_TOKEN):
он обязан быть в графе процесса.
createDataSourceOptions({ maxQueryExecutionTime }) — порог медленного запроса для логгера TypeORM, по умолчанию 1000 мс.
CLI миграций
createCliDataSource собирает DataSource для typeorm -d без Nest-контейнера: опции DatabaseConfig, миграции и
entities продукта глобами и таблицы пакетов — их объекты orm (PackageOrmInterface: классы entities и миграций).
import { orm as mailerOrm } from '@application-kit/mailer'
import { orm as outboxOrm } from '@application-kit/transaction-outbox'
import { createCliDataSource, DatabaseConfig, DatabaseEnvironment } from '@application-kit/typeorm/nest'
export default createCliDataSource({
app: { buildDirs: ['modules', 'libs'], sourceDirs: ['src'] },
config: new DatabaseConfig(exposeObjectWithValidation(process.env, DatabaseEnvironment)),
logger: typeormQueryLogger,
packages: [outboxOrm, mailerOrm],
})Каталоги — относительно cwd CLI; внутри каждого миграции ищутся по **/typeorm/migrations/, entities — по
**/typeorm/entities/. sourceDirs — исходники под ts-node: миграции .ts/.js и entities (по ним
migration:generate сравнивает схему). buildDirs — скомпилированный код образа: только миграции .js (рядом лежат
.d.ts), entities для migration:run не нужны. Глоб по корню образа не годится — он нашёл бы миграции пакетов в
node_modules дважды (ESM и CJS), поэтому пакеты отдают классы. logger обязателен: без него CLI пишет дефолтным
логгером TypeORM простым текстом, мимо маски bind-параметров (/logging).
