Skip to content

Commit

Permalink
feat: added author service
Browse files Browse the repository at this point in the history
  • Loading branch information
nathan2slime committed Jul 14, 2024
1 parent 184fdf6 commit fc60d4e
Show file tree
Hide file tree
Showing 16 changed files with 363 additions and 20 deletions.
15 changes: 7 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,19 @@
"test:e2e": "NODE_ENV=test jest --config ./tests/jest-e2e.json"
},
"dependencies": {
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mongoose": "^10.0.10",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/terminus": "^10.2.3",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.16.0",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"commitizen": "^4.3.0",
"cookie-parser": "^1.4.6",
"dotenv": "^16.4.5",
"husky": "^9.0.11",
"lint-staged": "^15.2.7",
"mongoose": "^8.5.0",
"mongoose-paginate": "^5.0.3",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
Expand All @@ -51,9 +43,16 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@automock/jest": "^2.1.0",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.16.0",
"commitizen": "^4.3.0",
"@nestjs/cli": "^10.0.0",
"lint-staged": "^15.2.7",
"@nestjs/schematics": "^10.0.0",
"husky": "^9.0.11",
"@nestjs/swagger": "^7.4.0",
"@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^5.0.2",
Expand Down
10 changes: 3 additions & 7 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@ import { MongooseModule } from '@nestjs/mongoose';

import { AuthModule } from '~/app/auth/auth.module';
import { UserModule } from '~/app/user/user.module';
import { AuthorModule } from '~/app/author/author.module';

import { env } from '~/env';

@Module({
imports: [
MongooseModule.forRoot(env.DATABASE_URL, {
connectionFactory: connection => {
connection.plugin(require('mongoose-paginate'));

return connection;
},
}),
MongooseModule.forRoot(env.DATABASE_URL),
AuthModule,
AuthorModule,
UserModule,
],
controllers: [],
Expand Down
23 changes: 23 additions & 0 deletions src/app/auth/role.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

import { Roles } from '~/app/auth/auth.decorator';
import { Session } from '~/schemas/session.schema';

@Injectable()
export class RoleGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get(Roles, context.getHandler());

if (roles) {
const req = context.switchToHttp().getRequest();
const session = req.user as Session;

return roles.includes(session.user.role);
}

return true;
}
}
2 changes: 1 addition & 1 deletion src/app/auth/tests/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as bcrypt from 'bcrypt';

import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from '~/app/auth/auth.service';
import { Model } from 'mongoose';
import { JwtService } from '@nestjs/jwt';
import { getModelToken } from '@nestjs/mongoose';
Expand All @@ -18,6 +17,7 @@ import {
USER_NOT_FOUND,
} from '~/errors';
import { UserService } from '~/app/user/user.service';
import { AuthService } from '~/app/auth/auth.service';

describe('AuthService', () => {
let authService: AuthService;
Expand Down
96 changes: 96 additions & 0 deletions src/app/author/author.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
Body,
Controller,
Delete,
Get,
HttpStatus,
Param,
Post,
Put,
Query,
Res,
UseGuards,
} from '@nestjs/common';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
import { Response } from 'express';

import {
CreateAuthorDto,
SearchAuthorDto,
UpdateAuthorDto,
} from '~/app/author/author.dto';
import { AuthorService } from '~/app/author/author.service';
import { JwtAuthGuard } from '~/app/auth/auth.guard';
import { RoleGuard } from '~/app/auth/role.guard';
import { Roles } from '~/app/auth/auth.decorator';
import { Role } from '~/types/role.enum';

@ApiTags('Author')
@Controller('author')
export class AuthorController {
constructor(private readonly authorService: AuthorService) {}

@Post('create')
@ApiResponse({
status: 201,
description: 'Create new author',
})
@UseGuards(JwtAuthGuard, RoleGuard)
@Roles([Role.ADMIN])
async create(@Body() body: CreateAuthorDto, @Res() res: Response) {
const data = await this.authorService.create(body);

return res.status(HttpStatus.CREATED).json(data);
}

@Get(':id')
@ApiResponse({
status: 200,
description: 'Get a author',
})
async getById(@Param('id') id: string, @Res() res: Response) {
const data = await this.authorService.getById(id);

return res.status(HttpStatus.CREATED).json(data);
}

@Delete('delete/:id')
@ApiResponse({
status: 201,
description: 'Delete a author',
})
@UseGuards(JwtAuthGuard, RoleGuard)
@Roles([Role.ADMIN])
async delete(@Param('id') id: string, @Res() res: Response) {
await this.authorService.delete(id);

return res.status(HttpStatus.OK).send();
}

@Put('update/:id')
@ApiResponse({
status: 201,
description: 'Update a author',
})
@UseGuards(JwtAuthGuard, RoleGuard)
@Roles([Role.ADMIN])
async update(
@Body() body: UpdateAuthorDto,
@Param('id') id: string,
@Res() res: Response,
) {
const data = await this.authorService.update(id, body);

return res.status(HttpStatus.OK).json(data);
}

@Get('search')
@ApiResponse({
status: 200,
description: 'Author search',
})
async search(@Query() query: SearchAuthorDto, @Res() res: Response) {
const data = await this.authorService.search(query);
return res.status(HttpStatus.OK).json(data);
}
}
67 changes: 67 additions & 0 deletions src/app/author/author.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsDateString, IsNotEmpty, IsOptional, Min } from 'class-validator';

import { SortOrder } from '~/types/filter.enum';

export class UpdateAuthorDto {
@ApiProperty({ required: false })
@IsOptional()
@IsNotEmpty()
name: string;

@ApiProperty({ required: false })
@IsOptional()
@IsDateString()
birthDate: Date;

@ApiProperty({ required: false })
@IsOptional()
@IsNotEmpty()
biography: string;
}

export class CreateAuthorDto {
@ApiProperty()
@IsNotEmpty()
name: string;

@ApiProperty()
@IsDateString()
birthDate: Date;

@ApiProperty()
@IsNotEmpty()
biography: string;
}

export class SearchAuthorDto {
@ApiProperty({ required: false })
@Transform(params => {
if (params.value) {
if (params.value.trim().length == 0) return undefined;
}

return params.value;
})
query: string;

@ApiProperty({ default: 1, required: false })
@Transform(params => parseInt(params.value))
@IsOptional()
@Min(1)
page: number = 1;

@ApiProperty({ enum: SortOrder, default: SortOrder.DESC, required: false })
sortOrder: SortOrder;

@ApiProperty({ required: false })
@IsOptional()
sortField: string;

@ApiProperty({ default: 12, required: false })
@Transform(params => parseInt(params.value))
@IsOptional()
@Min(1)
perPage: number = 12;
}
15 changes: 15 additions & 0 deletions src/app/author/author.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

import { AuthorController } from '~/app/author/author.controller';
import { AuthorService } from '~/app/author/author.service';
import { Author, AuthorSchema } from '~/schemas/author.schema';

@Module({
imports: [
MongooseModule.forFeature([{ name: Author.name, schema: AuthorSchema }]),
],
controllers: [AuthorController],
providers: [AuthorService],
})
export class AuthorModule {}
44 changes: 44 additions & 0 deletions src/app/author/author.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { FilterQuery, Model } from 'mongoose';

import {
CreateAuthorDto,
SearchAuthorDto,
UpdateAuthorDto,
} from '~/app/author/author.dto';
import { Author } from '~/schemas/author.schema';
import { paginate } from '~/utils/funcs/pagination';

@Injectable()
export class AuthorService {
constructor(
@InjectModel(Author.name) private readonly authorModel: Model<Author>,
) {}

async create(data: CreateAuthorDto) {
return this.authorModel.create(data);
}

async update(id: string, data: UpdateAuthorDto) {
return this.authorModel.findByIdAndUpdate(id, data, { new: true });
}

async delete(id: string) {
return this.authorModel.findByIdAndDelete(id);
}

async getById(id: string) {
return this.authorModel.findById(id);
}

async search(data: SearchAuthorDto) {
const query: FilterQuery<Author> = {};

if (data.query) {
query.$or = [{ name: { $regex: data.query, $options: 'i' } }];
}

return paginate<Author>(this.authorModel, query, data);
}
}
49 changes: 49 additions & 0 deletions src/app/author/tests/author.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Model } from 'mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { getModelToken } from '@nestjs/mongoose';

