├── .prettierrc ├── nest-cli.json ├── src ├── utils │ ├── mongoError.enum.ts │ ├── paramsWithId.ts │ ├── paginationParams.ts │ └── mongooseClassSerializer.interceptor.ts ├── authentication │ ├── tokenPayload.interface.ts │ ├── localAuthentication.guard.ts │ ├── jwt-authentication.guard.ts │ ├── requestWithUser.interface.ts │ ├── dto │ │ ├── logIn.dto.ts │ │ └── register.dto.ts │ ├── local.strategy.ts │ ├── jwt.strategy.ts │ ├── authentication.module.ts │ ├── authentication.controller.ts │ └── authentication.service.ts ├── users │ ├── dto │ │ └── createUser.dto.ts │ ├── users.module.ts │ ├── address.schema.ts │ ├── user.schema.ts │ └── users.service.ts ├── series │ ├── dto │ │ └── series.dto.ts │ ├── series.schema.ts │ ├── series.module.ts │ ├── series.service.ts │ └── series.controller.ts ├── categories │ ├── dto │ │ └── category.dto.ts │ ├── category.schema.ts │ ├── categories.module.ts │ ├── categories.service.ts │ └── categories.controller.ts ├── posts │ ├── dto │ │ ├── post.dto.ts │ │ └── updatePost.dto.ts │ ├── posts.module.ts │ ├── post.schema.ts │ ├── posts.controller.ts │ └── posts.service.ts ├── main.ts └── app.module.ts ├── tsconfig.build.json ├── docker-compose.yml ├── tsconfig.json ├── .gitignore ├── .eslintrc.js ├── package.json └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/mongoError.enum.ts: -------------------------------------------------------------------------------- 1 | enum MongoError { 2 | DuplicateKey = 11000, 3 | } 4 | 5 | export default MongoError; 6 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/authentication/tokenPayload.interface.ts: -------------------------------------------------------------------------------- 1 | interface TokenPayload { 2 | userId: string; 3 | } 4 | 5 | export default TokenPayload; 6 | -------------------------------------------------------------------------------- /src/users/dto/createUser.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateUserDto { 2 | email: string; 3 | firstName: string; 4 | password: string; 5 | } 6 | 7 | export default CreateUserDto; 8 | -------------------------------------------------------------------------------- /src/utils/paramsWithId.ts: -------------------------------------------------------------------------------- 1 | import { IsMongoId } from 'class-validator'; 2 | 3 | class ParamsWithId { 4 | @IsMongoId() 5 | id: string; 6 | } 7 | 8 | export default ParamsWithId; 9 | -------------------------------------------------------------------------------- /src/series/dto/series.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty } from 'class-validator'; 2 | 3 | export class SeriesDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | name: string; 7 | } 8 | 9 | export default SeriesDto; 10 | -------------------------------------------------------------------------------- /src/authentication/localAuthentication.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class LocalAuthenticationGuard extends AuthGuard('local') {} 6 | -------------------------------------------------------------------------------- /src/categories/dto/category.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty } from 'class-validator'; 2 | 3 | export class CategoryDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | name: string; 7 | } 8 | 9 | export default CategoryDto; 10 | -------------------------------------------------------------------------------- /src/authentication/jwt-authentication.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export default class JwtAuthenticationGuard extends AuthGuard('jwt') {} 6 | -------------------------------------------------------------------------------- /src/authentication/requestWithUser.interface.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { UserDocument } from '../users/user.schema'; 3 | 4 | interface RequestWithUser extends Request { 5 | user: UserDocument; 6 | } 7 | 8 | export default RequestWithUser; 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | mongo: 4 | image: mongo:latest 5 | environment: 6 | MONGO_INITDB_ROOT_USERNAME: ${MONGO_USERNAME} 7 | MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD} 8 | MONGO_INITDB_DATABASE: ${MONGO_DATABASE} 9 | ports: 10 | - '27017:27017' -------------------------------------------------------------------------------- /src/authentication/dto/logIn.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString, IsNotEmpty, MinLength } from 'class-validator'; 2 | 3 | export class LogInDto { 4 | @IsEmail() 5 | email: string; 6 | 7 | @IsString() 8 | @IsNotEmpty() 9 | @MinLength(7) 10 | password: string; 11 | } 12 | 13 | export default LogInDto; 14 | -------------------------------------------------------------------------------- /src/posts/dto/post.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty } from 'class-validator'; 2 | 3 | export class PostDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | title: string; 7 | 8 | @IsString() 9 | @IsNotEmpty() 10 | content: string; 11 | 12 | categories: string[]; 13 | } 14 | 15 | export default PostDto; 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ValidationPipe } from '@nestjs/common'; 4 | import * as cookieParser from 'cookie-parser'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | app.useGlobalPipes(new ValidationPipe({ transform: true })); 9 | app.use(cookieParser()); 10 | await app.listen(3000); 11 | } 12 | bootstrap(); 13 | -------------------------------------------------------------------------------- /src/authentication/dto/register.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString, IsNotEmpty, MinLength } from 'class-validator'; 2 | 3 | export class RegisterDto { 4 | @IsEmail() 5 | email: string; 6 | 7 | @IsString() 8 | @IsNotEmpty() 9 | firstName: string; 10 | 11 | @IsString() 12 | @IsNotEmpty() 13 | lastName: string; 14 | 15 | @IsString() 16 | @IsNotEmpty() 17 | @MinLength(7) 18 | password: string; 19 | } 20 | 21 | export default RegisterDto; 22 | -------------------------------------------------------------------------------- /src/utils/paginationParams.ts: -------------------------------------------------------------------------------- 1 | import { IsNumber, IsMongoId, Min, IsOptional } from 'class-validator'; 2 | import { Type } from 'class-transformer'; 3 | 4 | export class PaginationParams { 5 | @IsOptional() 6 | @IsMongoId() 7 | startId?: string; 8 | 9 | @IsOptional() 10 | @Type(() => Number) 11 | @IsNumber() 12 | @Min(0) 13 | skip?: number; 14 | 15 | @IsOptional() 16 | @Type(() => Number) 17 | @IsNumber() 18 | @Min(1) 19 | limit?: number; 20 | } 21 | -------------------------------------------------------------------------------- /src/series/series.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import { Document, ObjectId } from 'mongoose'; 3 | import { Transform } from 'class-transformer'; 4 | 5 | export type SeriesDocument = Series & Document; 6 | 7 | @Schema() 8 | export class Series { 9 | @Transform(({ value }) => value.toString()) 10 | _id: ObjectId; 11 | 12 | @Prop() 13 | name: string; 14 | } 15 | 16 | export const SeriesSchema = SchemaFactory.createForClass(Series); 17 | -------------------------------------------------------------------------------- /src/categories/category.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import { Document, ObjectId } from 'mongoose'; 3 | import { Transform } from 'class-transformer'; 4 | 5 | export type CategoryDocument = Category & Document; 6 | 7 | @Schema() 8 | export class Category { 9 | @Transform(({ value }) => value.toString()) 10 | _id: ObjectId; 11 | 12 | @Prop() 13 | name: string; 14 | } 15 | 16 | export const CategorySchema = SchemaFactory.createForClass(Category); 17 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import { UserSchema, User } from './user.schema'; 4 | import PostsModule from '../posts/posts.module'; 5 | import UsersService from './users.service'; 6 | 7 | @Module({ 8 | imports: [ 9 | MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), 10 | PostsModule, 11 | ], 12 | providers: [UsersService], 13 | exports: [UsersService], 14 | }) 15 | export class UsersModule {} 16 | -------------------------------------------------------------------------------- /src/users/address.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import { Document } from 'mongoose'; 3 | import { Transform } from 'class-transformer'; 4 | 5 | export type AddressDocument = Address & Document; 6 | 7 | @Schema() 8 | export class Address { 9 | @Transform(({ value }) => value.toString()) 10 | _id: string; 11 | 12 | @Prop() 13 | city: string; 14 | 15 | @Prop() 16 | street: string; 17 | } 18 | 19 | export const AddressSchema = SchemaFactory.createForClass(Address); 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strict": true, 16 | "noImplicitAny": true, 17 | "strictPropertyInitialization": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/series/series.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import SeriesController from './series.controller'; 4 | import SeriesService from './series.service'; 5 | import { Series, SeriesSchema } from './series.schema'; 6 | 7 | @Module({ 8 | imports: [ 9 | MongooseModule.forFeature([{ name: Series.name, schema: SeriesSchema }]), 10 | ], 11 | controllers: [SeriesController], 12 | providers: [SeriesService], 13 | }) 14 | class SeriesModule {} 15 | 16 | export default SeriesModule; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | .env -------------------------------------------------------------------------------- /src/posts/posts.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import PostsController from './posts.controller'; 4 | import PostsService from './posts.service'; 5 | import { Post, PostSchema } from './post.schema'; 6 | 7 | @Module({ 8 | imports: [ 9 | MongooseModule.forFeature([{ name: Post.name, schema: PostSchema }]), 10 | ], 11 | controllers: [PostsController], 12 | providers: [PostsService], 13 | exports: [PostsService], 14 | }) 15 | class PostsModule {} 16 | 17 | export default PostsModule; 18 | -------------------------------------------------------------------------------- /src/categories/categories.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import CategoriesController from './categories.controller'; 4 | import CategoriesService from './categories.service'; 5 | import { Category, CategorySchema } from './category.schema'; 6 | 7 | @Module({ 8 | imports: [ 9 | MongooseModule.forFeature([ 10 | { name: Category.name, schema: CategorySchema }, 11 | ]), 12 | ], 13 | controllers: [CategoriesController], 14 | providers: [CategoriesService], 15 | }) 16 | class CategoriesModule {} 17 | 18 | export default CategoriesModule; 19 | -------------------------------------------------------------------------------- /src/authentication/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy } from 'passport-local'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { AuthenticationService } from './authentication.service'; 5 | import { UserDocument } from '../users/user.schema'; 6 | 7 | @Injectable() 8 | export class LocalStrategy extends PassportStrategy(Strategy) { 9 | constructor(private authenticationService: AuthenticationService) { 10 | super({ 11 | usernameField: 'email', 12 | }); 13 | } 14 | async validate(email: string, password: string): Promise { 15 | return this.authenticationService.getAuthenticatedUser(email, password); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/posts/dto/updatePost.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty, IsOptional } from 'class-validator'; 2 | import { User } from '../../users/user.schema'; 3 | import { Exclude, Type } from 'class-transformer'; 4 | import { Category } from '../../categories/category.schema'; 5 | import { Series } from '../../series/series.schema'; 6 | 7 | export class UpdatePostDto { 8 | @IsOptional() 9 | @Exclude() 10 | _id: string; 11 | 12 | @IsString() 13 | @IsNotEmpty() 14 | title: string; 15 | 16 | @IsString() 17 | @IsNotEmpty() 18 | content: string; 19 | 20 | @Type(() => Category) 21 | categories: Category[]; 22 | 23 | @Type(() => User) 24 | @IsNotEmpty() 25 | author: User; 26 | 27 | @Type(() => Series) 28 | @IsOptional() 29 | series?: Series; 30 | } 31 | 32 | export default UpdatePostDto; 33 | -------------------------------------------------------------------------------- /src/authentication/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { Request } from 'express'; 6 | import TokenPayload from './tokenPayload.interface'; 7 | import UsersService from '../users/users.service'; 8 | 9 | @Injectable() 10 | export class JwtStrategy extends PassportStrategy(Strategy) { 11 | constructor( 12 | private readonly configService: ConfigService, 13 | private readonly userService: UsersService, 14 | ) { 15 | super({ 16 | jwtFromRequest: ExtractJwt.fromExtractors([ 17 | (request: Request) => { 18 | return request?.cookies?.Authentication; 19 | }, 20 | ]), 21 | secretOrKey: configService.get('JWT_SECRET'), 22 | }); 23 | } 24 | 25 | async validate(payload: TokenPayload) { 26 | return this.userService.getById(payload.userId); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/authentication/authentication.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthenticationService } from './authentication.service'; 3 | import { UsersModule } from '../users/users.module'; 4 | import { AuthenticationController } from './authentication.controller'; 5 | import { PassportModule } from '@nestjs/passport'; 6 | import { LocalStrategy } from './local.strategy'; 7 | import { JwtModule } from '@nestjs/jwt'; 8 | import { ConfigModule, ConfigService } from '@nestjs/config'; 9 | import { JwtStrategy } from './jwt.strategy'; 10 | 11 | @Module({ 12 | imports: [ 13 | UsersModule, 14 | PassportModule, 15 | ConfigModule, 16 | JwtModule.registerAsync({ 17 | imports: [ConfigModule], 18 | inject: [ConfigService], 19 | useFactory: async (configService: ConfigService) => ({ 20 | secret: configService.get('JWT_SECRET'), 21 | signOptions: { 22 | expiresIn: `${configService.get('JWT_EXPIRATION_TIME')}s`, 23 | }, 24 | }), 25 | }), 26 | ], 27 | providers: [AuthenticationService, LocalStrategy, JwtStrategy], 28 | controllers: [AuthenticationController], 29 | }) 30 | export class AuthenticationModule {} 31 | -------------------------------------------------------------------------------- /src/posts/post.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import { Document, ObjectId } from 'mongoose'; 3 | import * as mongoose from 'mongoose'; 4 | import { User } from '../users/user.schema'; 5 | import { Transform, Type } from 'class-transformer'; 6 | import { Category } from '../categories/category.schema'; 7 | import { Series } from '../series/series.schema'; 8 | 9 | export type PostDocument = Post & Document; 10 | 11 | @Schema() 12 | export class Post { 13 | @Transform(({ value }) => value.toString()) 14 | _id: ObjectId; 15 | 16 | @Prop() 17 | title: string; 18 | 19 | @Prop({ 20 | set: (content: string) => { 21 | return content.trim(); 22 | }, 23 | }) 24 | content: string; 25 | 26 | @Prop({ type: mongoose.Schema.Types.ObjectId, ref: User.name }) 27 | @Type(() => User) 28 | author: User; 29 | 30 | @Prop({ 31 | type: [{ type: mongoose.Schema.Types.ObjectId, ref: Category.name }], 32 | }) 33 | @Type(() => Category) 34 | categories: Category[]; 35 | 36 | @Prop({ 37 | type: mongoose.Schema.Types.ObjectId, 38 | ref: Series.name, 39 | }) 40 | @Type(() => Series) 41 | series?: Series; 42 | } 43 | 44 | const PostSchema = SchemaFactory.createForClass(Post); 45 | 46 | PostSchema.index({ title: 'text', content: 'text' }); 47 | 48 | export { PostSchema }; 49 | -------------------------------------------------------------------------------- /src/utils/mongooseClassSerializer.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClassSerializerInterceptor, 3 | PlainLiteralObject, 4 | Type, 5 | } from '@nestjs/common'; 6 | import { ClassTransformOptions, plainToClass } from 'class-transformer'; 7 | import { Document } from 'mongoose'; 8 | 9 | function MongooseClassSerializerInterceptor( 10 | classToIntercept: Type, 11 | ): typeof ClassSerializerInterceptor { 12 | return class Interceptor extends ClassSerializerInterceptor { 13 | private changePlainObjectToClass(document: PlainLiteralObject) { 14 | if (!(document instanceof Document)) { 15 | return document; 16 | } 17 | 18 | return plainToClass(classToIntercept, document.toJSON()); 19 | } 20 | 21 | private prepareResponse( 22 | response: PlainLiteralObject | PlainLiteralObject[], 23 | ): PlainLiteralObject { 24 | if (!Array.isArray(response) && response.results) { 25 | const results = this.prepareResponse(response.results); 26 | return { 27 | ...response, 28 | results, 29 | }; 30 | } 31 | 32 | if (Array.isArray(response)) { 33 | return response.map(this.changePlainObjectToClass); 34 | } 35 | 36 | return this.changePlainObjectToClass(response); 37 | } 38 | 39 | serialize( 40 | response: PlainLiteralObject | PlainLiteralObject[], 41 | options: ClassTransformOptions, 42 | ) { 43 | return super.serialize(this.prepareResponse(response), options); 44 | } 45 | }; 46 | } 47 | 48 | export default MongooseClassSerializerInterceptor; 49 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import { ConfigModule, ConfigService } from '@nestjs/config'; 4 | import PostsModule from './posts/posts.module'; 5 | import * as Joi from '@hapi/joi'; 6 | import { AuthenticationModule } from './authentication/authentication.module'; 7 | import CategoriesModule from './categories/categories.module'; 8 | import SeriesModule from './series/series.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | ConfigModule.forRoot({ 13 | validationSchema: Joi.object({ 14 | MONGO_USERNAME: Joi.string().required(), 15 | MONGO_PASSWORD: Joi.string().required(), 16 | MONGO_DATABASE: Joi.string().required(), 17 | MONGO_HOST: Joi.string().required(), 18 | }), 19 | }), 20 | MongooseModule.forRootAsync({ 21 | imports: [ConfigModule], 22 | useFactory: async (configService: ConfigService) => { 23 | const username = configService.get('MONGO_USERNAME'); 24 | const password = configService.get('MONGO_PASSWORD'); 25 | const database = configService.get('MONGO_DATABASE'); 26 | const host = configService.get('MONGO_HOST'); 27 | 28 | return { 29 | uri: `mongodb://${username}:${password}@${host}`, 30 | dbName: database, 31 | }; 32 | }, 33 | inject: [ConfigService], 34 | }), 35 | PostsModule, 36 | AuthenticationModule, 37 | CategoriesModule, 38 | SeriesModule, 39 | ], 40 | controllers: [], 41 | providers: [], 42 | }) 43 | export class AppModule {} 44 | -------------------------------------------------------------------------------- /src/series/series.service.ts: -------------------------------------------------------------------------------- 1 | import { Model } from 'mongoose'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { InjectModel } from '@nestjs/mongoose'; 4 | import { Series, SeriesDocument } from './series.schema'; 5 | import { NotFoundException } from '@nestjs/common'; 6 | import SeriesDto from './dto/series.dto'; 7 | import { User } from '../users/user.schema'; 8 | 9 | @Injectable() 10 | class SeriesService { 11 | constructor( 12 | @InjectModel(Series.name) private seriesModel: Model, 13 | ) {} 14 | 15 | async findAll() { 16 | return this.seriesModel.find().populate('author'); 17 | } 18 | 19 | async findOne(id: string) { 20 | const series = await this.seriesModel.findById(id).populate('author'); 21 | if (!series) { 22 | throw new NotFoundException(); 23 | } 24 | return series; 25 | } 26 | 27 | create(seriesData: SeriesDto, author: User) { 28 | const createdSeries = new this.seriesModel({ 29 | ...seriesData, 30 | author, 31 | }); 32 | return createdSeries.save(); 33 | } 34 | 35 | async update(id: string, seriesData: SeriesDto) { 36 | const series = await this.seriesModel 37 | .findByIdAndUpdate(id, seriesData) 38 | .setOptions({ overwrite: true, new: true }); 39 | if (!series) { 40 | throw new NotFoundException(); 41 | } 42 | return series; 43 | } 44 | 45 | async delete(seriesId: string) { 46 | const result = await this.seriesModel.findByIdAndDelete(seriesId); 47 | if (!result) { 48 | throw new NotFoundException(); 49 | } 50 | } 51 | } 52 | 53 | export default SeriesService; 54 | -------------------------------------------------------------------------------- /src/series/series.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | Post, 8 | Put, 9 | Req, 10 | UseGuards, 11 | UseInterceptors, 12 | } from '@nestjs/common'; 13 | import SeriesService from './series.service'; 14 | import ParamsWithId from '../utils/paramsWithId'; 15 | import SeriesDto from './dto/series.dto'; 16 | import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard'; 17 | import RequestWithUser from '../authentication/requestWithUser.interface'; 18 | import MongooseClassSerializerInterceptor from '../utils/mongooseClassSerializer.interceptor'; 19 | import { Series } from './series.schema'; 20 | 21 | @Controller('series') 22 | @UseInterceptors(MongooseClassSerializerInterceptor(Series)) 23 | export default class SeriesController { 24 | constructor(private readonly seriesService: SeriesService) {} 25 | 26 | @Get() 27 | async getAllSeries() { 28 | return this.seriesService.findAll(); 29 | } 30 | 31 | @Get(':id') 32 | async getSeries(@Param() { id }: ParamsWithId) { 33 | return this.seriesService.findOne(id); 34 | } 35 | 36 | @Post() 37 | @UseGuards(JwtAuthenticationGuard) 38 | async createSeries(@Body() series: SeriesDto, @Req() req: RequestWithUser) { 39 | return this.seriesService.create(series, req.user); 40 | } 41 | 42 | @Delete(':id') 43 | async deleteSeries(@Param() { id }: ParamsWithId) { 44 | return this.seriesService.delete(id); 45 | } 46 | 47 | @Put(':id') 48 | async updateSeries(@Param() { id }: ParamsWithId, @Body() series: SeriesDto) { 49 | return this.seriesService.update(id, series); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/categories/categories.service.ts: -------------------------------------------------------------------------------- 1 | import { Model } from 'mongoose'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { InjectModel } from '@nestjs/mongoose'; 4 | import { Category, CategoryDocument } from './category.schema'; 5 | import { NotFoundException } from '@nestjs/common'; 6 | import CategoryDto from './dto/category.dto'; 7 | import { User } from '../users/user.schema'; 8 | 9 | @Injectable() 10 | class CategoriesService { 11 | constructor( 12 | @InjectModel(Category.name) private categoryModel: Model, 13 | ) {} 14 | 15 | async findAll() { 16 | return this.categoryModel.find().populate('author'); 17 | } 18 | 19 | async findOne(id: string) { 20 | const category = await this.categoryModel.findById(id).populate('author'); 21 | if (!category) { 22 | throw new NotFoundException(); 23 | } 24 | return category; 25 | } 26 | 27 | create(categoryData: CategoryDto, author: User) { 28 | const createdCategory = new this.categoryModel({ 29 | ...categoryData, 30 | author, 31 | }); 32 | return createdCategory.save(); 33 | } 34 | 35 | async update(id: string, categoryData: CategoryDto) { 36 | const category = await this.categoryModel 37 | .findByIdAndUpdate(id, categoryData) 38 | .setOptions({ overwrite: true, new: true }); 39 | if (!category) { 40 | throw new NotFoundException(); 41 | } 42 | return category; 43 | } 44 | 45 | async delete(categoryId: string) { 46 | const result = await this.categoryModel.findByIdAndDelete(categoryId); 47 | if (!result) { 48 | throw new NotFoundException(); 49 | } 50 | } 51 | } 52 | 53 | export default CategoriesService; 54 | -------------------------------------------------------------------------------- /src/users/user.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import { Document, ObjectId } from 'mongoose'; 3 | import { Exclude, Transform, Type } from 'class-transformer'; 4 | import { Address, AddressSchema } from './address.schema'; 5 | import { Post } from '../posts/post.schema'; 6 | 7 | export type UserDocument = User & Document; 8 | 9 | @Schema({ 10 | toJSON: { 11 | getters: true, 12 | virtuals: true, 13 | }, 14 | }) 15 | export class User { 16 | @Transform(({ value }) => value.toString()) 17 | _id: ObjectId; 18 | 19 | @Prop({ unique: true }) 20 | email: string; 21 | 22 | @Prop() 23 | firstName: string; 24 | 25 | @Prop() 26 | lastName: string; 27 | 28 | fullName: string; 29 | 30 | @Prop() 31 | @Exclude() 32 | password: string; 33 | 34 | @Prop({ type: AddressSchema }) 35 | @Type(() => Address) 36 | address: Address; 37 | 38 | @Prop({ 39 | get: (creditCardNumber: string) => { 40 | if (!creditCardNumber) { 41 | return; 42 | } 43 | const lastFourDigits = creditCardNumber.slice( 44 | creditCardNumber.length - 4, 45 | ); 46 | return `****-****-****-${lastFourDigits}`; 47 | }, 48 | }) 49 | creditCardNumber?: string; 50 | 51 | @Type(() => Post) 52 | posts: Post[]; 53 | } 54 | 55 | const UserSchema = SchemaFactory.createForClass(User); 56 | 57 | UserSchema.index({ firstName: 'text', lastName: 'text' }); 58 | 59 | UserSchema.virtual('fullName').get(function (this: User) { 60 | return `${this.firstName} ${this.lastName}`; 61 | }); 62 | 63 | UserSchema.virtual('posts', { 64 | ref: 'Post', 65 | localField: '_id', 66 | foreignField: 'author', 67 | }); 68 | 69 | export { UserSchema }; 70 | -------------------------------------------------------------------------------- /src/categories/categories.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | Post, 8 | Put, 9 | Req, 10 | UseGuards, 11 | UseInterceptors, 12 | } from '@nestjs/common'; 13 | import CategoriesService from './categories.service'; 14 | import ParamsWithId from '../utils/paramsWithId'; 15 | import CategoryDto from './dto/category.dto'; 16 | import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard'; 17 | import RequestWithUser from '../authentication/requestWithUser.interface'; 18 | import MongooseClassSerializerInterceptor from '../utils/mongooseClassSerializer.interceptor'; 19 | import { Category } from './category.schema'; 20 | 21 | @Controller('categories') 22 | @UseInterceptors(MongooseClassSerializerInterceptor(Category)) 23 | export default class CategoriesController { 24 | constructor(private readonly categoriesService: CategoriesService) {} 25 | 26 | @Get() 27 | async getAllCategories() { 28 | return this.categoriesService.findAll(); 29 | } 30 | 31 | @Get(':id') 32 | async getCategory(@Param() { id }: ParamsWithId) { 33 | return this.categoriesService.findOne(id); 34 | } 35 | 36 | @Post() 37 | @UseGuards(JwtAuthenticationGuard) 38 | async createCategory( 39 | @Body() category: CategoryDto, 40 | @Req() req: RequestWithUser, 41 | ) { 42 | return this.categoriesService.create(category, req.user); 43 | } 44 | 45 | @Delete(':id') 46 | async deleteCategory(@Param() { id }: ParamsWithId) { 47 | return this.categoriesService.delete(id); 48 | } 49 | 50 | @Put(':id') 51 | async updateCategory( 52 | @Param() { id }: ParamsWithId, 53 | @Body() category: CategoryDto, 54 | ) { 55 | return this.categoriesService.update(id, category); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/authentication/authentication.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Req, 4 | Controller, 5 | HttpCode, 6 | Post, 7 | UseGuards, 8 | Get, 9 | UseInterceptors, 10 | } from '@nestjs/common'; 11 | import { AuthenticationService } from './authentication.service'; 12 | import RegisterDto from './dto/register.dto'; 13 | import RequestWithUser from './requestWithUser.interface'; 14 | import { LocalAuthenticationGuard } from './localAuthentication.guard'; 15 | import JwtAuthenticationGuard from './jwt-authentication.guard'; 16 | import { User } from '../users/user.schema'; 17 | import MongooseClassSerializerInterceptor from '../utils/mongooseClassSerializer.interceptor'; 18 | 19 | @Controller('authentication') 20 | @UseInterceptors(MongooseClassSerializerInterceptor(User)) 21 | export class AuthenticationController { 22 | constructor(private readonly authenticationService: AuthenticationService) {} 23 | 24 | @Post('register') 25 | async register(@Body() registrationData: RegisterDto) { 26 | return this.authenticationService.register(registrationData); 27 | } 28 | 29 | @HttpCode(200) 30 | @UseGuards(LocalAuthenticationGuard) 31 | @Post('log-in') 32 | async logIn(@Req() request: RequestWithUser) { 33 | const { user } = request; 34 | const cookie = this.authenticationService.getCookieWithJwtToken(user._id); 35 | request.res?.setHeader('Set-Cookie', cookie); 36 | return user; 37 | } 38 | 39 | @UseGuards(JwtAuthenticationGuard) 40 | @Post('log-out') 41 | @HttpCode(200) 42 | async logOut(@Req() request: RequestWithUser) { 43 | request.res?.setHeader( 44 | 'Set-Cookie', 45 | this.authenticationService.getCookieForLogOut(), 46 | ); 47 | } 48 | 49 | @UseGuards(JwtAuthenticationGuard) 50 | @Get() 51 | authenticate(@Req() request: RequestWithUser) { 52 | return request.user; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/posts/posts.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | Post, 8 | Put, 9 | Query, 10 | Req, 11 | UseGuards, 12 | UseInterceptors, 13 | } from '@nestjs/common'; 14 | import PostsService from './posts.service'; 15 | import ParamsWithId from '../utils/paramsWithId'; 16 | import PostDto from './dto/post.dto'; 17 | import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard'; 18 | import RequestWithUser from '../authentication/requestWithUser.interface'; 19 | import MongooseClassSerializerInterceptor from '../utils/mongooseClassSerializer.interceptor'; 20 | import { Post as PostModel } from './post.schema'; 21 | import { PaginationParams } from '../utils/paginationParams'; 22 | import UpdatePostDto from './dto/updatePost.dto'; 23 | 24 | @Controller('posts') 25 | @UseInterceptors(MongooseClassSerializerInterceptor(PostModel)) 26 | export default class PostsController { 27 | constructor(private readonly postsService: PostsService) {} 28 | 29 | @Get() 30 | async getAllPosts( 31 | @Query() { skip, limit, startId }: PaginationParams, 32 | @Query('searchQuery') searchQuery: string, 33 | ) { 34 | return this.postsService.findAll(skip, limit, startId, searchQuery); 35 | } 36 | 37 | @Get(':id') 38 | async getPost(@Param() { id }: ParamsWithId) { 39 | return this.postsService.findOne(id); 40 | } 41 | 42 | @Post() 43 | @UseGuards(JwtAuthenticationGuard) 44 | async createPost(@Body() post: PostDto, @Req() req: RequestWithUser) { 45 | return this.postsService.create(post, req.user); 46 | } 47 | 48 | @Delete(':id') 49 | async deletePost(@Param() { id }: ParamsWithId) { 50 | return this.postsService.delete(id); 51 | } 52 | 53 | @Put(':id') 54 | async updatePost(@Param() { id }: ParamsWithId, @Body() post: UpdatePostDto) { 55 | return this.postsService.update(id, post); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | import { InjectModel } from '@nestjs/mongoose'; 3 | import { Model } from 'mongoose'; 4 | import { UserDocument, User } from './user.schema'; 5 | import CreateUserDto from './dto/createUser.dto'; 6 | import PostsService from '../posts/posts.service'; 7 | import { InjectConnection } from '@nestjs/mongoose'; 8 | import * as mongoose from 'mongoose'; 9 | 10 | @Injectable() 11 | class UsersService { 12 | constructor( 13 | @InjectModel(User.name) private userModel: Model, 14 | private readonly postsService: PostsService, 15 | @InjectConnection() private readonly connection: mongoose.Connection, 16 | ) {} 17 | 18 | async getByEmail(email: string) { 19 | const user = await this.userModel.findOne({ email }).populate({ 20 | path: 'posts', 21 | populate: { 22 | path: 'categories', 23 | }, 24 | }); 25 | 26 | if (!user) { 27 | throw new NotFoundException(); 28 | } 29 | 30 | return user; 31 | } 32 | 33 | async getById(id: string) { 34 | const user = await this.userModel.findById(id).populate({ 35 | path: 'posts', 36 | populate: { 37 | path: 'categories', 38 | }, 39 | }); 40 | 41 | if (!user) { 42 | throw new NotFoundException(); 43 | } 44 | 45 | return user; 46 | } 47 | 48 | async create(userData: CreateUserDto) { 49 | const createdUser = new this.userModel(userData); 50 | await createdUser 51 | .populate({ 52 | path: 'posts', 53 | populate: { 54 | path: 'categories', 55 | }, 56 | }) 57 | .execPopulate(); 58 | return createdUser.save(); 59 | } 60 | 61 | async delete(userId: string) { 62 | const session = await this.connection.startSession(); 63 | 64 | session.startTransaction(); 65 | try { 66 | const user = await this.userModel 67 | .findByIdAndDelete(userId) 68 | .populate('posts') 69 | .session(session); 70 | 71 | if (!user) { 72 | throw new NotFoundException(); 73 | } 74 | const posts = user.posts; 75 | 76 | await this.postsService.deleteMany( 77 | posts.map((post) => post._id.toString()), 78 | session, 79 | ); 80 | await session.commitTransaction(); 81 | } catch (error) { 82 | await session.abortTransaction(); 83 | throw error; 84 | } finally { 85 | session.endSession(); 86 | } 87 | } 88 | } 89 | 90 | export default UsersService; 91 | -------------------------------------------------------------------------------- /src/authentication/authentication.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; 2 | import RegisterDto from './dto/register.dto'; 3 | import * as bcrypt from 'bcrypt'; 4 | import { JwtService } from '@nestjs/jwt'; 5 | import { ConfigService } from '@nestjs/config'; 6 | import TokenPayload from './tokenPayload.interface'; 7 | import MongoError from '../utils/mongoError.enum'; 8 | import UsersService from '../users/users.service'; 9 | 10 | @Injectable() 11 | export class AuthenticationService { 12 | constructor( 13 | private readonly usersService: UsersService, 14 | private readonly jwtService: JwtService, 15 | private readonly configService: ConfigService, 16 | ) {} 17 | 18 | public async register(registrationData: RegisterDto) { 19 | const hashedPassword = await bcrypt.hash(registrationData.password, 10); 20 | try { 21 | return await this.usersService.create({ 22 | ...registrationData, 23 | password: hashedPassword, 24 | }); 25 | } catch (error) { 26 | if (error?.code === MongoError.DuplicateKey) { 27 | throw new HttpException( 28 | 'User with that email already exists', 29 | HttpStatus.BAD_REQUEST, 30 | ); 31 | } 32 | throw new HttpException( 33 | 'Something went wrong', 34 | HttpStatus.INTERNAL_SERVER_ERROR, 35 | ); 36 | } 37 | } 38 | 39 | public getCookieWithJwtToken(userId: string) { 40 | const payload: TokenPayload = { userId }; 41 | const token = this.jwtService.sign(payload); 42 | return `Authentication=${token}; HttpOnly; Path=/; Max-Age=${this.configService.get( 43 | 'JWT_EXPIRATION_TIME', 44 | )}`; 45 | } 46 | 47 | public getCookieForLogOut() { 48 | return `Authentication=; HttpOnly; Path=/; Max-Age=0`; 49 | } 50 | 51 | public async getAuthenticatedUser(email: string, plainTextPassword: string) { 52 | try { 53 | const user = await this.usersService.getByEmail(email); 54 | await this.verifyPassword(plainTextPassword, user.password); 55 | return user; 56 | } catch (error) { 57 | throw new HttpException( 58 | 'Wrong credentials provided', 59 | HttpStatus.BAD_REQUEST, 60 | ); 61 | } 62 | } 63 | 64 | private async verifyPassword( 65 | plainTextPassword: string, 66 | hashedPassword: string, 67 | ) { 68 | const isPasswordMatching = await bcrypt.compare( 69 | plainTextPassword, 70 | hashedPassword, 71 | ); 72 | if (!isPasswordMatching) { 73 | throw new HttpException( 74 | 'Wrong credentials provided', 75 | HttpStatus.BAD_REQUEST, 76 | ); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-mongodb", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@hapi/joi": "^17.1.1", 25 | "@nestjs/common": "^8.0.0", 26 | "@nestjs/config": "^1.0.1", 27 | "@nestjs/core": "^8.0.0", 28 | "@nestjs/jwt": "^8.0.0", 29 | "@nestjs/mongoose": "^8.0.0", 30 | "@nestjs/passport": "^8.0.1", 31 | "@nestjs/platform-express": "^8.0.0", 32 | "@types/bcrypt": "^5.0.0", 33 | "@types/cookie-parser": "^1.4.2", 34 | "@types/hapi__joi": "^17.1.7", 35 | "@types/passport-jwt": "^3.0.6", 36 | "@types/passport-local": "^1.0.34", 37 | "bcrypt": "^5.0.1", 38 | "class-transformer": "^0.4.0", 39 | "class-validator": "^0.13.1", 40 | "cookie-parser": "^1.4.5", 41 | "mongoose": "^5.13.7", 42 | "passport": "^0.4.1", 43 | "passport-jwt": "^4.0.0", 44 | "passport-local": "^1.0.0", 45 | "reflect-metadata": "^0.1.13", 46 | "rimraf": "^3.0.2", 47 | "rxjs": "^7.2.0" 48 | }, 49 | "devDependencies": { 50 | "@nestjs/cli": "^8.0.0", 51 | "@nestjs/schematics": "^8.0.0", 52 | "@nestjs/testing": "^8.0.0", 53 | "@types/express": "^4.17.13", 54 | "@types/jest": "^26.0.24", 55 | "@types/node": "^16.0.0", 56 | "@types/supertest": "^2.0.11", 57 | "@typescript-eslint/eslint-plugin": "^4.28.2", 58 | "@typescript-eslint/parser": "^4.28.2", 59 | "eslint": "^7.30.0", 60 | "eslint-config-prettier": "^8.3.0", 61 | "eslint-plugin-prettier": "^3.4.0", 62 | "jest": "27.0.6", 63 | "prettier": "^2.3.2", 64 | "supertest": "^6.1.3", 65 | "ts-jest": "^27.0.3", 66 | "ts-loader": "^9.2.3", 67 | "ts-node": "^10.0.0", 68 | "tsconfig-paths": "^3.10.1", 69 | "typescript": "^4.3.5" 70 | }, 71 | "jest": { 72 | "moduleFileExtensions": [ 73 | "js", 74 | "json", 75 | "ts" 76 | ], 77 | "rootDir": "src", 78 | "testRegex": ".*\\.spec\\.ts$", 79 | "transform": { 80 | "^.+\\.(t|j)s$": "ts-jest" 81 | }, 82 | "collectCoverageFrom": [ 83 | "**/*.(t|j)s" 84 | ], 85 | "coverageDirectory": "../coverage", 86 | "testEnvironment": "node" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/posts/posts.service.ts: -------------------------------------------------------------------------------- 1 | import { FilterQuery, Model } from 'mongoose'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { InjectModel } from '@nestjs/mongoose'; 4 | import { Post, PostDocument } from './post.schema'; 5 | import { NotFoundException } from '@nestjs/common'; 6 | import PostDto from './dto/post.dto'; 7 | import { User } from '../users/user.schema'; 8 | import * as mongoose from 'mongoose'; 9 | import UpdatePostDto from './dto/updatePost.dto'; 10 | 11 | @Injectable() 12 | class PostsService { 13 | constructor(@InjectModel(Post.name) private postModel: Model) {} 14 | 15 | async findAll( 16 | documentsToSkip = 0, 17 | limitOfDocuments?: number, 18 | startId?: string, 19 | searchQuery?: string, 20 | ) { 21 | const filters: FilterQuery = startId 22 | ? { 23 | _id: { 24 | $gt: startId, 25 | }, 26 | } 27 | : {}; 28 | 29 | if (searchQuery) { 30 | filters.$text = { 31 | $search: searchQuery, 32 | }; 33 | } 34 | 35 | const findQuery = this.postModel 36 | .find(filters) 37 | .sort({ _id: 1 }) 38 | .skip(documentsToSkip) 39 | .populate('author') 40 | .populate('categories') 41 | .populate('series'); 42 | 43 | if (limitOfDocuments) { 44 | findQuery.limit(limitOfDocuments); 45 | } 46 | 47 | const results = await findQuery; 48 | const count = await this.postModel.count(); 49 | 50 | return { results, count }; 51 | } 52 | 53 | async findOne(id: string) { 54 | const post = await this.postModel 55 | .findById(id) 56 | .populate('author') 57 | .populate('categories') 58 | .populate('series'); 59 | if (!post) { 60 | throw new NotFoundException(); 61 | } 62 | return post; 63 | } 64 | 65 | async create(postData: PostDto, author: User) { 66 | const createdPost = new this.postModel({ 67 | ...postData, 68 | author, 69 | }); 70 | await createdPost.populate('categories').populate('series').execPopulate(); 71 | return createdPost.save(); 72 | } 73 | 74 | async update(id: string, postData: UpdatePostDto) { 75 | const post = await this.postModel 76 | .findOneAndReplace({ _id: id }, postData, { new: true }) 77 | .populate('author') 78 | .populate('categories') 79 | .populate('series'); 80 | if (!post) { 81 | throw new NotFoundException(); 82 | } 83 | return post; 84 | } 85 | 86 | async delete(postId: string) { 87 | const result = await this.postModel.findByIdAndDelete(postId); 88 | if (!result) { 89 | throw new NotFoundException(); 90 | } 91 | } 92 | 93 | async deleteMany( 94 | ids: string[], 95 | session: mongoose.ClientSession | null = null, 96 | ) { 97 | return this.postModel.deleteMany({ _id: ids }).session(session); 98 | } 99 | } 100 | 101 | export default PostsService; 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | --------------------------------------------------------------------------------