From 471ac1ba440f04c0606d072f96f8e47f07c6a7b1 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 12 Apr 2026 09:32:09 +0300 Subject: [PATCH] feat: add movie and category services --- src/core/app.module.ts | 4 ++ src/modules/category/category.controller.ts | 20 +++++++ src/modules/category/category.grpc.ts | 13 ++++ src/modules/category/category.module.ts | 13 ++++ src/modules/movie/dto/index.ts | 1 + .../movie/dto/rq/get-movies.request.dto.ts | 46 ++++++++++++++ src/modules/movie/dto/rq/index.ts | 1 + src/modules/movie/movie.controller.ts | 35 +++++++++++ src/modules/movie/movie.grpc.ts | 13 ++++ src/modules/movie/movie.mapper.ts | 13 ++++ src/modules/movie/movie.module.ts | 13 ++++ yarn.lock | 60 +++++++++---------- 12 files changed, 202 insertions(+), 30 deletions(-) create mode 100644 src/modules/category/category.controller.ts create mode 100644 src/modules/category/category.grpc.ts create mode 100644 src/modules/category/category.module.ts create mode 100644 src/modules/movie/dto/index.ts create mode 100644 src/modules/movie/dto/rq/get-movies.request.dto.ts create mode 100644 src/modules/movie/dto/rq/index.ts create mode 100644 src/modules/movie/movie.controller.ts create mode 100644 src/modules/movie/movie.grpc.ts create mode 100644 src/modules/movie/movie.mapper.ts create mode 100644 src/modules/movie/movie.module.ts diff --git a/src/core/app.module.ts b/src/core/app.module.ts index 1b2fcb6..09f0706 100644 --- a/src/core/app.module.ts +++ b/src/core/app.module.ts @@ -4,7 +4,9 @@ import { PassportModule } from '@teacinema/passport' import { AccountModule } from '../modules/account/account.module' import { AuthModule } from '../modules/auth/auth.module' +import { CategoryModule } from '../modules/category/category.module' import { MediaModule } from '../modules/media/media.module' +import { MovieModule } from '../modules/movie/movie.module' import { UsersModule } from '../modules/users/users.module' import { ObservabilityModule } from '../observability/observability.module' @@ -27,6 +29,8 @@ import { getPassportConfig } from './config' AccountModule, MediaModule, UsersModule, + MovieModule, + CategoryModule, PassportModule.registerAsync({ useFactory: getPassportConfig, inject: [ConfigService] diff --git a/src/modules/category/category.controller.ts b/src/modules/category/category.controller.ts new file mode 100644 index 0000000..cddc168 --- /dev/null +++ b/src/modules/category/category.controller.ts @@ -0,0 +1,20 @@ +import { + Controller, + Get, + HttpCode, + HttpStatus, +} from '@nestjs/common' + +import { CategoryClientGrpc } from './category.grpc' + +@Controller('categories') +export class CategoryController { + constructor(private readonly client: CategoryClientGrpc) {} + + @Get() + @HttpCode(HttpStatus.OK) + public async getAll() { + const { categories } = await this.client.call('getAllCategories', {}) + return categories + } +} diff --git a/src/modules/category/category.grpc.ts b/src/modules/category/category.grpc.ts new file mode 100644 index 0000000..018a8e0 --- /dev/null +++ b/src/modules/category/category.grpc.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common' +import type { ClientGrpc } from '@nestjs/microservices' +import { InjectGrpc } from '@teacinema/common' +import { CategoryServiceClient } from '@teacinema/contracts/gen/ts/category' + +import { AbstractGrpcClient } from '../../shared/grpc/abstract-grpc.client' + +@Injectable() +export class CategoryClientGrpc extends AbstractGrpcClient { + constructor(@InjectGrpc('CATEGORY_PACKAGE') client: ClientGrpc) { + super(client, 'CategoryService') + } +} diff --git a/src/modules/category/category.module.ts b/src/modules/category/category.module.ts new file mode 100644 index 0000000..8cfe6f7 --- /dev/null +++ b/src/modules/category/category.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common' +import { GrpcModule } from '@teacinema/common' + +import { CategoryController } from './category.controller' +import { CategoryClientGrpc } from './category.grpc' + +@Module({ + imports: [GrpcModule.register(['CATEGORY_PACKAGE'])], + controllers: [CategoryController], + providers: [CategoryClientGrpc], + exports: [CategoryClientGrpc] +}) +export class CategoryModule {} diff --git a/src/modules/movie/dto/index.ts b/src/modules/movie/dto/index.ts new file mode 100644 index 0000000..66d0236 --- /dev/null +++ b/src/modules/movie/dto/index.ts @@ -0,0 +1 @@ +export * from './rq' diff --git a/src/modules/movie/dto/rq/get-movies.request.dto.ts b/src/modules/movie/dto/rq/get-movies.request.dto.ts new file mode 100644 index 0000000..a71bcf3 --- /dev/null +++ b/src/modules/movie/dto/rq/get-movies.request.dto.ts @@ -0,0 +1,46 @@ +import { ApiPropertyOptional } from '@nestjs/swagger' +import { Transform } from 'class-transformer' +import { + IsBoolean, + IsInt, + IsOptional, + IsString, + Max, + Min +} from 'class-validator' + +export class GetMoviesRequestDto { + @ApiPropertyOptional({ + example: 'classic' + }) + @IsOptional() + @IsString() + // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call + @Transform(({ value }) => value.toString().trim()) + public category: string + + @ApiPropertyOptional({ + example: true + }) + @IsOptional() + @IsBoolean() + @Transform(({ value }) => { + if (value === 'true') return true + if (value === 'false') return false + return value as boolean + }) + public random: boolean + + @ApiPropertyOptional({ + example: 10 + }) + @IsInt() + @IsOptional() + @Min(1) + @Max(100) + @Transform(({ value }) => { + if (value !== undefined) return Number(value) + return undefined + }) + public limit: number +} diff --git a/src/modules/movie/dto/rq/index.ts b/src/modules/movie/dto/rq/index.ts new file mode 100644 index 0000000..0472d0e --- /dev/null +++ b/src/modules/movie/dto/rq/index.ts @@ -0,0 +1 @@ +export * from './get-movies.request.dto' diff --git a/src/modules/movie/movie.controller.ts b/src/modules/movie/movie.controller.ts new file mode 100644 index 0000000..d229bf8 --- /dev/null +++ b/src/modules/movie/movie.controller.ts @@ -0,0 +1,35 @@ +import { + Controller, + Get, + HttpCode, + HttpStatus, + Param, + Query +} from '@nestjs/common' +import { Movie, MovieDetails } from '@teacinema/contracts/gen/ts/movie' + +import { GetMoviesRequestDto } from './dto' +import { MovieClientGrpc } from './movie.grpc' +import { MovieMapper } from './movie.mapper' + +@Controller('movies') +export class MovieController { + constructor(private readonly client: MovieClientGrpc) {} + + @Get() + @HttpCode(HttpStatus.OK) + public async getAll(@Query() dto: GetMoviesRequestDto) { + const { movies } = await this.client.call('listMovies', dto) + // eslint-disable-next-line @typescript-eslint/unbound-method + return movies.map(MovieMapper.toMovie) + } + + @Get(':slug') + @HttpCode(HttpStatus.OK) + public async getBySlug(@Param('slug') slug: string) { + const { movie } = await this.client.call('getMovie', { slug }) + + // @ts-expect-error -- ok + return MovieMapper.toMovie(movie) + } +} diff --git a/src/modules/movie/movie.grpc.ts b/src/modules/movie/movie.grpc.ts new file mode 100644 index 0000000..c7b9a1b --- /dev/null +++ b/src/modules/movie/movie.grpc.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common' +import type { ClientGrpc } from '@nestjs/microservices' +import { InjectGrpc } from '@teacinema/common' +import { MovieServiceClient } from '@teacinema/contracts/gen/ts/movie' + +import { AbstractGrpcClient } from '../../shared/grpc/abstract-grpc.client' + +@Injectable() +export class MovieClientGrpc extends AbstractGrpcClient { + constructor(@InjectGrpc('MOVIE_PACKAGE') client: ClientGrpc) { + super(client, 'MovieService') + } +} diff --git a/src/modules/movie/movie.mapper.ts b/src/modules/movie/movie.mapper.ts new file mode 100644 index 0000000..b26cc93 --- /dev/null +++ b/src/modules/movie/movie.mapper.ts @@ -0,0 +1,13 @@ +import { timestampToISO } from '@teacinema/common' +import { Movie, MovieDetails } from '@teacinema/contracts/gen/ts/movie' + +export class MovieMapper { + public static toMovie(entity: T) { + if (!entity.releaseDate) return entity + + return { + ...entity, + releaseDate: timestampToISO(entity.releaseDate) + } + } +} diff --git a/src/modules/movie/movie.module.ts b/src/modules/movie/movie.module.ts new file mode 100644 index 0000000..ec16121 --- /dev/null +++ b/src/modules/movie/movie.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common' +import { GrpcModule } from '@teacinema/common' + +import { MovieController } from './movie.controller' +import { MovieClientGrpc } from './movie.grpc' + +@Module({ + imports: [GrpcModule.register(['MOVIE_PACKAGE'])], + controllers: [MovieController], + providers: [MovieClientGrpc], + exports: [MovieClientGrpc] +}) +export class MovieModule {} diff --git a/yarn.lock b/yarn.lock index 97b8cdf..742904a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1028,12 +1028,12 @@ tslib "2.8.1" "@nestjs/common@^11.1.16": - version "11.1.17" - resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-11.1.17.tgz#58a330f3f6748cf92b0a244a2a232cfddba1bfd2" - integrity sha512-hLODw5Abp8OQgA+mUO4tHou4krKgDtUcM9j5Ihxncst9XeyxYBTt2bwZm4e4EQr5E352S4Fyy6V3iFx9ggxKAg== + version "11.1.18" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-11.1.18.tgz#329b214aadea5a4a1b12b4eee61c32b9c5b5082c" + integrity sha512-0sLq8Z+TIjLnz1Tqp0C/x9BpLbqpt1qEu0VcH4/fkE0y3F5JxhfK1AdKQ/SPbKhKgwqVDoY4gS8GQr2G6ujaWg== dependencies: uid "2.0.2" - file-type "21.3.2" + file-type "21.3.4" iterare "1.2.1" load-esm "1.0.3" tslib "2.8.1" @@ -1048,13 +1048,13 @@ 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== + version "4.0.4" + resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-4.0.4.tgz#311d806fdc32a8bf29cf9ced903b97cbdb7e064b" + integrity sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow== dependencies: - dotenv "17.2.3" + dotenv "17.4.1" dotenv-expand "12.0.3" - lodash "4.17.23" + lodash "4.18.1" "@nestjs/core@^11.0.1": version "11.1.12" @@ -1094,9 +1094,9 @@ tslib "2.8.1" "@nestjs/microservices@^11.1.16": - version "11.1.17" - resolved "https://registry.yarnpkg.com/@nestjs/microservices/-/microservices-11.1.17.tgz#f2b1aa25e24532377477a437583f1af002118602" - integrity sha512-rubBPEEXS9PwA3LwdNN/ytjEUmON+d8+4fbiUCIFGW8i+rR/g36vrpwuvoAzcGpxlu45/TpZPfHTb0nJciG9Yw== + version "11.1.18" + resolved "https://registry.yarnpkg.com/@nestjs/microservices/-/microservices-11.1.18.tgz#3f5edd756e32f7c85c1c67c65f8ba3b62a736ebf" + integrity sha512-sodzBqqKAcOv5GctTbb0j3vzgx+X3/IxmJ8pBKL9hEmsarsYPkig6dcOcVFmM2Pknm+o6Cxaqv1YZZETISNi+w== dependencies: iterare "1.2.1" tslib "2.8.1" @@ -1997,9 +1997,9 @@ "@sinonjs/commons" "^3.0.1" "@teacinema/common@^1.0.0": - version "1.1.6" - resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcommon/-/1.1.6/common-1.1.6.tgz#4b43fc7c1f19cef0ba138a3a7988918f54f52ebb" - integrity sha512-a0wU0EaJ92PdMlwFQ5pA0EZ9G1/f9Gca8CCN2qUy75q0HldNTGKKYUEyDHx4Y0z16N1e3/RO12gagebOrV6Nbg== + version "1.3.2" + resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcommon/-/1.3.2/common-1.3.2.tgz#51ef68a7a92bb9fbd604052953a10b84846ddd77" + integrity sha512-uD83OWCPZrjDknQNvDGz90WyHDHsRvzn/kE3ofK5oToOOG5f0WXrq0vr1UGi7+DZzTbRfsUe06mgMyahuJ/j4w== dependencies: "@nestjs/common" "^11.1.16" "@nestjs/config" "^4.0.3" @@ -2007,9 +2007,9 @@ "@teacinema/contracts" "^1.1.3" "@teacinema/contracts@^1.0.0", "@teacinema/contracts@^1.1.3": - version "1.1.8" - resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.1.8/contracts-1.1.8.tgz#e96c23a957d2682503c157250672eb4c46bf4ef9" - integrity sha512-wSFTntA0IoQrC/iYz3rylJW6qK6KGWa25oQrl/n55aSOcMk1ACmSBt0aqAPML86SQWAsn7yikpr/foaTgJH3zg== + version "1.2.3" + resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.2.3/contracts-1.2.3.tgz#d356a9dc7395b304a9d5ab639565fa9bcebb9c74" + integrity sha512-Rvw8DVmIpRD/I2Yk1gQyO8ZmxyOIY3FW+purxwiLBIi22gHQtVAXV27a9xK6sO/XXHx70KhrY/8fU8JH/+ehkw== dependencies: "@nestjs/microservices" "^11.1.12" protoc "33.4.0" @@ -3455,10 +3455,10 @@ dotenv@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@17.4.1: + version "17.4.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.4.1.tgz#d8e2179fe287365ef3aecb9459668454168eda88" + integrity sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw== dotenv@^16.4.5: version "16.6.1" @@ -3853,10 +3853,10 @@ file-type@21.3.0: token-types "^6.1.1" uint8array-extras "^1.4.0" -file-type@21.3.2: - version "21.3.2" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-21.3.2.tgz#b28bb3a19ea4b03f59af31546b9d95ee8a3623eb" - integrity sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w== +file-type@21.3.4: + version "21.3.4" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-21.3.4.tgz#e3f902faee8ec4aa152909fc902a7a77f9c06725" + integrity sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g== dependencies: "@tokenizer/inflate" "^0.4.1" strtok3 "^10.3.4" @@ -4909,10 +4909,10 @@ 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== +lodash@4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== log-symbols@^4.1.0: version "4.1.0"