Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,100 changes: 2,100 additions & 0 deletions ConduitUpdated.postman_collection.json

Large diffs are not rendered by default.

174 changes: 115 additions & 59 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"@nestjs/microservices": "^7.0.5",
"@nestjs/platform-express": "^7.0.5",
"@nestjs/swagger": "^4.4.0",
"@nestjs/testing": "^7.0.5",
"@nestjs/typeorm": "^7.0.0",
"@nestjs/websockets": "^7.0.5",
"argon2": "^0.26.2",
Expand All @@ -37,7 +36,7 @@
"crypto": "^1.0.1",
"crypto-js": "^4.0.0",
"jsonwebtoken": "^8.5.1",
"mysql": "^2.18.1",
"mysql2": "^2.2.5",
"passport-jwt": "^4.0.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^6.5.5",
Expand All @@ -47,6 +46,7 @@
"typescript": "^3.8.3"
},
"devDependencies": {
"@nestjs/testing": "^7.6.15",
"@types/jest": "^25.2.1",
"@types/node": "^13.13.4",
"atob": ">=2.1.0",
Expand Down
104 changes: 104 additions & 0 deletions src/article/article.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Test, TestingModule } from "@nestjs/testing";
import { ArticleController } from "./article.controller";
import { ArticleService } from "./article.service";
import { UserService } from "../user/user.service";
import { UserController } from "../user/user.controller";
import { UserEntity } from "../user/user.entity";
import { TypeOrmModule } from "@nestjs/typeorm";
import { ArticleEntity } from "./article.entity";
import { UserData, UserRO } from "../user/user.interface";
import { Comment } from "./comment.entity";
import { FollowsEntity } from "../profile/follows.entity";
import { ExecutionContext } from "@nestjs/common";
import * as jwt from "jsonwebtoken";
import { SECRET } from "../config";

describe("ArticleController", () => {
let articleController: ArticleController;
let articleService: ArticleService;
let userController: UserController;
let userService: UserService;
let user: UserEntity;

beforeAll(async () => {
const app: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot(),
TypeOrmModule.forFeature([
ArticleEntity,
Comment,
UserEntity,
FollowsEntity,
]),
],
controllers: [ArticleController, UserController],
providers: [ArticleService, UserService],
}).compile();

articleController = app.get<ArticleController>(ArticleController);
userController = app.get<UserController>(UserController);
articleService = app.get<ArticleService>(ArticleService);
userService = app.get<UserService>(UserService);

let timestamp = Date.now();
let email = `test${timestamp}@test.io`;
let password = "testPASS123";
let username = `user${timestamp}`;

let userRO = await userController.create({
email,
password,
username,
});

userRO = await userController.login({
email,
password,
});

user = jwt.verify(userRO.user.token, SECRET);
});

describe("root", () => {
it("should return article list", () => {
expect(articleController.getFeed(user.id, null)).not.toBeNull();
});
});

describe("root", () => {
it("should create an article", async () => {
const createdArticle = await articleController.create(user.id, {
title: "string",
description: "string",
body: "string",
tagList: [],
});

expect(createdArticle instanceof ArticleEntity).toBe(true);
});

it("should allow to mark an article as mature content on creation", async () => {
const createdArticle = await articleController.create(user.id, {
title: "string",
description: "string",
body: "string",
tagList: [],
isMatureContent: true
});

expect(createdArticle.isMatureContent).toBe(true);
});

it("should set isMatureContent to false by default", async () => {
const createdArticle = await articleController.create(user.id, {
title: "string",
description: "string",
body: "string",
tagList: [],
isMatureContent: false
});

expect(createdArticle.isMatureContent).toBe(false);
});
});
});
24 changes: 23 additions & 1 deletion src/article/article.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Get, Post, Body, Put, Delete, Query, Param, Controller} from '@nestjs/common';
import {Get, Post, Body, Put, Delete, Query, Param, Controller, NotFoundException} from '@nestjs/common';
import { Request } from 'express';
import { ArticleService } from './article.service';
import { CreateArticleDto, CreateCommentDto } from './dto';
Expand Down Expand Up @@ -103,4 +103,26 @@ export class ArticleController {
return await this.articleService.unFavorite(userId, slug);
}

