From 15e63f97252c621a9e1fe69a969965b1bca15825 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 28 Jan 2026 05:31:51 +0300 Subject: [PATCH] feat: add grpc env validator --- package.json | 2 ++ src/app.module.ts | 5 +-- src/config/env/grpc.env.ts | 15 +++++++++ src/config/env/index.ts | 1 + src/config/index.ts | 2 ++ .../interfaces/all-configs.interface.ts | 5 +++ src/config/interfaces/grpc.interface.ts | 4 +++ src/config/interfaces/index.ts | 1 + src/config/validators/grpc.validator.ts | 9 ++++++ src/config/validators/index.ts | 1 + src/main.ts | 9 +++++- src/shared/utils/env.ts | 32 +++++++++++++++++++ src/shared/utils/index.ts | 1 + tsconfig.json | 2 +- yarn.lock | 29 +++++++++++++++++ 15 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 src/config/env/grpc.env.ts create mode 100644 src/config/env/index.ts create mode 100644 src/config/index.ts create mode 100644 src/config/interfaces/all-configs.interface.ts create mode 100644 src/config/interfaces/grpc.interface.ts create mode 100644 src/config/interfaces/index.ts create mode 100644 src/config/validators/grpc.validator.ts create mode 100644 src/config/validators/index.ts create mode 100644 src/shared/utils/env.ts create mode 100644 src/shared/utils/index.ts diff --git a/package.json b/package.json index 634bae2..f2e5294 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,8 @@ "@prisma/client": "^7.3.0", "@teacinema/common": "^1.0.0", "@teacinema/contracts": "^1.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.3", "dotenv-expand": "^12.0.3", "ioredis": "^5.9.2", "prisma": "^7.3.0", diff --git a/src/app.module.ts b/src/app.module.ts index 6580e4e..c6f3675 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,14 +1,15 @@ import { Module } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' +import { grpcEnv } from './config' import { PrismaModule } from './infra/prisma/prisma.module' import { RedisModule } from './infra/redis/redis.module' import { AuthModule } from './modules/auth/auth.module' -import { OtpModule } from './modules/otp/otp.module'; +import { OtpModule } from './modules/otp/otp.module' @Module({ imports: [ - ConfigModule.forRoot({ isGlobal: true }), + ConfigModule.forRoot({ isGlobal: true, load: [grpcEnv] }), PrismaModule, RedisModule, AuthModule, diff --git a/src/config/env/grpc.env.ts b/src/config/env/grpc.env.ts new file mode 100644 index 0000000..c6cb201 --- /dev/null +++ b/src/config/env/grpc.env.ts @@ -0,0 +1,15 @@ +import { registerAs } from '@nestjs/config' + +import { validateEnv } from '@/shared/utils' + +import type { GrpcConfig } from '../interfaces/grpc.interface' +import { GrpcValidator } from '../validators' + +export const grpcEnv = registerAs('grpc', () => { + validateEnv(process.env, GrpcValidator) + + return { + host: process.env.GRPC_HOST, + port: parseInt(process.env.GRPC_PORT) + } +}) diff --git a/src/config/env/index.ts b/src/config/env/index.ts new file mode 100644 index 0000000..e23a50f --- /dev/null +++ b/src/config/env/index.ts @@ -0,0 +1 @@ +export * from './grpc.env' diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..0f0484c --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,2 @@ +export * from './env' +export * from './interfaces' diff --git a/src/config/interfaces/all-configs.interface.ts b/src/config/interfaces/all-configs.interface.ts new file mode 100644 index 0000000..20be84b --- /dev/null +++ b/src/config/interfaces/all-configs.interface.ts @@ -0,0 +1,5 @@ +import { GrpcConfig } from './grpc.interface' + +export interface AllConfigs { + grpc: GrpcConfig +} diff --git a/src/config/interfaces/grpc.interface.ts b/src/config/interfaces/grpc.interface.ts new file mode 100644 index 0000000..06c26a9 --- /dev/null +++ b/src/config/interfaces/grpc.interface.ts @@ -0,0 +1,4 @@ +export interface GrpcConfig { + host: string + port: number +} diff --git a/src/config/interfaces/index.ts b/src/config/interfaces/index.ts new file mode 100644 index 0000000..0879f02 --- /dev/null +++ b/src/config/interfaces/index.ts @@ -0,0 +1 @@ +export * from './all-configs.interface' diff --git a/src/config/validators/grpc.validator.ts b/src/config/validators/grpc.validator.ts new file mode 100644 index 0000000..8e91d1b --- /dev/null +++ b/src/config/validators/grpc.validator.ts @@ -0,0 +1,9 @@ +import { IsInt, IsString } from 'class-validator' + +export class GrpcValidator { + @IsString() + public GRPC_HOST: string + + @IsInt() + public GRPC_PORT: number +} diff --git a/src/config/validators/index.ts b/src/config/validators/index.ts new file mode 100644 index 0000000..ec7ed52 --- /dev/null +++ b/src/config/validators/index.ts @@ -0,0 +1 @@ +export * from './grpc.validator' diff --git a/src/main.ts b/src/main.ts index b7e93c7..995def4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,15 +1,22 @@ +import { ConfigService } from '@nestjs/config' import { NestFactory } from '@nestjs/core' import { MicroserviceOptions, Transport } from '@nestjs/microservices' import { AppModule } from './app.module' +import { AllConfigs } from './config' async function bootstrap() { const app = await NestFactory.create(AppModule) + const config = app.get(ConfigService) + const host = config.get('grpc.host', { infer: true }) + const port = config.get('grpc.port', { infer: true }) + const url = `${host}:${port}` + app.connectMicroservice({ transport: Transport.GRPC, options: { - url: 'localhost:50051', + url, package: 'auth.v1', protoPath: 'node_modules/@teacinema/contracts/proto/auth.proto', loader: { diff --git a/src/shared/utils/env.ts b/src/shared/utils/env.ts new file mode 100644 index 0000000..76e257c --- /dev/null +++ b/src/shared/utils/env.ts @@ -0,0 +1,32 @@ +import { ClassConstructor, plainToClass } from 'class-transformer' +import { validateSync } from 'class-validator' + +export function validateEnv( + config: Record, + envVariableClass: ClassConstructor +) { + const validatedConfig = plainToClass(envVariableClass, config, { + enableImplicitConversion: true + }) + + const errors = validateSync(validatedConfig, { + skipMissingProperties: false + }) + + if (errors.length > 0) { + const errorMessages = errors + .map( + error => + `\nError in ${error.property}\n: + + ${Object.entries(error.constraints) + .map(([key, value]) => `${key}: ${value}`) + .join('\n')}` + ) + .join('\n') + + console.error(`\n${errors.toString()}`) + throw new Error(errorMessages) + } + + return validatedConfig +} diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts new file mode 100644 index 0000000..94812ca --- /dev/null +++ b/src/shared/utils/index.ts @@ -0,0 +1 @@ +export * from './env' diff --git a/tsconfig.json b/tsconfig.json index 28a05ca..6e45860 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "baseUrl": "./", "incremental": true, "skipLibCheck": true, - "strictNullChecks": true, + "strictNullChecks": false, "forceConsistentCasingInFileNames": true, "noImplicitAny": false, "strictBindCallApply": false, diff --git a/yarn.lock b/yarn.lock index 604be93..1cbf652 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1632,6 +1632,11 @@ "@types/methods" "^1.1.4" "@types/superagent" "^8.1.0" +"@types/validator@^13.15.3": + version "13.15.10" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.15.10.tgz#742b77ec34d58554b94a76a14cef30d59e3c16b9" + integrity sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA== + "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -2437,6 +2442,20 @@ cjs-module-lexer@^2.1.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz#b3ca5101843389259ade7d88c77bd06ce55849ca" integrity sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ== +class-transformer@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" + integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== + +class-validator@^0.14.3: + version "0.14.3" + resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.14.3.tgz#834a4caafa8359aed73d7708badb4cf271be50fe" + integrity sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA== + dependencies: + "@types/validator" "^13.15.3" + libphonenumber-js "^1.11.1" + validator "^13.15.20" + cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -4134,6 +4153,11 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +libphonenumber-js@^1.11.1: + version "1.12.35" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.12.35.tgz#149d4a66b2a7a518998c430c6d33465abaed23c4" + integrity sha512-T/Cz6iLcsZdb5jDncDcUNhSAJ0VlSC9TnsqtBNdpkaAmy24/R1RhErtNWVWBrcUZKs9hSgaVsBkc7HxYnazIfw== + lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" @@ -5740,6 +5764,11 @@ valibot@1.2.0: resolved "https://registry.yarnpkg.com/valibot/-/valibot-1.2.0.tgz#8fc720d9e4082ba16e30a914064a39619b2f1d6f" integrity sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg== +validator@^13.15.20: + version "13.15.26" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.15.26.tgz#36c3deeab30e97806a658728a155c66fcaa5b944" + integrity sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA== + vary@^1, vary@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"