diff --git a/src/modules/account/account.controller.ts b/src/modules/account/account.controller.ts index f182ccc..787a7cc 100644 --- a/src/modules/account/account.controller.ts +++ b/src/modules/account/account.controller.ts @@ -27,7 +27,7 @@ export class AccountController { @Body() dto: InitEmailChangeRequestDto, @CurrentUser() userId: string ) { - return this.client.initEmailChange({ ...dto, userId }) + return this.client.call('initEmailChange', { email: dto.email, userId }) } @ApiOperation({ @@ -42,7 +42,7 @@ export class AccountController { @Body() dto: ConfirmEmailChangeRequestDto, @CurrentUser() userId: string ) { - return this.client.confirmEmailChange({ ...dto, userId }) + return this.client.call('confirmEmailChange', { ...dto, userId }) } @ApiOperation({ @@ -57,7 +57,7 @@ export class AccountController { @Body() dto: InitPhoneChangeRequestDto, @CurrentUser() userId: string ) { - return this.client.initPhoneChange({ ...dto, userId }) + return this.client.call('initPhoneChange', { ...dto, userId }) } @ApiOperation({ @@ -72,6 +72,6 @@ export class AccountController { @Body() dto: ConfirmPhoneChangeRequestDto, @CurrentUser() userId: string ) { - return this.client.confirmPhoneChange({ ...dto, userId }) + return this.client.call('confirmPhoneChange', { ...dto, userId }) } } diff --git a/src/modules/account/account.grpc.ts b/src/modules/account/account.grpc.ts index e02266e..0cc0374 100644 --- a/src/modules/account/account.grpc.ts +++ b/src/modules/account/account.grpc.ts @@ -1,42 +1,13 @@ -import { Inject, Injectable, OnModuleInit } from '@nestjs/common' +import { Injectable } from '@nestjs/common' import type { ClientGrpc } from '@nestjs/microservices' -import { - AccountServiceClient, - ConfirmEmailChangeRequest, - ConfirmPhoneChangeRequest, - GetAccountRequest, - InitEmailChangeRequest, - InitPhoneChangeRequest, -} from '@teacinema/contracts/gen/account' +import { InjectGrpc } from '@teacinema/common' +import { AccountServiceClient } from '@teacinema/contracts/gen/account' + +import { AbstractGrpcClient } from '../../shared/grpc/abstract-grpc.client' @Injectable() -export class AccountClientGrpc implements OnModuleInit { - private accountService: AccountServiceClient - - constructor(@Inject('ACCOUNT_PACKAGE') private readonly client: ClientGrpc) {} - - onModuleInit() { - this.accountService = - this.client.getService('AccountService') - } - - public getAccount(request: GetAccountRequest) { - return this.accountService.getAccount(request) - } - - public initEmailChange(request: InitEmailChangeRequest) { - return this.accountService.initEmailChange(request) - } - - public initPhoneChange(request: InitPhoneChangeRequest) { - return this.accountService.initPhoneChange(request) - } - - public confirmEmailChange(request: ConfirmEmailChangeRequest) { - return this.accountService.confirmEmailChange(request) - } - - public confirmPhoneChange(request: ConfirmPhoneChangeRequest) { - return this.accountService.confirmPhoneChange(request) +export class AccountClientGrpc extends AbstractGrpcClient { + constructor(@InjectGrpc('ACCOUNT_PACKAGE') client: ClientGrpc) { + super(client, 'AccountService') } } diff --git a/src/modules/account/account.module.ts b/src/modules/account/account.module.ts index 040e978..1c9218e 100644 --- a/src/modules/account/account.module.ts +++ b/src/modules/account/account.module.ts @@ -1,28 +1,11 @@ import { Module } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' -import { ClientsModule, Transport } from '@nestjs/microservices' -import { PROTO_PATHS } from '@teacinema/contracts' +import { GrpcModule } from '@teacinema/common' import { AccountController } from './account.controller' import { AccountClientGrpc } from './account.grpc' @Module({ - imports: [ - ClientsModule.registerAsync([ - { - name: 'ACCOUNT_PACKAGE', - useFactory: (configService: ConfigService) => ({ - transport: Transport.GRPC, - options: { - package: 'account.v1', - protoPath: PROTO_PATHS.ACCOUNT, - url: configService.getOrThrow('AUTH_GRPC_URL') - } - }), - inject: [ConfigService] - } - ]) - ], + imports: [GrpcModule.register(['ACCOUNT_PACKAGE'])], controllers: [AccountController], providers: [AccountClientGrpc], exports: [AccountClientGrpc] diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 5a97aef..0fd6761 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -12,7 +12,6 @@ import { import { ConfigService } from '@nestjs/config' import { ApiOperation } from '@nestjs/swagger' import type { Request, Response } from 'express' -import { lastValueFrom } from 'rxjs' import { AuthClientGrpc } from './auth.grpc' import { @@ -36,7 +35,7 @@ export class AuthController { @Post('otp/send') @HttpCode(HttpStatus.OK) public sendOtp(@Body() dto: SendOtpRequestDto) { - return this.client.sendOtp(dto) + return this.client.call('sendOtp', dto) } @ApiOperation({ @@ -49,8 +48,9 @@ export class AuthController { @Body() dto: VerifyOtpRequestDto, @Res({ passthrough: true }) res: Response ) { - const { accessToken, refreshToken } = await lastValueFrom( - this.client.verifyOtp(dto) + const { accessToken, refreshToken } = await this.client.call( + 'verifyOtp', + dto ) res.cookie('refreshToken', refreshToken, { @@ -75,10 +75,8 @@ export class AuthController { @Res({ passthrough: true }) res: Response ) { const refreshToken = req.cookies?.refreshToken as string - - const { accessToken, refreshToken: newRefreshToken } = await lastValueFrom( - this.client.refresh({ refreshToken }) - ) + const { accessToken, refreshToken: newRefreshToken } = + await this.client.call('refresh', { refreshToken }) res.cookie('refreshToken', newRefreshToken, { httpOnly: true, @@ -105,7 +103,7 @@ export class AuthController { @Get('telegram/init') @HttpCode(HttpStatus.OK) public telegramInit() { - return this.client.telegramInit() + return this.client.call('telegramInit', {}) } @Post('telegram/verify') @@ -117,7 +115,7 @@ export class AuthController { const query = JSON.parse(atob(dto.tgAuthResult)) as { [key: string]: string } - const result = await lastValueFrom(this.client.telegramVerify({ query })) + const result = await this.client.call('telegramVerify', { query }) if (result.url) { return result @@ -146,10 +144,9 @@ export class AuthController { @Body() dto: TelegramFinalyzeRequestDto, @Res({ passthrough: true }) res: Response ) { - const { sessionId } = dto - - const { accessToken, refreshToken } = await lastValueFrom( - this.client.telegramConsume({ sessionId }) + const { accessToken, refreshToken } = await this.client.call( + 'telegramConsume', + dto ) res.cookie('refreshToken', refreshToken, { diff --git a/src/modules/auth/auth.grpc.ts b/src/modules/auth/auth.grpc.ts index 1a33433..65319e1 100644 --- a/src/modules/auth/auth.grpc.ts +++ b/src/modules/auth/auth.grpc.ts @@ -1,45 +1,13 @@ -import { Inject, Injectable, OnModuleInit } from '@nestjs/common' +import { Injectable } from '@nestjs/common' import type { ClientGrpc } from '@nestjs/microservices' -import { - AuthServiceClient, - RefreshRequest, - SendOtpRequest, - TelegramConsumeRequest, - TelegramVerifyRequest, - VerifyOtpRequest -} from '@teacinema/contracts/gen/auth' +import { InjectGrpc } from '@teacinema/common' +import { AuthServiceClient } from '@teacinema/contracts/gen/auth' + +import { AbstractGrpcClient } from '../../shared/grpc/abstract-grpc.client' @Injectable() -export class AuthClientGrpc implements OnModuleInit { - private authService: AuthServiceClient - - constructor(@Inject('AUTH_PACKAGE') private readonly client: ClientGrpc) {} - - onModuleInit() { - this.authService = this.client.getService('AuthService') - } - - public sendOtp(request: SendOtpRequest) { - return this.authService.sendOtp(request) - } - - public verifyOtp(request: VerifyOtpRequest) { - return this.authService.verifyOtp(request) - } - - public refresh(request: RefreshRequest) { - return this.authService.refresh(request) - } - - public telegramInit() { - return this.authService.telegramInit({}) - } - - public telegramVerify(request: TelegramVerifyRequest) { - return this.authService.telegramVerify(request) - } - - public telegramConsume(request: TelegramConsumeRequest) { - return this.authService.telegramConsume(request) +export class AuthClientGrpc extends AbstractGrpcClient { + constructor(@InjectGrpc('AUTH_PACKAGE') client: ClientGrpc) { + super(client, 'AuthService') } } diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index e9acb60..6f1d039 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -1,31 +1,11 @@ import { Module } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' -import { ClientsModule, Transport } from '@nestjs/microservices' -import { PROTO_PATHS } from '@teacinema/contracts' - -import { AccountModule } from '../account/account.module' +import { GrpcModule } from '@teacinema/common' import { AuthController } from './auth.controller' import { AuthClientGrpc } from './auth.grpc' @Module({ - imports: [ - ClientsModule.registerAsync([ - { - name: 'AUTH_PACKAGE', - useFactory: (configService: ConfigService) => ({ - transport: Transport.GRPC, - options: { - package: 'auth.v1', - protoPath: PROTO_PATHS.AUTH, - url: configService.getOrThrow('AUTH_GRPC_URL') - } - }), - inject: [ConfigService] - } - ]), - AccountModule - ], + imports: [GrpcModule.register(['AUTH_PACKAGE'])], controllers: [AuthController], providers: [AuthClientGrpc] }) diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index e21b005..15ded32 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -7,7 +7,6 @@ import { Patch } from '@nestjs/common' import { ApiBearerAuth, ApiOkResponse, ApiOperation } from '@nestjs/swagger' -import { lastValueFrom } from 'rxjs' import { CurrentUser, Protected } from '../../shared/decorators' @@ -32,7 +31,7 @@ export class UsersController { @Get('me') @HttpCode(HttpStatus.OK) public async getMe(@CurrentUser() userId: string) { - const { user } = await lastValueFrom(this.client.getMe({ id: userId })) + const { user } = await this.client.call('getMe', { id: userId }) return user } @@ -49,6 +48,6 @@ export class UsersController { @CurrentUser() userId: string, @Body() dto: PatchUserRequestDto ) { - return this.client.patchUser({ userId, ...dto }) + return this.client.call('patchUser', { userId, ...dto }) } } diff --git a/src/modules/users/users.grpc.ts b/src/modules/users/users.grpc.ts index 8617ad0..6f93401 100644 --- a/src/modules/users/users.grpc.ts +++ b/src/modules/users/users.grpc.ts @@ -1,27 +1,13 @@ -import { Inject, Injectable, OnModuleInit } from '@nestjs/common' +import { Injectable } from '@nestjs/common' import type { ClientGrpc } from '@nestjs/microservices' -import { - GetMeRequest, - PatchUserRequest, - UsersServiceClient -} from '@teacinema/contracts/gen/users' +import { InjectGrpc } from '@teacinema/common' +import { UsersServiceClient } from '@teacinema/contracts/gen/users' + +import { AbstractGrpcClient } from '../../shared/grpc/abstract-grpc.client' @Injectable() -export class UsersClientGrpc implements OnModuleInit { - private usersService: UsersServiceClient - - constructor(@Inject('USERS_PACKAGE') private readonly client: ClientGrpc) {} - - onModuleInit() { - this.usersService = - this.client.getService('UsersService') - } - - public getMe(request: GetMeRequest) { - return this.usersService.getMe(request) - } - - public patchUser(request: PatchUserRequest) { - return this.usersService.patchUser(request) +export class UsersClientGrpc extends AbstractGrpcClient { + constructor(@InjectGrpc('USERS_PACKAGE') client: ClientGrpc) { + super(client, 'UsersService') } } diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts index 482b4c4..222652f 100644 --- a/src/modules/users/users.module.ts +++ b/src/modules/users/users.module.ts @@ -1,28 +1,11 @@ import { Module } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' -import { ClientsModule, Transport } from '@nestjs/microservices' -import { PROTO_PATHS } from '@teacinema/contracts' +import { GrpcModule } from '@teacinema/common' import { UsersController } from './users.controller' import { UsersClientGrpc } from './users.grpc' @Module({ - imports: [ - ClientsModule.registerAsync([ - { - name: 'USERS_PACKAGE', - useFactory: (configService: ConfigService) => ({ - transport: Transport.GRPC, - options: { - package: 'users.v1', - protoPath: PROTO_PATHS.USERS, - url: configService.getOrThrow('USERS_GRPC_URL') - } - }), - inject: [ConfigService] - } - ]) - ], + imports: [GrpcModule.register(['USERS_PACKAGE'])], controllers: [UsersController], providers: [UsersClientGrpc], exports: [UsersClientGrpc] diff --git a/src/shared/grpc/abstract-grpc.client.ts b/src/shared/grpc/abstract-grpc.client.ts new file mode 100644 index 0000000..70fac45 --- /dev/null +++ b/src/shared/grpc/abstract-grpc.client.ts @@ -0,0 +1,32 @@ +import { OnModuleInit } from '@nestjs/common' +import { ClientGrpc } from '@nestjs/microservices' +import { lastValueFrom, Observable } from 'rxjs' + +type UnwrapObservable = U extends Observable ? R : U + +export abstract class AbstractGrpcClient< + T extends Record Observable> +> implements OnModuleInit { + protected service!: T + + protected constructor( + private readonly client: ClientGrpc, + private readonly serviceName: string + ) {} + + public onModuleInit(): any { + this.service = this.client.getService(this.serviceName) + } + + public async call( + method: K, + payload: Parameters[0] + ): Promise>> { + const observable = this.service[method](payload) as Observable< + ReturnType + > + const result = await lastValueFrom(observable) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return result as UnwrapObservable> + } +} diff --git a/src/shared/guards/roles.guard.ts b/src/shared/guards/roles.guard.ts index 93c0c6a..f890855 100644 --- a/src/shared/guards/roles.guard.ts +++ b/src/shared/guards/roles.guard.ts @@ -8,7 +8,6 @@ import { import { Reflector } from '@nestjs/core' import { Role } from '@teacinema/contracts/gen/account' import { Request } from 'express' -import { lastValueFrom } from 'rxjs' import { AccountClientGrpc } from '../../modules/account/account.grpc' import { ROLES_KEY } from '../decorators' @@ -37,9 +36,7 @@ export class RolesGuard implements CanActivate { throw new ForbiddenException('User context missing') } - const account = await lastValueFrom( - this.accountClient.getAccount({ id: user.id }) - ) + const account = await this.accountClient.call('getAccount', { id: user.id }) if (!account) { throw new NotFoundException('Account not found') diff --git a/yarn.lock b/yarn.lock index 5f36557..7efd8a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1022,6 +1022,17 @@ load-esm "1.0.3" tslib "2.8.1" +"@nestjs/common@^11.1.16": + version "11.1.16" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-11.1.16.tgz#1f4061f66d7ae63e407f8d2be2aaeaf83097332b" + integrity sha512-JSIeW+USuMJkkcNbiOdcPkVCeI3TSnXstIVEPpp3HiaKnPRuSbUUKm9TY9o/XpIcPHWUOQItAtC5BiAwFdVITQ== + dependencies: + uid "2.0.2" + file-type "21.3.0" + iterare "1.2.1" + load-esm "1.0.3" + tslib "2.8.1" + "@nestjs/config@^4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-4.0.2.tgz#a2777a1fd2d0d594bab3953f50fbca95c14cce52" @@ -1031,6 +1042,15 @@ dotenv-expand "12.0.1" lodash "4.17.21" +"@nestjs/config@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-4.0.3.tgz#bb24a5f64e4e3ccf6fbf9e02f63303d14d7f308e" + integrity sha512-FQ3M3Ohqfl+nHAn5tp7++wUQw0f2nAk+SFKe8EpNRnIifPqvfJP6JQxPKtFLMOHbyer4X646prFG4zSRYEssQQ== + dependencies: + dotenv "17.2.3" + dotenv-expand "12.0.3" + lodash "4.17.23" + "@nestjs/core@^11.0.1": version "11.1.12" resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-11.1.12.tgz#cdbff023cc87ed071aebafeacb209bc5997da1e4" @@ -1068,6 +1088,14 @@ iterare "1.2.1" tslib "2.8.1" +"@nestjs/microservices@^11.1.16": + version "11.1.16" + resolved "https://registry.yarnpkg.com/@nestjs/microservices/-/microservices-11.1.16.tgz#6f445c44adcee88af983830e0667c93ccefe2869" + integrity sha512-eG/ArIq0UJyR3i/GTYuApA4OZylhuLGacVaVT9mMxQgT7ZTpp5CZgOwLNdcUUdOS6qypK3waG1m2AC54xzdf0Q== + dependencies: + iterare "1.2.1" + tslib "2.8.1" + "@nestjs/platform-express@^11.0.1": version "11.1.12" resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-11.1.12.tgz#c9449230c3b8843370bca65d5b2c9909d3e3f0f6" @@ -1216,11 +1244,16 @@ "@sinonjs/commons" "^3.0.1" "@teacinema/common@^1.0.0": - version "1.0.2" - resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcommon/-/1.0.2/common-1.0.2.tgz#72b98bc641042e8c6a42c2285bfd49d84ea6a195" - integrity sha512-gMPQtWtlVQ5lEm4Z9eO/Iqd+5jyjsoHYR1HgD+JmkTai+Gj+Vz6UN4hNL2tso1mGIe6foNfYozbin2TEbBRzdg== + version "1.1.3" + resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcommon/-/1.1.3/common-1.1.3.tgz#73c8e3642f37d9c6574f6a1e5b542dd6cbfdbf2f" + integrity sha512-Bk2JzG3P1K7YFoFywtSP3zHxNfUpy+NyboqChj+4GZ6JuoWg2V1wkrjMljeE90cPOaqCiNHxk08zX037waiKJg== + dependencies: + "@nestjs/common" "^11.1.16" + "@nestjs/config" "^4.0.3" + "@nestjs/microservices" "^11.1.16" + "@teacinema/contracts" "^1.1.3" -"@teacinema/contracts@^1.0.0": +"@teacinema/contracts@^1.0.0", "@teacinema/contracts@^1.1.3": version "1.1.3" resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.1.3/contracts-1.1.3.tgz#e08600a8dc09977dae35654d372e29e411029e33" integrity sha512-kwcSh2YIxmoBJQ56L+tQpGj12ySwNwf9KRNcUqhPffLPO60PR5aiKA2eN2SfKc23TKp0YoSkdC9LWeor0a9Ysg== @@ -2547,11 +2580,23 @@ dotenv-expand@12.0.1: dependencies: dotenv "^16.4.5" +dotenv-expand@12.0.3: + version "12.0.3" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-12.0.3.tgz#6323ceca51ca0c1b1f0055e2aba39c79781739a6" + integrity sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA== + dependencies: + dotenv "^16.4.5" + dotenv@16.4.7: version "16.4.7" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== +dotenv@17.2.3: + version "17.2.3" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.2.3.tgz#ad995d6997f639b11065f419a22fabf567cdb9a2" + integrity sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w== + dotenv@^16.4.5: version "16.6.1" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" @@ -3918,6 +3963,11 @@ lodash@4.17.21, lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@4.17.23: + version "4.17.23" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" + integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== + log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"