import { Author } from '~/schemas/author.schema';
import { AuthorService } from '~/app/author/author.service';
import { CreateAuthorDto } from '~/app/author/author.dto';
import { Entity } from '~/types';

describe('AuthorService', () => {
let authorModel: Model<Author>;
let authorService: AuthorService;

afterEach(() => jest.clearAllMocks());

beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [],
providers: [
AuthorService,
{
provide: getModelToken(Author.name),
useValue: Model,
},
],
}).compile();

authorService = app.get<AuthorService>(AuthorService);
authorModel = app.get<Model<Author>>(getModelToken(Author.name));
});

describe('create', () => {
const payload: CreateAuthorDto = {
biography: expect.anything(),
name: expect.anything(),
birthDate: expect.anything(),
};

const author = {} as Entity<Author>[];
it('should return author created', async () => {
jest.spyOn(authorModel, 'create').mockResolvedValue(author);

const data = await authorService.create(payload);

expect(data).toBe(author);
expect(authorModel.create).toHaveBeenCalledWith(payload);
});
});
});
2 changes: 1 addition & 1 deletion src/app/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { FilterQuery, Model, Types } from 'mongoose';

import { User } from '~/schemas/user.schema';
import { SignUpDto } from '~/app/auth/auth.dto';
import { UpdateUserProfileDto } from './user.dto';
import { UpdateUserProfileDto } from '~/app/user/user.dto';

@Injectable()
export class UserService {
Expand Down
Loading

0 comments on commit fc60d4e

Please sign in to comment.