@ApiOperation({ summary: 'Add article to "Read later" list' })
@ApiResponse({ status: 201, description: 'The article has been successfully added to the "Read later" list.'})
@ApiResponse({ status: 403, description: 'Forbidden.' })
@Post(':slug/read-later')
async addReadLater(@User('id') userId: number, @Param('slug') slug) {
const result = await this.articleService.addReadLater(userId, slug);

if (result === null) {
throw new NotFoundException('Article of given slug could not be found!');
}

return result;
}

@ApiOperation({ summary: 'Remove an article from the "Read later" list' })
@ApiResponse({ status: 201, description: 'The article has been successfully removed from the "Read later" list.'})
@ApiResponse({ status: 403, description: 'Forbidden.' })
@Delete(':slug/read-later')
async removeReadLater(@User('id') userId: number, @Param('slug') slug) {
return await this.articleService.removeReadLater(userId, slug);
}

}
3 changes: 3 additions & 0 deletions src/article/article.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export class ArticleEntity {
@Column({default: ''})
body: string;

@Column()
isMatureContent: boolean;

@Column({ type: 'timestamp', default: () => "CURRENT_TIMESTAMP"})
created: Date;

Expand Down
49 changes: 47 additions & 2 deletions src/article/article.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export class ArticleService {
qb.andWhere("article.tagList LIKE :tag", { tag: `%${query.tag}%` });
}

if ('title' in query) {
qb.andWhere("article.title = :title", { title: query.title });
}

if ('author' in query) {
const author = await this.userRepository.findOne({username: query.author});
qb.andWhere("article.authorId = :id", { id: author.id });
Expand Down Expand Up @@ -130,7 +134,8 @@ export class ArticleService {

async favorite(id: number, slug: string): Promise<ArticleRO> {
let article = await this.articleRepository.findOne({slug});
const user = await this.userRepository.findOne(id);

const user = await this.userRepository.findOne(id, {relations: ["favorites"]});

const isNewFavorite = user.favorites.findIndex(_article => _article.id === article.id) < 0;
if (isNewFavorite) {
Expand All @@ -146,7 +151,7 @@ export class ArticleService {

async unFavorite(id: number, slug: string): Promise<ArticleRO> {
let article = await this.articleRepository.findOne({slug});
const user = await this.userRepository.findOne(id);
const user = await this.userRepository.findOne(id, {relations: ["favorites"]});

const deleteIndex = user.favorites.findIndex(_article => _article.id === article.id);

Expand All @@ -162,6 +167,45 @@ export class ArticleService {
return {article};
}

async addReadLater(id: number, slug: string): Promise<ArticleRO> {
let article = await this.articleRepository.findOne({slug});

if (!article) {
return null;
}

const user = await this.userRepository.findOne(id, {relations: ["readLater"]});

const isNewReadLater = !user.readLater || user.readLater.findIndex(_article => _article.id === article.id) < 0;
if (isNewReadLater) {
user.readLater.push(article);

await this.userRepository.save(user);
article = await this.articleRepository.save(article);
}

return {article};
}

async removeReadLater(id: number, slug: string): Promise<ArticleRO> {
let article = await this.articleRepository.findOne({slug});
const user = await this.userRepository.findOne(id, {relations: ["readLater"]});

const deleteIndex = user.readLater.findIndex(_article => _article.id === article.id);

if (deleteIndex >= 0) {

user.readLater.splice(deleteIndex, 1);



await this.userRepository.save(user);
article = await this.articleRepository.save(article);
}

return {article};
}

async findComments(slug: string): Promise<CommentsRO> {
const article = await this.articleRepository.findOne({slug});
return {comments: article.comments};
Expand All @@ -175,6 +219,7 @@ export class ArticleService {
article.slug = this.slugify(articleData.title);
article.tagList = articleData.tagList || [];
article.comments = [];
article.isMatureContent = articleData.isMatureContent === true;

const newArticle = await this.articleRepository.save(article);

Expand Down
1 change: 1 addition & 0 deletions src/article/dto/create-article.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export class CreateArticleDto {
readonly description: string;
readonly body: string;
readonly tagList: string[];
readonly isMatureContent?: boolean;
}
4 changes: 4 additions & 0 deletions src/user/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export class UserEntity {
@JoinTable()
favorites: ArticleEntity[];

@ManyToMany(type => ArticleEntity)
@JoinTable()
readLater: ArticleEntity[];

@OneToMany(type => ArticleEntity, article => article.author)
articles: ArticleEntity[];
}