From 291e3ebd2c313a6903aced0bd3d38fc1084a94a2 Mon Sep 17 00:00:00 2001 From: soorq Date: Mon, 11 May 2026 23:57:51 +0300 Subject: [PATCH 1/4] refactor: database module to class definition, implement to other services --- libs/bootstrap/src/configs/throttler.ts | 3 +- .../{database.constants.ts => constants.ts} | 1 - .../src/database.module-definition.ts | 16 ++ libs/database/src/database.module.ts | 179 ++++-------------- libs/database/src/index.ts | 4 +- .../interfaces/database-module.interface.ts | 34 ---- libs/database/src/interfaces/index.ts | 2 +- .../src/interfaces/module.interface.ts | 49 +++++ libs/database/src/migration.service.ts | 16 +- 9 files changed, 119 insertions(+), 185 deletions(-) rename libs/database/src/{database.constants.ts => constants.ts} (50%) create mode 100644 libs/database/src/database.module-definition.ts delete mode 100644 libs/database/src/interfaces/database-module.interface.ts create mode 100644 libs/database/src/interfaces/module.interface.ts diff --git a/libs/bootstrap/src/configs/throttler.ts b/libs/bootstrap/src/configs/throttler.ts index 64d1d19..95532f9 100644 --- a/libs/bootstrap/src/configs/throttler.ts +++ b/libs/bootstrap/src/configs/throttler.ts @@ -1,5 +1,4 @@ -import * as dotenv from 'dotenv'; -dotenv.config(); +import 'dotenv/config'; import type { ThrottlerModuleOptions } from '@nestjs/throttler'; export const DEFAULT_THROTTLER_OPTIONS: ThrottlerModuleOptions = [ diff --git a/libs/database/src/database.constants.ts b/libs/database/src/constants.ts similarity index 50% rename from libs/database/src/database.constants.ts rename to libs/database/src/constants.ts index a6109aa..56b4814 100644 --- a/libs/database/src/database.constants.ts +++ b/libs/database/src/constants.ts @@ -1,2 +1 @@ -export const DATABASE_OPTIONS = 'DATABASE_OPTIONS'; export const DATABASE_SERVICE = 'DATABASE_SERVICE'; diff --git a/libs/database/src/database.module-definition.ts b/libs/database/src/database.module-definition.ts new file mode 100644 index 0000000..a9cbc33 --- /dev/null +++ b/libs/database/src/database.module-definition.ts @@ -0,0 +1,16 @@ +import { ConfigurableModuleBuilder } from '@nestjs/common'; +import type { DatabaseModuleOptions } from './interfaces'; + +export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = + new ConfigurableModuleBuilder() + .setClassMethodName('register') + .setExtras( + { + global: false, + }, + (definition, extras) => ({ + ...definition, + global: extras.global, + }), + ) + .build(); diff --git a/libs/database/src/database.module.ts b/libs/database/src/database.module.ts index f3bfa4d..2b3b36a 100644 --- a/libs/database/src/database.module.ts +++ b/libs/database/src/database.module.ts @@ -1,162 +1,65 @@ -import { - type DynamicModule, - Logger, - Module, - OnApplicationShutdown, - type Provider, - type Type, -} from '@nestjs/common'; +import { Logger, Module, OnApplicationShutdown } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { drizzle } from 'drizzle-orm/node-postgres'; -import { Pool } from 'pg'; -import { DATABASE_OPTIONS, DATABASE_SERVICE } from './database.constants'; -import type { - DatabaseModuleAsyncOptions, - DatabaseModuleOptions, - DatabaseModuleOptionsFactory, -} from './interfaces'; +import { DATABASE_SERVICE } from './constants'; import { MigrationService } from './migration.service'; +import { Pool } from 'pg'; +import { + ConfigurableModuleClass, + MODULE_OPTIONS_TOKEN, + OPTIONS_TYPE, +} from './database.module-definition'; @Module({ - providers: [], -}) -export class DatabaseModule implements OnApplicationShutdown { - private static logger = new Logger(DatabaseModule.name); - - private static pool: Pool; - - static register(config: DatabaseModuleOptions): DynamicModule { - return { - module: DatabaseModule, - global: config.global ?? false, - providers: [ - this.createOptionsProvider(config), - this.createDatabaseProvider(), - MigrationService, - ], - exports: [DATABASE_SERVICE], - }; - } - - static registerAsync(config: DatabaseModuleAsyncOptions): DynamicModule { - return { - module: DatabaseModule, - global: config.global ?? false, - imports: config.imports ?? [], - providers: [ - ...this.createAsyncProviders(config), - this.createDatabaseProvider(), - MigrationService, - ], - exports: [DATABASE_SERVICE], - }; - } - - private static createOptionsProvider(options: DatabaseModuleOptions): Provider { - return { - provide: DATABASE_OPTIONS, - useValue: options, - }; - } - - private static createDatabaseProvider(): Provider { - return { - provide: DATABASE_SERVICE, - useFactory: async (cfg: ConfigService, opts: DatabaseModuleOptions) => { - const baseUrl = cfg.get('DATABASE_URL'); + providers: [ + MigrationService, + { + provide: Pool, + inject: [ConfigService, MODULE_OPTIONS_TOKEN], + useFactory: (configService: ConfigService, opts: typeof OPTIONS_TYPE) => { + const baseUrl = configService.getOrThrow('DATABASE_URL'); const url = new URL(baseUrl); - url.searchParams.set('options', `-c search_path=${opts.schemaName || 'public'}`); - const pool = new Pool({ - connectionString: url.toString(), - max: 20, - min: 2, - connectionTimeoutMillis: 2000, - idleTimeoutMillis: 10000, - maxUses: 5000, - keepAlive: true, - }); + if (opts.schemaName) { + url.searchParams.set('options', `-c search_path=${opts.schemaName}`); + } - pool.on('error', (err) => { - DatabaseModule.logger.error('Database pool connection lost or reset', err); - }); - - this.pool = pool; + return new Pool(url.toString()); + }, + }, + { + provide: DATABASE_SERVICE, + inject: [Pool, MODULE_OPTIONS_TOKEN], + useFactory: (pool: Pool, opts: typeof OPTIONS_TYPE) => { + const logger = new Logger('Drizzle'); return drizzle(pool, { schema: opts.schema, logger: opts.logging ? { logQuery(query, params) { - const start = Date.now(); - DatabaseModule.logger.debug(`SQL: ${query}`); - - if (params?.length) { - DatabaseModule.logger.debug( - `Params: ${JSON.stringify(params)}`, - ); - } - - const duration = Date.now() - start; - DatabaseModule.logger.debug(`Execution time: ${duration}ms`); + logger.debug(`SQL: ${query}`); + if (params?.length) + logger.debug(`Params: ${JSON.stringify(params)}`); }, } : false, }); }, - inject: [ConfigService, DATABASE_OPTIONS], - }; - } - - private static createAsyncProviders(options: DatabaseModuleAsyncOptions): Provider[] { - if (options.useFactory) { - return [ - { - provide: DATABASE_OPTIONS, - useFactory: options.useFactory, - inject: options.inject || [], - }, - ...(options.extraProviders || []), - ]; - } - - const providers: Provider[] = []; - - const useClass = options.useClass || options.useExisting; - if (!useClass) { - throw new Error( - 'You must provide either useClass, useExisting or useFactory in DatabaseModuleAsyncOptions', - ); - } - - providers.push(this.createAsyncOptionsProvider(useClass)); - - if (options.useClass) { - providers.push({ provide: useClass, useClass }); - } - - if (options.extraProviders) { - providers.push(...options.extraProviders); - } - - return providers; - } + }, + ], + exports: [DATABASE_SERVICE], +}) +export class DatabaseModule extends ConfigurableModuleClass implements OnApplicationShutdown { + private readonly logger = new Logger(DatabaseModule.name); - private static createAsyncOptionsProvider( - useClass: Type, - ): Provider { - return { - provide: DATABASE_OPTIONS, - useFactory: async (optionsFactory: DatabaseModuleOptionsFactory) => - optionsFactory.createDatabaseOptions(), - inject: [useClass], - }; + constructor() { + // @Inject(DATABASE_SERVICE) + // private readonly db: DatabaseService, + super(); } - async onApplicationShutdown(_signal?: string) { - if (DatabaseModule.pool) { - DatabaseModule.logger.log('Closing database connections...'); - await DatabaseModule.pool.end(); - } + async onApplicationShutdown() { + this.logger.log('Closing database connections...'); } } diff --git a/libs/database/src/index.ts b/libs/database/src/index.ts index f6e9f8f..ce3920e 100644 --- a/libs/database/src/index.ts +++ b/libs/database/src/index.ts @@ -1,3 +1,3 @@ export * from './database.module'; -export * from './database.constants'; -export type { DatabaseService } from './interfaces/database-module.interface'; +export * from './constants'; +export type { DatabaseService } from './interfaces'; diff --git a/libs/database/src/interfaces/database-module.interface.ts b/libs/database/src/interfaces/database-module.interface.ts deleted file mode 100644 index 5320f61..0000000 --- a/libs/database/src/interfaces/database-module.interface.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { FactoryProvider, ModuleMetadata, Provider, Type } from '@nestjs/common'; -import type { NodePgDatabase } from 'drizzle-orm/node-postgres'; - -export interface DatabaseModuleOptions { - schemaName: string; - schema: Record; - logging?: boolean; - global?: boolean; - /** - * Запускать миграции автоматически при инициализации модуля - * @default true - */ - runMigrations?: boolean; -} - -export interface DatabaseModuleOptionsFactory { - createDatabaseOptions(): Promise | DatabaseModuleOptions; -} - -export interface DatabaseModuleAsyncOptions extends Pick< - ModuleMetadata, - 'imports' -> { - useExisting?: Type; - useClass?: Type; - useFactory?: ( - ...args: TArgs - ) => Promise> | Omit; - inject?: FactoryProvider['inject']; - global?: boolean; - extraProviders?: Provider[]; -} - -export type DatabaseService> = NodePgDatabase; diff --git a/libs/database/src/interfaces/index.ts b/libs/database/src/interfaces/index.ts index 9d0a95e..5e3751a 100644 --- a/libs/database/src/interfaces/index.ts +++ b/libs/database/src/interfaces/index.ts @@ -1 +1 @@ -export * from './database-module.interface'; +export * from './module.interface'; diff --git a/libs/database/src/interfaces/module.interface.ts b/libs/database/src/interfaces/module.interface.ts new file mode 100644 index 0000000..c46219f --- /dev/null +++ b/libs/database/src/interfaces/module.interface.ts @@ -0,0 +1,49 @@ +import type { NodePgDatabase } from 'drizzle-orm/node-postgres'; + +export interface DatabaseModuleOptions { + /** + * Схема базы данных PostgreSQL (устанавливает `search_path`). + * * Все запросы без явного указания схемы будут выполняться в этом пространстве имен. + * @default 'public' + * @example 'auth_service' + */ + schemaName?: string; + + /** + * Объект схемы Drizzle, содержащий определения таблиц и связей. + * * Рекомендуется импортировать целиком: `import * as schema from './schema'`. + * @example schema + */ + schema: Record; + + /** + * Включение или выключение логирования SQL-запросов в консоль через NestJS Logger. + * @default false + */ + logging?: boolean; + + /** + * Флаг для автоматического запуска миграций при старте приложения. + * * Полезно для локальной разработки и стейджинга. + * @default true + */ + runMigrations?: boolean; + + /** + * Абсолютный путь к директории с файлами миграций (SQL или JS/TS). + * * Если не указано, используется путь `./migrations` от корня проекта. + * @default path.resolve(process.cwd(), 'migrations') + */ + migrationsPath?: string; +} + +/** + * Основной тип сервиса базы данных для инъекции в репозитории. + * * Включает в себя типизированный API Drizzle и прямой доступ к драйверу через `$client`. + * * @template T - Тип вашей схемы данных (например, `typeof schema`). + * * @example + * constructor( + * @Inject(DATABASE_SERVICE) private readonly db: DatabaseService + * ) {} + */ +export type DatabaseService> = NodePgDatabase; diff --git a/libs/database/src/migration.service.ts b/libs/database/src/migration.service.ts index 4236b72..7c268b1 100644 --- a/libs/database/src/migration.service.ts +++ b/libs/database/src/migration.service.ts @@ -1,9 +1,9 @@ import { Inject, Injectable, OnModuleInit, Logger } from '@nestjs/common'; import { migrate } from 'drizzle-orm/node-postgres/migrator'; - -import { DATABASE_OPTIONS, DATABASE_SERVICE } from './database.constants'; -import type { DatabaseService, DatabaseModuleOptions } from './interfaces'; +import { DATABASE_SERVICE } from './constants'; +import type { DatabaseService } from './interfaces'; import * as path from 'path'; +import { MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } from './database.module-definition'; @Injectable() export class MigrationService implements OnModuleInit { @@ -11,9 +11,9 @@ export class MigrationService implements OnModuleInit { constructor( @Inject(DATABASE_SERVICE) - private readonly db: DatabaseService>, - @Inject(DATABASE_OPTIONS) - private readonly options: DatabaseModuleOptions, + private readonly db: DatabaseService, + @Inject(MODULE_OPTIONS_TOKEN) + private readonly options: typeof OPTIONS_TYPE, ) {} async onModuleInit() { @@ -21,10 +21,12 @@ export class MigrationService implements OnModuleInit { return; } + const migrationsFolder = path.resolve(process.cwd(), 'migrations'); + this.logger.debug('Checking for database migrations...'); try { await migrate(this.db, { - migrationsFolder: path.resolve(process.cwd(), 'migrations'), + migrationsFolder, }); this.logger.debug('Migrations completed or already up to date'); } catch (error) { From 59129a19cebd4c38ac331e82da57071446f494cb Mon Sep 17 00:00:00 2001 From: soorq Date: Tue, 12 May 2026 00:04:41 +0300 Subject: [PATCH 2/4] chore(postgres): migrate from np to postgres, resolve all error at repositories rowCount --- libs/database/src/database.module.ts | 38 ++++++++--- .../src/interfaces/module.interface.ts | 16 ++++- libs/database/src/migration.service.ts | 2 +- package.json | 4 +- pnpm-lock.yaml | 68 +++++++++---------- .../repositories/session.repository.ts | 6 +- src/shared/error/filter.ts | 12 ++-- .../repositories/teams.repository.ts | 20 +++--- .../repositories/user.repository.ts | 20 +++--- 9 files changed, 105 insertions(+), 81 deletions(-) diff --git a/libs/database/src/database.module.ts b/libs/database/src/database.module.ts index 2b3b36a..919097b 100644 --- a/libs/database/src/database.module.ts +++ b/libs/database/src/database.module.ts @@ -1,20 +1,21 @@ -import { Logger, Module, OnApplicationShutdown } from '@nestjs/common'; +import { Inject, Logger, Module, OnApplicationShutdown } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { drizzle } from 'drizzle-orm/node-postgres'; +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; import { DATABASE_SERVICE } from './constants'; import { MigrationService } from './migration.service'; -import { Pool } from 'pg'; import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, } from './database.module-definition'; +import { DatabaseService } from './interfaces'; @Module({ providers: [ MigrationService, { - provide: Pool, + provide: 'SQL_CLIENT', inject: [ConfigService, MODULE_OPTIONS_TOKEN], useFactory: (configService: ConfigService, opts: typeof OPTIONS_TYPE) => { const baseUrl = configService.getOrThrow('DATABASE_URL'); @@ -24,16 +25,29 @@ import { url.searchParams.set('options', `-c search_path=${opts.schemaName}`); } - return new Pool(url.toString()); + return postgres(url.toString(), { + onnotice: (msg) => new Logger('PostgresJS').verbose(msg), + backoff: (attempt) => Math.min(attempt * 100, 3000), + target_session_attrs: 'read-write', + publications: 'alltables', + connect_timeout: 2, + idle_timeout: 5, + max_lifetime: 60 * 60, + keep_alive: 30, + transform: { + undefined: null, + }, + ...opts.pool, + }); }, }, { provide: DATABASE_SERVICE, - inject: [Pool, MODULE_OPTIONS_TOKEN], - useFactory: (pool: Pool, opts: typeof OPTIONS_TYPE) => { + inject: ['SQL_CLIENT', MODULE_OPTIONS_TOKEN], + useFactory: (sql: postgres.Sql, opts: typeof OPTIONS_TYPE) => { const logger = new Logger('Drizzle'); - return drizzle(pool, { + return drizzle(sql, { schema: opts.schema, logger: opts.logging ? { @@ -53,13 +67,15 @@ import { export class DatabaseModule extends ConfigurableModuleClass implements OnApplicationShutdown { private readonly logger = new Logger(DatabaseModule.name); - constructor() { - // @Inject(DATABASE_SERVICE) - // private readonly db: DatabaseService, + constructor( + @Inject(DATABASE_SERVICE) + private readonly db: DatabaseService, + ) { super(); } async onApplicationShutdown() { this.logger.log('Closing database connections...'); + await this.db.$client.end(); } } diff --git a/libs/database/src/interfaces/module.interface.ts b/libs/database/src/interfaces/module.interface.ts index c46219f..078c486 100644 --- a/libs/database/src/interfaces/module.interface.ts +++ b/libs/database/src/interfaces/module.interface.ts @@ -1,4 +1,5 @@ -import type { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import type { Options, Sql } from 'postgres'; export interface DatabaseModuleOptions { /** @@ -16,6 +17,15 @@ export interface DatabaseModuleOptions { */ schema: Record; + /** + * Настройки драйвера `postgres.js`. + * * Позволяет настроить пул соединений, таймауты и SSL. + * * **Внимание:** Параметры отличаются от драйвера `pg`. + * @see https://github.com/porsager/postgres#options + * @example { max: 20, idle_timeout: 30, connect_timeout: 5 } + */ + pool?: Options; + /** * Включение или выключение логирования SQL-запросов в консоль через NestJS Logger. * @default false @@ -46,4 +56,6 @@ export interface DatabaseModuleOptions { * @Inject(DATABASE_SERVICE) private readonly db: DatabaseService * ) {} */ -export type DatabaseService> = NodePgDatabase; +export type DatabaseService> = PostgresJsDatabase & { + $client: Sql; +}; diff --git a/libs/database/src/migration.service.ts b/libs/database/src/migration.service.ts index 7c268b1..2aeefdc 100644 --- a/libs/database/src/migration.service.ts +++ b/libs/database/src/migration.service.ts @@ -1,5 +1,5 @@ import { Inject, Injectable, OnModuleInit, Logger } from '@nestjs/common'; -import { migrate } from 'drizzle-orm/node-postgres/migrator'; +import { migrate } from 'drizzle-orm/postgres-js/migrator'; import { DATABASE_SERVICE } from './constants'; import type { DatabaseService } from './interfaces'; import * as path from 'path'; diff --git a/package.json b/package.json index c7d6c1d..12809ee 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "argon2": "^0.44.0", "axios": "^1.16.0", "bullmq": "^5.73.4", - "cls-rtracer": "^2.6.3", "dotenv": "^17.4.2", "drizzle-orm": "^0.45.2", "drizzle-zod": "^0.8.3", @@ -73,7 +72,7 @@ "otplib": "^13.4.0", "passport": "^0.7.0", "passport-jwt": "^4.0.1", - "pg": "^8.20.0", + "postgres": "^3.4.9", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "transliteration": "^2.6.1", @@ -90,7 +89,6 @@ "@types/node": "^20.3.1", "@types/nodemailer": "^8.0.0", "@types/passport-jwt": "^4.0.1", - "@types/pg": "^8.20.0", "@types/ua-parser-js": "^0.7.39", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94c94b4..ef2954e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,18 +92,15 @@ importers: bullmq: specifier: ^5.73.4 version: 5.73.4 - cls-rtracer: - specifier: ^2.6.3 - version: 2.6.3 dotenv: specifier: ^17.4.2 version: 17.4.2 drizzle-orm: specifier: ^0.45.2 - version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0) + version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0)(postgres@3.4.9) drizzle-zod: specifier: ^0.8.3 - version: 0.8.3(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0))(zod@4.3.6) + version: 0.8.3(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0)(postgres@3.4.9))(zod@4.3.6) fastify: specifier: ^5.8.4 version: 5.8.4 @@ -131,9 +128,9 @@ importers: passport-jwt: specifier: ^4.0.1 version: 4.0.1 - pg: - specifier: ^8.20.0 - version: 8.20.0 + postgres: + specifier: ^3.4.9 + version: 3.4.9 reflect-metadata: specifier: ^0.2.0 version: 0.2.2 @@ -177,9 +174,6 @@ importers: '@types/passport-jwt': specifier: ^4.0.1 version: 4.0.1 - '@types/pg': - specifier: ^8.20.0 - version: 8.20.0 '@types/ua-parser-js': specifier: ^0.7.39 version: 0.7.39 @@ -2526,10 +2520,6 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - cls-rtracer@2.6.3: - resolution: {integrity: sha512-O7M/m2M/KfT9v+q7ka9nmsadS67ce9P8+1Zgm6VFamK56oFd1iCoJ9m8hYKUQpK4+RofyaexxHJlOBkxqCDs3Q==} - engines: {node: '>=12.17.0 <13.0.0 || >=13.14.0 <14.0.0 || >=14.0.0'} - cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} @@ -3893,6 +3883,10 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} + postgres@3.4.9: + resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==} + engines: {node: '>=12'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -4373,11 +4367,6 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). - hasBin: true - vite@8.0.8: resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6644,6 +6633,7 @@ snapshots: '@types/node': 20.19.39 pg-protocol: 1.13.0 pg-types: 2.2.0 + optional: true '@types/qs@6.15.0': {} @@ -7151,10 +7141,6 @@ snapshots: clone@1.0.4: {} - cls-rtracer@2.6.3: - dependencies: - uuid: 9.0.1 - cluster-key-slot@1.1.2: {} color-convert@2.0.1: @@ -7318,15 +7304,16 @@ snapshots: esbuild: 0.25.12 tsx: 4.21.0 - drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0): + drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0)(postgres@3.4.9): optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/pg': 8.20.0 pg: 8.20.0 + postgres: 3.4.9 - drizzle-zod@0.8.3(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0))(zod@4.3.6): + drizzle-zod@0.8.3(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0)(postgres@3.4.9))(zod@4.3.6): dependencies: - drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0) + drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0)(postgres@3.4.9) zod: 4.3.6 dunder-proto@1.0.1: @@ -8407,15 +8394,19 @@ snapshots: pg-cloudflare@1.3.0: optional: true - pg-connection-string@2.12.0: {} + pg-connection-string@2.12.0: + optional: true - pg-int8@1.0.1: {} + pg-int8@1.0.1: + optional: true pg-pool@3.13.0(pg@8.20.0): dependencies: pg: 8.20.0 + optional: true - pg-protocol@1.13.0: {} + pg-protocol@1.13.0: + optional: true pg-types@2.2.0: dependencies: @@ -8424,6 +8415,7 @@ snapshots: postgres-bytea: 1.0.1 postgres-date: 1.0.7 postgres-interval: 1.2.0 + optional: true pg@8.20.0: dependencies: @@ -8434,10 +8426,12 @@ snapshots: pgpass: 1.0.5 optionalDependencies: pg-cloudflare: 1.3.0 + optional: true pgpass@1.0.5: dependencies: split2: 4.2.0 + optional: true picocolors@1.1.1: {} @@ -8473,15 +8467,21 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postgres-array@2.0.0: {} + postgres-array@2.0.0: + optional: true - postgres-bytea@1.0.1: {} + postgres-bytea@1.0.1: + optional: true - postgres-date@1.0.7: {} + postgres-date@1.0.7: + optional: true postgres-interval@1.2.0: dependencies: xtend: 4.0.2 + optional: true + + postgres@3.4.9: {} prelude-ls@1.2.1: {} @@ -8916,8 +8916,6 @@ snapshots: uuid@11.1.0: {} - uuid@9.0.1: {} - vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 diff --git a/src/auth/infrastructure/persistence/repositories/session.repository.ts b/src/auth/infrastructure/persistence/repositories/session.repository.ts index 709593a..8e38f4a 100644 --- a/src/auth/infrastructure/persistence/repositories/session.repository.ts +++ b/src/auth/infrastructure/persistence/repositories/session.repository.ts @@ -35,12 +35,12 @@ export class SessionRepository implements ISessionRepository { } async revoke(id: string) { - const { rowCount } = await this.db + const result = await this.db .update(schema.sessions) .set({ isRevoked: true, updatedAt: new Date() }) .where(eq(schema.sessions.id, id)); - return (rowCount ?? 0) > 0; + return (result?.count ?? 0) > 0; } async revokeAllByUserId(userId: string, exceptSessionId?: string) { @@ -61,6 +61,6 @@ export class SessionRepository implements ISessionRepository { .delete(schema.sessions) .where(lt(schema.sessions.expiresAt, new Date())); - return result.rowCount; + return result?.count ?? 0; } } diff --git a/src/shared/error/filter.ts b/src/shared/error/filter.ts index 1fa7f9b..b06dc7f 100644 --- a/src/shared/error/filter.ts +++ b/src/shared/error/filter.ts @@ -8,8 +8,8 @@ import { } from '@nestjs/common'; import { ZodValidationException } from 'nestjs-zod'; import type { FastifyReply, FastifyRequest } from 'fastify'; -import { DatabaseError } from 'pg'; -import { BaseException, IErrorOptions } from './exception'; +import { PostgresError } from 'postgres'; +import { BaseException, type IErrorOptions } from './exception'; import { DrizzleQueryError } from 'drizzle-orm'; import type { ZodError, ZodIssue } from 'zod/v4'; import { DATABASE_ERRORS } from './swagger'; @@ -65,9 +65,9 @@ export class GlobalExceptionFilter implements ExceptionFilter { const { request, response } = this.getCtxBase(host); const error = - exception.cause instanceof DatabaseError + exception.cause instanceof PostgresError ? exception.cause - : exception instanceof DatabaseError + : exception instanceof PostgresError ? exception : null; @@ -85,9 +85,9 @@ export class GlobalExceptionFilter implements ExceptionFilter { this.log(exception, host, status, { dbCode: error?.code, - dbTable: error?.table, + dbTable: error?.table_name, dbDetail: error?.detail, - query: this.isDev ? exception.message : undefined, + query: this.isDev ? exception.query : undefined, }); return response.status(status).send( diff --git a/src/teams/infrastructure/persistence/repositories/teams.repository.ts b/src/teams/infrastructure/persistence/repositories/teams.repository.ts index a11c275..845c376 100644 --- a/src/teams/infrastructure/persistence/repositories/teams.repository.ts +++ b/src/teams/infrastructure/persistence/repositories/teams.repository.ts @@ -22,14 +22,14 @@ export class TeamsRepository implements ITeamsRepository { }; public addMember = async (dto: NewTeamMember) => { - const { rowCount } = await this.db + const result = await this.db .insert(schema.teamMembers) .values(dto) .onConflictDoNothing({ target: [schema.teamMembers.teamId, schema.teamMembers.userId], }); - return (rowCount ?? 0) > 0; + return (result?.count ?? 0) > 0; }; public create = async (ownerId: string, dto: NewTeam, tags?: string[]) => { @@ -95,7 +95,7 @@ export class TeamsRepository implements ITeamsRepository { public remove = async (teamId: string, userId) => { const suffix = Date.now().toString(); - const { rowCount } = await this.db + const result = await this.db .update(schema.teams) .set({ deletedAt: new Date(), @@ -103,7 +103,7 @@ export class TeamsRepository implements ITeamsRepository { }) .where(and(eq(schema.teams.id, teamId), eq(schema.teams.ownerId, userId))); - return (rowCount ?? 0) > 0; + return (result?.count ?? 0) > 0; }; public findMember = async (teamId: string, userId: string) => { @@ -195,7 +195,7 @@ export class TeamsRepository implements ITeamsRepository { and(eq(schema.teamMembers.teamId, teamId), eq(schema.teamMembers.userId, userId)), ); - return (result.rowCount ?? 0) > 0; + return (result?.count ?? 0) > 0; }; public syncTags = async (teamId: string, tagNames: string[]) => { @@ -239,23 +239,23 @@ export class TeamsRepository implements ITeamsRepository { and(eq(schema.teamMembers.teamId, teamId), eq(schema.teamMembers.userId, userId)), ); - return (result.rowCount ?? 0) > 0; + return (result?.count ?? 0) > 0; }; public async updateTeamAvatar(teamId: string, url: string): Promise { - const { rowCount } = await this.db + const result = await this.db .update(schema.teams) .set({ avatarUrl: url, updatedAt: new Date() }) .where(eq(schema.teams.id, teamId)); - return (rowCount ?? 0) > 0; + return (result?.count ?? 0) > 0; } public async updateTeamBanner(teamId: string, url: string): Promise { - const { rowCount } = await this.db + const result = await this.db .update(schema.teams) .set({ coverUrl: url, updatedAt: new Date() }) .where(eq(schema.teams.id, teamId)); - return (rowCount ?? 0) > 0; + return (result?.count ?? 0) > 0; } private get memberSelection() { diff --git a/src/user/infrastructure/persistence/repositories/user.repository.ts b/src/user/infrastructure/persistence/repositories/user.repository.ts index b036891..76e4b06 100644 --- a/src/user/infrastructure/persistence/repositories/user.repository.ts +++ b/src/user/infrastructure/persistence/repositories/user.repository.ts @@ -77,46 +77,46 @@ export class UserRepository implements IUserRepository { } async updateProfile(id: string, data: Partial) { - const { rowCount } = await this.db + const result = await this.db .update(sc.users) .set({ ...data, updatedAt: new Date() }) .where(eq(sc.users.id, id)); - return (rowCount ?? 0) > 0; + return (result?.count ?? 0) > 0; } async updateNotifications(id: string, settings: UserNotifications['settings']) { - const { rowCount } = await this.db + const result = await this.db .update(sc.userNotifications) .set({ settings }) .where(eq(sc.userNotifications.userId, id)); - return (rowCount ?? 0) > 0; + return (result?.count ?? 0) > 0; } async updateAvatar(id: string, url: string) { - const { rowCount } = await this.db + const result = await this.db .update(sc.users) .set({ avatarUrl: url, updatedAt: new Date() }) .where(eq(sc.users.id, id)); - return (rowCount ?? 0) > 0; + return (result?.count ?? 0) > 0; } async updatePasswordHash(id: string, hash: string) { - const { rowCount } = await this.db + const result = await this.db .insert(sc.userSecurity) .values({ userId: id, passwordHash: hash }) .onConflictDoUpdate({ target: sc.userSecurity.userId, set: { passwordHash: hash, lastPasswordChange: new Date() }, }); - return (rowCount ?? 0) > 0; + return (result?.count ?? 0) > 0; } async logActivity(data: NewUserActivity) { - const { rowCount } = await this.db.insert(sc.userActivity).values({ + const result = await this.db.insert(sc.userActivity).values({ ...data, id: data.id ?? createId(), }); - return (rowCount ?? 0) > 0; + return (result?.count ?? 0) > 0; } async findActivityByUser(userId: string, options: { limit: number; offset: number }) { From da18a91665474efd2610bf37727f9e95b1fffccd Mon Sep 17 00:00:00 2001 From: soorq Date: Tue, 12 May 2026 00:15:39 +0300 Subject: [PATCH 3/4] chore(db): upgrade drizzle config --- drizzle.config.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/drizzle.config.ts b/drizzle.config.ts index 247cdcc..a4b3b6c 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -4,6 +4,15 @@ export default defineConfig({ schema: './src/shared/entities/index.ts', out: './migrations', dialect: 'postgresql', + breakpoints: false, + casing: 'snake_case', + migrations: { + prefix: 'index', + table: 'migrations', + schema: 'drizzle', + }, + strict: true, + verbose: true, dbCredentials: { url: process.env.DATABASE_URL!, }, From f1b02d544c7781a5634a05853a77d3824e17424b Mon Sep 17 00:00:00 2001 From: soorq Date: Tue, 12 May 2026 00:27:22 +0300 Subject: [PATCH 4/4] refactor(db): replace pg with postgres.js, update k6 scripts and database module --- infra/k6/scripts/clear-k6-data.ts | 12 ++++++------ infra/k6/scripts/k6-env.ts | 4 +--- infra/k6/scripts/seed-k6-data.ts | 13 +++++++------ libs/database/src/constants.ts | 1 + libs/database/src/database.module.ts | 13 ++++++------- libs/database/src/index.ts | 2 +- .../src/interfaces/module.interface.ts | 19 ++++++++++--------- 7 files changed, 32 insertions(+), 32 deletions(-) diff --git a/infra/k6/scripts/clear-k6-data.ts b/infra/k6/scripts/clear-k6-data.ts index 46518d3..ccee9fe 100644 --- a/infra/k6/scripts/clear-k6-data.ts +++ b/infra/k6/scripts/clear-k6-data.ts @@ -1,12 +1,12 @@ import Redis from 'ioredis'; -import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import * as sc from '../../../src/shared/entities'; import { sql } from 'drizzle-orm'; -import { Pool } from 'pg'; +import postgres from 'postgres'; import { assertEnv, DB_URL, REDIS_URL } from './k6-env'; import { KEYS } from './k6-data-keys'; -async function clearDB(db: NodePgDatabase) { +async function clearDB(db: PostgresJsDatabase) { console.log('Cleaning up ONLY k6 test data from DB...'); return await db.transaction(async (tx) => { await tx.delete(sc.users).where(sql`${sc.users.email} LIKE 'k6_user_%'`); @@ -32,8 +32,8 @@ async function clearRedis(redis: Redis) { async function main() { assertEnv(); const redis = new Redis(REDIS_URL); - const pool = new Pool({ connectionString: DB_URL }); - const db = drizzle(pool, { schema: sc }); + const queryClient = postgres(DB_URL, { max: 1 }); + const db = drizzle(queryClient, { schema: sc }); try { await clearDB(db); @@ -42,7 +42,7 @@ async function main() { console.error('Error:', e); process.exit(1); } finally { - await pool.end(); + await queryClient.end(); await redis.quit(); } } diff --git a/infra/k6/scripts/k6-env.ts b/infra/k6/scripts/k6-env.ts index 11c909b..d95a666 100644 --- a/infra/k6/scripts/k6-env.ts +++ b/infra/k6/scripts/k6-env.ts @@ -1,6 +1,4 @@ -import * as dotenv from 'dotenv'; - -dotenv.config(); +import 'dotenv/config'; export const DB_URL = process.env.DATABASE_URL; export const REDIS_URL = diff --git a/infra/k6/scripts/seed-k6-data.ts b/infra/k6/scripts/seed-k6-data.ts index 23b20b7..82fbb1f 100644 --- a/infra/k6/scripts/seed-k6-data.ts +++ b/infra/k6/scripts/seed-k6-data.ts @@ -1,7 +1,7 @@ import { createId } from '@paralleldrive/cuid2'; import * as argon from 'argon2'; -import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { Pool } from 'pg'; +import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; import { writeFileSync, mkdirSync, readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import * as sc from '../../../src/shared/entities/index'; @@ -9,7 +9,7 @@ import Redis from 'ioredis'; import { assertEnv, DB_URL, REDIS_URL } from './k6-env'; import { KEYS } from './k6-data-keys'; -async function seed_db(db: NodePgDatabase) { +async function seed_db(db: PostgresJsDatabase) { const COUNT = 1000; const OUT_USERS_FILE = resolve(process.cwd(), 'infra/k6/data/users.json'); const OUT_TEAMS_FILE = resolve(process.cwd(), 'infra/k6/data/teams.json'); @@ -149,6 +149,7 @@ async function seed_redis(redis: Redis) { const INVITES_PER_TEAM = 10; const invitesData = []; + teams.forEach((team, teamIdx) => { for (let j = 1; j <= INVITES_PER_TEAM; j++) { const inviteeIdx = (teamIdx + j) % users.length; @@ -193,8 +194,8 @@ async function seed_redis(redis: Redis) { async function main() { assertEnv(); const redis = new Redis(REDIS_URL); - const pool = new Pool({ connectionString: DB_URL }); - const db = drizzle(pool, { schema: sc }); + const queryClient = postgres(DB_URL, { max: 1 }); + const db = drizzle(queryClient, { schema: sc }); try { await seed_db(db); @@ -203,7 +204,7 @@ async function main() { console.error('Error:', e); process.exit(1); } finally { - await pool.end(); + await queryClient.end(); await redis.quit(); } } diff --git a/libs/database/src/constants.ts b/libs/database/src/constants.ts index 56b4814..087de13 100644 --- a/libs/database/src/constants.ts +++ b/libs/database/src/constants.ts @@ -1 +1,2 @@ export const DATABASE_SERVICE = 'DATABASE_SERVICE'; +export const SQL_CLIENT = 'SQL_CLIENT'; diff --git a/libs/database/src/database.module.ts b/libs/database/src/database.module.ts index 919097b..b5fcf2f 100644 --- a/libs/database/src/database.module.ts +++ b/libs/database/src/database.module.ts @@ -2,20 +2,19 @@ import { Inject, Logger, Module, OnApplicationShutdown } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { drizzle } from 'drizzle-orm/postgres-js'; import postgres from 'postgres'; -import { DATABASE_SERVICE } from './constants'; +import { DATABASE_SERVICE, SQL_CLIENT } from './constants'; import { MigrationService } from './migration.service'; import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, } from './database.module-definition'; -import { DatabaseService } from './interfaces'; @Module({ providers: [ MigrationService, { - provide: 'SQL_CLIENT', + provide: SQL_CLIENT, inject: [ConfigService, MODULE_OPTIONS_TOKEN], useFactory: (configService: ConfigService, opts: typeof OPTIONS_TYPE) => { const baseUrl = configService.getOrThrow('DATABASE_URL'); @@ -43,7 +42,7 @@ import { DatabaseService } from './interfaces'; }, { provide: DATABASE_SERVICE, - inject: ['SQL_CLIENT', MODULE_OPTIONS_TOKEN], + inject: [SQL_CLIENT, MODULE_OPTIONS_TOKEN], useFactory: (sql: postgres.Sql, opts: typeof OPTIONS_TYPE) => { const logger = new Logger('Drizzle'); @@ -68,14 +67,14 @@ export class DatabaseModule extends ConfigurableModuleClass implements OnApplica private readonly logger = new Logger(DatabaseModule.name); constructor( - @Inject(DATABASE_SERVICE) - private readonly db: DatabaseService, + @Inject(SQL_CLIENT) + private readonly sql: postgres.Sql, ) { super(); } async onApplicationShutdown() { this.logger.log('Closing database connections...'); - await this.db.$client.end(); + await this.sql.end(); } } diff --git a/libs/database/src/index.ts b/libs/database/src/index.ts index ce3920e..e258d47 100644 --- a/libs/database/src/index.ts +++ b/libs/database/src/index.ts @@ -1,3 +1,3 @@ export * from './database.module'; -export * from './constants'; +export { DATABASE_SERVICE } from './constants'; export type { DatabaseService } from './interfaces'; diff --git a/libs/database/src/interfaces/module.interface.ts b/libs/database/src/interfaces/module.interface.ts index 078c486..bbe4021 100644 --- a/libs/database/src/interfaces/module.interface.ts +++ b/libs/database/src/interfaces/module.interface.ts @@ -1,5 +1,5 @@ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import type { Options, Sql } from 'postgres'; +import type { Options } from 'postgres'; export interface DatabaseModuleOptions { /** @@ -48,14 +48,15 @@ export interface DatabaseModuleOptions { } /** - * Основной тип сервиса базы данных для инъекции в репозитории. - * * Включает в себя типизированный API Drizzle и прямой доступ к драйверу через `$client`. - * * @template T - Тип вашей схемы данных (например, `typeof schema`). - * * @example + * Тип для внедрения Drizzle ORM в репозитории. + * Использует драйвер postgres-js под капотом. + * + * @example + * // В репозитории: * constructor( - * @Inject(DATABASE_SERVICE) private readonly db: DatabaseService + * @Inject(DATABASE_SERVICE) private readonly db: DatabaseService * ) {} + * + * @template TSchema - Тип вашей схемы данных (например, `typeof schema`). */ -export type DatabaseService> = PostgresJsDatabase & { - $client: Sql; -}; +export type DatabaseService> = PostgresJsDatabase;