├── .env.example
├── .gitignore
├── .prettierrc
├── README.md
├── index.html
├── nest-cli.json
├── package.json
├── src
├── app.gateway.ts
├── app.module.ts
├── comment
│ ├── comment.controller.spec.ts
│ ├── comment.controller.ts
│ ├── comment.entity.ts
│ ├── comment.module.ts
│ ├── comment.resolver.spec.ts
│ ├── comment.resolver.ts
│ ├── comment.service.spec.ts
│ ├── comment.service.ts
│ └── dto
│ │ └── comment.dto.ts
├── idea
│ ├── dto
│ │ └── idea.dto.ts
│ ├── idea.controller.spec.ts
│ ├── idea.controller.ts
│ ├── idea.entity.ts
│ ├── idea.module.ts
│ ├── idea.resolver.spec.ts
│ ├── idea.resolver.ts
│ ├── idea.service.spec.ts
│ └── idea.service.ts
├── main.ts
├── shared
│ ├── auth.guard.ts
│ ├── http-error.filter.ts
│ ├── logging.interceptor.ts
│ ├── validation.pipe.ts
│ └── votes.enum.ts
└── user
│ ├── Auth.response.ts
│ ├── dto
│ └── user.dto.ts
│ ├── user.controller.spec.ts
│ ├── user.controller.ts
│ ├── user.decorator.ts
│ ├── user.entity.ts
│ ├── user.module.ts
│ ├── user.resolver.spec.ts
│ ├── user.resolver.ts
│ ├── user.service.spec.ts
│ └── user.service.ts
├── test
├── app.e2e-spec.ts
└── jest-e2e.json
├── tsconfig.build.json
├── tsconfig.json
└── tslint.json
/.env.example:
--------------------------------------------------------------------------------
1 | POSTGRES_HOST=
2 | POSTGRES_USERNAME=
3 | POSTGRES_PASSWORD=
4 |
5 | JWT_SECRET=
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 | .env
5 | schema.gql
6 |
7 | # Logs
8 | logs
9 | *.log
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | lerna-debug.log*
14 | yarn.lock
15 |
16 | # OS
17 | .DS_Store
18 |
19 | # Tests
20 | /coverage
21 | /.nyc_output
22 |
23 | # IDEs and editors
24 | /.idea
25 | .project
26 | .classpath
27 | .c9/
28 | *.launch
29 | .settings/
30 | *.sublime-workspace
31 |
32 | # IDE - VSCode
33 | .vscode/*
34 | !.vscode/settings.json
35 | !.vscode/tasks.json
36 | !.vscode/launch.json
37 | !.vscode/extensions.json
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all"
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 | This is a little example of how to use nest.js creating a REST and GraphQL API using Type-GraphQL.
4 |
5 | ## Tips
6 |
7 | Generate new module:
8 |
9 | - `nest g module MODULE_NAME`
10 | - `nest g controller CONTROLLER_NAME # This is for REST api`
11 | - `nest g service SERVICE_NAME`
12 | - `nest g resolver RESOLVER_NAME # This is for GraphQL api`
13 |
14 | Update all the packages:
15 |
16 | `yarn upgrade-interactive --latest`
17 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
17 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "language": "ts",
3 | "collection": "@nestjs/schematics",
4 | "sourceRoot": "src"
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nestjs-graphql-api",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "license": "MIT",
7 | "scripts": {
8 | "build": "rimraf dist && tsc -p tsconfig.build.json",
9 | "format": "prettier --write \"src/**/*.ts\"",
10 | "start": "ts-node -r tsconfig-paths/register src/main.ts",
11 | "start:dev": "tsc-watch -p tsconfig.build.json --onSuccess \"node dist/main.js\"",
12 | "start:debug": "tsc-watch -p tsconfig.build.json --onSuccess \"node --inspect-brk dist/main.js\"",
13 | "start:prod": "node dist/main.js",
14 | "lint": "tslint -p tsconfig.json -c tslint.json",
15 | "test": "jest",
16 | "test:watch": "jest --watch",
17 | "test:cov": "jest --coverage",
18 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
19 | "test:e2e": "jest --config ./test/jest-e2e.json"
20 | },
21 | "dependencies": {
22 | "@nestjs/common": "^6.5.3",
23 | "@nestjs/core": "^6.5.3",
24 | "@nestjs/graphql": "^6.4.2",
25 | "@nestjs/platform-express": "^6.5.3",
26 | "@nestjs/platform-socket.io": "^6.5.3",
27 | "@nestjs/typeorm": "^6.1.3",
28 | "@nestjs/websockets": "^6.5.3",
29 | "apollo-server-express": "^2.7.2",
30 | "argon2": "^0.24.0",
31 | "class-transformer": "^0.2.3",
32 | "class-validator": "^0.9.1",
33 | "dotenv": "^8.0.0",
34 | "graphql": "^14.4.2",
35 | "jsonwebtoken": "^8.5.1",
36 | "pg": "^7.12.0",
37 | "reflect-metadata": "^0.1.12",
38 | "rimraf": "^2.6.2",
39 | "rxjs": "^6.3.3",
40 | "type-graphql": "^0.17.4",
41 | "typeorm": "^0.2.18"
42 | },
43 | "devDependencies": {
44 | "@nestjs/testing": "6.5.3",
45 | "@types/argon2": "^0.15.0",
46 | "@types/dotenv": "^6.1.1",
47 | "@types/express": "4.17.0",
48 | "@types/graphql": "^14.2.3",
49 | "@types/jest": "24.0.15",
50 | "@types/jsonwebtoken": "^8.3.2",
51 | "@types/node": "^12.6.8",
52 | "@types/socket.io": "^2.1.2",
53 | "@types/supertest": "2.0.8",
54 | "jest": "24.8.0",
55 | "prettier": "1.18.2",
56 | "supertest": "4.0.2",
57 | "ts-jest": "24.0.2",
58 | "ts-node": "8.3.0",
59 | "tsc-watch": "2.2.1",
60 | "tsconfig-paths": "3.8.0",
61 | "tslint": "5.18.0",
62 | "typescript": "3.5.3"
63 | },
64 | "jest": {
65 | "moduleFileExtensions": [
66 | "js",
67 | "json",
68 | "ts"
69 | ],
70 | "rootDir": "src",
71 | "testRegex": ".spec.ts$",
72 | "transform": {
73 | "^.+\\.(t|j)s$": "ts-jest"
74 | },
75 | "coverageDirectory": "../coverage",
76 | "testEnvironment": "node"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/app.gateway.ts:
--------------------------------------------------------------------------------
1 | import {
2 | WebSocketGateway,
3 | WebSocketServer,
4 | SubscribeMessage,
5 | } from "@nestjs/websockets";
6 | import { Logger } from "@nestjs/common";
7 | import { Client, Server } from "socket.io";
8 |
9 | @WebSocketGateway(4000)
10 | export class AppGateway {
11 | @WebSocketServer()
12 | wss: Server;
13 |
14 | private logger = new Logger("AppGateway");
15 |
16 | @SubscribeMessage("client")
17 | onEvent(client: Client, data: string): string {
18 | this.logger.log("New client connected" + client);
19 |
20 | return "Successfully connected to server" + data;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from "@nestjs/core";
2 | import { Module } from "@nestjs/common";
3 | import { TypeOrmModule } from "@nestjs/typeorm";
4 | import { GraphQLModule } from "@nestjs/graphql";
5 |
6 | import { IdeaModule } from "./idea/idea.module";
7 | import { HttpErrorFilter } from "./shared/http-error.filter";
8 | import { LoggingInterceptor } from "./shared/logging.interceptor";
9 | import { ValidationPipe } from "./shared/validation.pipe";
10 | import { UserModule } from "./user/user.module";
11 | import { CommentModule } from "./comment/comment.module";
12 |
13 | @Module({
14 | imports: [
15 | TypeOrmModule.forRoot({
16 | type: "postgres",
17 | host: process.env.POSTGRES_HOST,
18 | port: 5432,
19 | username: process.env.POSTGRES_USERNAME,
20 | password: process.env.POSTGRES_PASSWORD,
21 | database: "ideas",
22 | entities: [__dirname + "/**/*.entity{.ts,.js}"],
23 | synchronize: true,
24 | logging: true,
25 | }),
26 | GraphQLModule.forRoot({
27 | playground: process.env.NODE_ENV !== "production",
28 | autoSchemaFile: "schema.gql",
29 | context: ({ req }) => ({ headers: req.headers }),
30 | }),
31 | IdeaModule,
32 | UserModule,
33 | CommentModule,
34 | ],
35 | providers: [
36 | {
37 | provide: APP_FILTER,
38 | useClass: HttpErrorFilter,
39 | },
40 | {
41 | provide: APP_INTERCEPTOR,
42 | useClass: LoggingInterceptor,
43 | },
44 | {
45 | provide: APP_PIPE,
46 | useClass: ValidationPipe,
47 | },
48 | ],
49 | })
50 | export class AppModule {}
51 |
--------------------------------------------------------------------------------
/src/comment/comment.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { CommentController } from './comment.controller';
3 |
4 | describe('Comment Controller', () => {
5 | let controller: CommentController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [CommentController],
10 | }).compile();
11 |
12 | controller = module.get(CommentController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/comment/comment.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Get,
4 | Param,
5 | UseGuards,
6 | Post,
7 | Body,
8 | Delete,
9 | Query,
10 | } from "@nestjs/common";
11 |
12 | import { CommentService } from "./comment.service";
13 | import { AuthGuard } from "../shared/auth.guard";
14 | import { User } from "../user/user.decorator";
15 | import { CommentDTO } from "./dto/comment.dto";
16 |
17 | @Controller("comments")
18 | export class CommentController {
19 | constructor(private readonly commentService: CommentService) {}
20 |
21 | @Get("idea/:id")
22 | public showCommentsByIdea(
23 | @Param("id") idea: string,
24 | @Query("page") page: number,
25 | ) {
26 | return this.commentService.showByIdea(idea, page);
27 | }
28 |
29 | @Get("user/:id")
30 | public showCommentsByUser(
31 | @Param("id") user: string,
32 | @Query("page") page: number,
33 | ) {
34 | return this.commentService.showByUser(user, page);
35 | }
36 |
37 | @Post("idea/:id")
38 | @UseGuards(new AuthGuard())
39 | public createComment(
40 | @Param("id") idea: string,
41 | @User("id") user: string,
42 | @Body() data: CommentDTO,
43 | ) {
44 | return this.commentService.create(idea, user, data);
45 | }
46 |
47 | @Get(":id")
48 | public showComment(@Param("id") id: string) {
49 | return this.commentService.show(id);
50 | }
51 |
52 | @Delete(":id")
53 | @UseGuards(new AuthGuard())
54 | public destroyComment(@Param("id") id: string, @User("id") user: string) {
55 | return this.commentService.destroy(id, user);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/comment/comment.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | CreateDateColumn,
5 | Column,
6 | ManyToOne,
7 | JoinTable,
8 | } from "typeorm";
9 | import { ObjectType, Field, ID } from "type-graphql";
10 |
11 | import { UserEntity } from "../user/user.entity";
12 | import { IdeaEntity } from "../idea/idea.entity";
13 |
14 | @ObjectType()
15 | @Entity("comment")
16 | export class CommentEntity {
17 | @Field(() => ID)
18 | @PrimaryGeneratedColumn("uuid")
19 | id: string;
20 |
21 | @Field(() => Date)
22 | @CreateDateColumn()
23 | created: Date;
24 |
25 | @Field()
26 | @Column("text")
27 | comment: string;
28 |
29 | @ManyToOne(() => UserEntity)
30 | @JoinTable()
31 | author: UserEntity;
32 |
33 | @ManyToOne(() => IdeaEntity, idea => idea.comments)
34 | idea: IdeaEntity;
35 | }
36 |
--------------------------------------------------------------------------------
/src/comment/comment.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from "@nestjs/common";
2 | import { TypeOrmModule } from "@nestjs/typeorm";
3 |
4 | import { CommentController } from "./comment.controller";
5 | import { CommentService } from "./comment.service";
6 | import { IdeaEntity } from "../idea/idea.entity";
7 | import { UserEntity } from "../user/user.entity";
8 | import { CommentEntity } from "./comment.entity";
9 | import { CommentResolver } from "./comment.resolver";
10 |
11 | @Module({
12 | imports: [TypeOrmModule.forFeature([IdeaEntity, UserEntity, CommentEntity])],
13 | controllers: [CommentController],
14 | providers: [CommentService, CommentResolver],
15 | })
16 | export class CommentModule {}
17 |
--------------------------------------------------------------------------------
/src/comment/comment.resolver.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { CommentResolver } from './comment.resolver';
3 |
4 | describe('CommentResolver', () => {
5 | let resolver: CommentResolver;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [CommentResolver],
10 | }).compile();
11 |
12 | resolver = module.get(CommentResolver);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(resolver).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/comment/comment.resolver.ts:
--------------------------------------------------------------------------------
1 | import { Resolver, Query, Args, Mutation, Context } from "@nestjs/graphql";
2 | import { UseGuards } from "@nestjs/common";
3 |
4 | import { CommentEntity } from "./comment.entity";
5 | import { CommentService } from "./comment.service";
6 | import { CommentDTO } from "./dto/comment.dto";
7 | import { AuthGuard } from "../shared/auth.guard";
8 |
9 | @Resolver()
10 | export class CommentResolver {
11 | constructor(private readonly commentService: CommentService) {}
12 |
13 | @Query(() => CommentEntity)
14 | public comment(@Args("id") id: string) {
15 | return this.commentService.show(id);
16 | }
17 |
18 | @Mutation(() => CommentEntity)
19 | @UseGuards(new AuthGuard())
20 | public createComment(
21 | @Args("ideaId") ideaId: string,
22 | @Args("comment") comment: CommentDTO,
23 | @Context("user") { id: userId },
24 | ) {
25 | return this.commentService.create(ideaId, userId, comment);
26 | }
27 |
28 | @Mutation(() => CommentEntity)
29 | @UseGuards(new AuthGuard())
30 | public deleteComment(
31 | @Args("id") id: string,
32 | @Context("user") { id: userId },
33 | ) {
34 | return this.commentService.destroy(id, userId);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/comment/comment.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { CommentService } from './comment.service';
3 |
4 | describe('CommentService', () => {
5 | let service: CommentService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [CommentService],
10 | }).compile();
11 |
12 | service = module.get(CommentService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/comment/comment.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, HttpException, HttpStatus } from "@nestjs/common";
2 | import { InjectRepository } from "@nestjs/typeorm";
3 | import { Repository } from "typeorm";
4 |
5 | import { CommentEntity } from "./comment.entity";
6 | import { IdeaEntity } from "../idea/idea.entity";
7 | import { UserEntity } from "../user/user.entity";
8 | import { CommentDTO } from "./dto/comment.dto";
9 |
10 | @Injectable()
11 | export class CommentService {
12 | constructor(
13 | @InjectRepository(CommentEntity)
14 | private readonly commentRepository: Repository,
15 | @InjectRepository(IdeaEntity)
16 | private readonly ideaRepository: Repository,
17 | @InjectRepository(UserEntity)
18 | private readonly userRepository: Repository,
19 | ) {}
20 |
21 | private toResponseObject(comment: CommentEntity) {
22 | const responseObject: any = {
23 | ...comment,
24 | author: comment.author
25 | ? {
26 | id: comment.author.id,
27 | username: comment.author.username,
28 | created: comment.author.created,
29 | updated: comment.author.created,
30 | }
31 | : null,
32 | idea: comment.idea
33 | ? {
34 | id: comment.idea.id,
35 | idea: comment.idea.idea,
36 | description: comment.idea.description,
37 | created: comment.idea.created,
38 | updated: comment.idea.updated,
39 | }
40 | : null,
41 | };
42 |
43 | return responseObject;
44 | }
45 |
46 | public async showByIdea(ideaId: string, page: number = 1) {
47 | /* const idea = await this.ideaRepository.findOne({
48 | where: { id },
49 | relations: ["comments", "comments.author", "comments.idea"],
50 | }); */
51 |
52 | const comments = await this.commentRepository.find({
53 | where: { idea: { id: ideaId } },
54 | relations: ["author"],
55 | take: 25,
56 | skip: 25 * (page - 1),
57 | });
58 |
59 | return comments.map(idea => this.toResponseObject(idea));
60 | }
61 |
62 | public async showByUser(userId: string, page: number = 1) {
63 | const comments = await this.commentRepository.find({
64 | where: { author: { id: userId } },
65 | relations: ["author"],
66 | take: 25,
67 | skip: 25 * (page - 1),
68 | });
69 |
70 | return comments.map(comment => this.toResponseObject(comment));
71 | }
72 |
73 | public async show(id: string) {
74 | const comment = await this.commentRepository.findOne({
75 | where: { id },
76 | relations: ["author", "idea"],
77 | });
78 |
79 | return this.toResponseObject(comment);
80 | }
81 |
82 | public async create(ideaId: string, userId: string, data: CommentDTO) {
83 | const idea = await this.ideaRepository.findOne({ where: { id: ideaId } });
84 | const user = await this.userRepository.findOne({ where: { id: userId } });
85 |
86 | const comment = await this.commentRepository.create({
87 | ...data,
88 | idea,
89 | author: user,
90 | });
91 |
92 | await this.commentRepository.save(comment);
93 |
94 | return this.toResponseObject(comment);
95 | }
96 |
97 | public async destroy(id: string, userId: string) {
98 | const comment = await this.commentRepository.findOne({
99 | where: { id },
100 | relations: ["author", "idea"],
101 | });
102 |
103 | if (!comment) {
104 | throw new HttpException("Comment not found", HttpStatus.BAD_REQUEST);
105 | }
106 |
107 | if (comment.author.id !== userId) {
108 | throw new HttpException(
109 | "You do not own this comment",
110 | HttpStatus.UNAUTHORIZED,
111 | );
112 | }
113 |
114 | await this.commentRepository.delete(comment);
115 |
116 | return this.toResponseObject(comment);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/comment/dto/comment.dto.ts:
--------------------------------------------------------------------------------
1 | import { Field, InputType } from "type-graphql";
2 | import { IsString } from "class-validator";
3 |
4 | @InputType()
5 | export class CommentDTO {
6 | @Field()
7 | @IsString()
8 | comment: string;
9 | }
10 |
--------------------------------------------------------------------------------
/src/idea/dto/idea.dto.ts:
--------------------------------------------------------------------------------
1 | import { InputType, Field } from "type-graphql";
2 | import { IsString } from "class-validator";
3 |
4 | @InputType()
5 | export class IdeaDTO {
6 | @Field()
7 | @IsString()
8 | readonly idea: string;
9 |
10 | @Field()
11 | @IsString()
12 | readonly description: string;
13 | }
14 |
15 | @InputType()
16 | export class IdeaUpdateDTO {
17 | @Field({ nullable: true })
18 | @IsString()
19 | readonly idea?: string;
20 |
21 | @Field({ nullable: true })
22 | @IsString()
23 | readonly description?: string;
24 | }
25 |
--------------------------------------------------------------------------------
/src/idea/idea.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { IdeaController } from './idea.controller';
3 |
4 | describe('Idea Controller', () => {
5 | let controller: IdeaController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [IdeaController],
10 | }).compile();
11 |
12 | controller = module.get(IdeaController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/idea/idea.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Get,
4 | Post,
5 | Put,
6 | Delete,
7 | Body,
8 | Param,
9 | UseGuards,
10 | Query,
11 | } from "@nestjs/common";
12 |
13 | import { IdeaService } from "./idea.service";
14 | import { IdeaDTO } from "./dto/idea.dto";
15 | import { AuthGuard } from "../shared/auth.guard";
16 | import { User } from "../user/user.decorator";
17 |
18 | @Controller("idea")
19 | export class IdeaController {
20 | constructor(private readonly ideaService: IdeaService) {}
21 |
22 | @Get()
23 | public showAllIdeas(@Query("page") page: number) {
24 | return this.ideaService.showAll(page);
25 | }
26 |
27 | @Get("/newest")
28 | public showNewestIdeas(@Query("page") page: number) {
29 | return this.ideaService.showAll(page, true);
30 | }
31 |
32 | @Post()
33 | @UseGuards(new AuthGuard())
34 | public createIdea(@Body() data: IdeaDTO, @User("id") user) {
35 | return this.ideaService.create(data, user);
36 | }
37 |
38 | @Get(":id")
39 | public readIdea(@Param("id") id: string) {
40 | return this.ideaService.read(id);
41 | }
42 |
43 | @Put(":id")
44 | @UseGuards(new AuthGuard())
45 | public updateIdea(
46 | @Param("id") id: string,
47 | @Body() data: Partial,
48 | @User("id") user,
49 | ) {
50 | return this.ideaService.update(id, data, user);
51 | }
52 |
53 | @Delete(":id")
54 | @UseGuards(new AuthGuard())
55 | public deleteIdea(@Param("id") id: string, @User("id") user: string) {
56 | return this.ideaService.destroy(id, user);
57 | }
58 |
59 | @Post(":id/upvote")
60 | @UseGuards(new AuthGuard())
61 | public upvoteIdea(@Param("id") id: string, @User("id") user: string) {
62 | return this.ideaService.upvote(id, user);
63 | }
64 |
65 | @Post(":id/downvote")
66 | @UseGuards(new AuthGuard())
67 | public downvoteIdea(@Param("id") id: string, @User("id") user: string) {
68 | return this.ideaService.downvote(id, user);
69 | }
70 |
71 | @Post(":id/bookmark")
72 | @UseGuards(new AuthGuard())
73 | public bookmarkIdea(@Param("id") id: string, @User("id") user: string) {
74 | return this.ideaService.bookmark(id, user);
75 | }
76 |
77 | @Delete(":id/bookmark")
78 | @UseGuards(new AuthGuard())
79 | public unbookmarkIdea(@Param("id") id: string, @User("id") user: string) {
80 | return this.ideaService.unbookmark(id, user);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/idea/idea.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | CreateDateColumn,
5 | UpdateDateColumn,
6 | Column,
7 | ManyToOne,
8 | ManyToMany,
9 | JoinTable,
10 | OneToMany,
11 | } from "typeorm";
12 | import { ObjectType, Field, ID, Int } from "type-graphql";
13 |
14 | import { UserEntity } from "../user/user.entity";
15 | import { CommentEntity } from "../comment/comment.entity";
16 |
17 | @ObjectType()
18 | @Entity("idea")
19 | export class IdeaEntity {
20 | @Field(() => ID)
21 | @PrimaryGeneratedColumn("uuid")
22 | id: string;
23 |
24 | @Field(() => Date)
25 | @CreateDateColumn()
26 | created: Date;
27 |
28 | @Field()
29 | @UpdateDateColumn()
30 | updated: Date;
31 |
32 | @Field()
33 | @Column("text")
34 | idea: string;
35 |
36 | @Field()
37 | @Column("text")
38 | description: string;
39 |
40 | @Field(() => UserEntity, { nullable: true })
41 | @ManyToOne(() => UserEntity, author => author.ideas)
42 | author: UserEntity;
43 |
44 | @ManyToMany(() => UserEntity, { cascade: true })
45 | @JoinTable()
46 | upvotes: UserEntity[];
47 |
48 | @Field(() => Int)
49 | upvotesAmount: number;
50 |
51 | @ManyToMany(() => UserEntity, { cascade: true })
52 | @JoinTable()
53 | downvotes: UserEntity[];
54 |
55 | @Field(() => Int)
56 | downvotesAmount: number;
57 |
58 | @Field(() => [CommentEntity])
59 | @OneToMany(() => CommentEntity, comment => comment.idea, { cascade: true })
60 | comments: CommentEntity[];
61 | }
62 |
--------------------------------------------------------------------------------
/src/idea/idea.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from "@nestjs/common";
2 | import { TypeOrmModule } from "@nestjs/typeorm";
3 |
4 | import { AppGateway } from "../app.gateway";
5 | import { IdeaController } from "./idea.controller";
6 | import { IdeaService } from "./idea.service";
7 | import { IdeaEntity } from "./idea.entity";
8 | import { UserEntity } from "../user/user.entity";
9 | import { IdeaResolver } from "./idea.resolver";
10 | import { CommentEntity } from "../comment/comment.entity";
11 | import { CommentService } from "../comment/comment.service";
12 |
13 | @Module({
14 | imports: [TypeOrmModule.forFeature([IdeaEntity, UserEntity, CommentEntity])],
15 | controllers: [IdeaController],
16 | providers: [IdeaService, CommentService, IdeaResolver, AppGateway],
17 | })
18 | export class IdeaModule {}
19 |
--------------------------------------------------------------------------------
/src/idea/idea.resolver.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { IdeaResolver } from './idea.resolver';
3 |
4 | describe('IdeaResolver', () => {
5 | let resolver: IdeaResolver;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [IdeaResolver],
10 | }).compile();
11 |
12 | resolver = module.get(IdeaResolver);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(resolver).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/idea/idea.resolver.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Resolver,
3 | Query,
4 | Args,
5 | ResolveProperty,
6 | Parent,
7 | Mutation,
8 | Context,
9 | } from "@nestjs/graphql";
10 | import { Int } from "type-graphql";
11 | import { UseGuards } from "@nestjs/common";
12 |
13 | import { IdeaService } from "./idea.service";
14 | import { IdeaEntity } from "./idea.entity";
15 | import { CommentService } from "../comment/comment.service";
16 | import { IdeaDTO, IdeaUpdateDTO } from "./dto/idea.dto";
17 | import { AuthGuard } from "../shared/auth.guard";
18 | import { UserEntity } from "../user/user.entity";
19 |
20 | @Resolver(() => IdeaEntity)
21 | export class IdeaResolver {
22 | constructor(
23 | private readonly ideaService: IdeaService,
24 | private readonly commentService: CommentService,
25 | ) {}
26 |
27 | @Query(() => [IdeaEntity])
28 | public ideas(
29 | @Args({ name: "page", type: () => Int, nullable: true }) page?: number,
30 | @Args({ name: "newest", type: () => Boolean, nullable: true })
31 | newest?: boolean,
32 | ) {
33 | return this.ideaService.showAll(page, newest);
34 | }
35 |
36 | @Query(() => IdeaEntity)
37 | public idea(@Args("id") id: string) {
38 | return this.ideaService.read(id);
39 | }
40 |
41 | @Mutation(() => IdeaEntity)
42 | @UseGuards(new AuthGuard())
43 | public createIdea(
44 | @Args("input") input: IdeaDTO,
45 | @Context("user") { id: userId },
46 | ) {
47 | return this.ideaService.create(input, userId);
48 | }
49 |
50 | @Mutation(() => IdeaEntity)
51 | @UseGuards(new AuthGuard())
52 | public updateIdea(
53 | @Args("id") id: string,
54 | @Args("input") input: IdeaUpdateDTO,
55 | @Context("user") { id: userId },
56 | ) {
57 | return this.ideaService.update(id, input, userId);
58 | }
59 |
60 | @Mutation(() => IdeaEntity)
61 | @UseGuards(new AuthGuard())
62 | public deleteIdea(@Args("id") id: string, @Context("user") { id: userId }) {
63 | return this.ideaService.destroy(id, userId);
64 | }
65 |
66 | @Mutation(() => IdeaEntity)
67 | @UseGuards(new AuthGuard())
68 | public upvote(@Args("id") id: string, @Context("user") { id: userId }) {
69 | return this.ideaService.upvote(id, userId);
70 | }
71 |
72 | @Mutation(() => IdeaEntity)
73 | @UseGuards(new AuthGuard())
74 | public downvote(@Args("id") id: string, @Context("user") { id: userId }) {
75 | return this.ideaService.downvote(id, userId);
76 | }
77 |
78 | @Mutation(() => UserEntity)
79 | @UseGuards(new AuthGuard())
80 | public bookmark(@Args("id") id: string, @Context("user") { id: userId }) {
81 | return this.ideaService.bookmark(id, userId);
82 | }
83 |
84 | @Mutation(() => UserEntity)
85 | @UseGuards(new AuthGuard())
86 | public unbookmark(@Args("id") id: string, @Context("user") { id: userId }) {
87 | return this.ideaService.unbookmark(id, userId);
88 | }
89 |
90 | @ResolveProperty("comments")
91 | public comments(@Parent() { id }) {
92 | return this.commentService.showByIdea(id);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/idea/idea.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { IdeaService } from './idea.service';
3 |
4 | describe('IdeaService', () => {
5 | let service: IdeaService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [IdeaService],
10 | }).compile();
11 |
12 | service = module.get(IdeaService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/idea/idea.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, HttpException, HttpStatus } from "@nestjs/common";
2 | import { InjectRepository } from "@nestjs/typeorm";
3 | import { Repository } from "typeorm";
4 |
5 | import { IdeaEntity } from "./idea.entity";
6 | import { IdeaDTO, IdeaUpdateDTO } from "./dto/idea.dto";
7 | import { UserEntity } from "../user/user.entity";
8 | import { Votes } from "../shared/votes.enum";
9 | import { AppGateway } from "../app.gateway";
10 |
11 | @Injectable()
12 | export class IdeaService {
13 | constructor(
14 | @InjectRepository(IdeaEntity)
15 | private readonly ideaRepository: Repository,
16 | @InjectRepository(UserEntity)
17 | private readonly userRepository: Repository,
18 | private readonly geteway: AppGateway,
19 | ) {}
20 |
21 | private toResponseObject(idea: IdeaEntity) {
22 | const responseObject: any = {
23 | ...idea,
24 | author: idea.author
25 | ? {
26 | id: idea.author.id,
27 | username: idea.author.username,
28 | created: idea.author.created,
29 | updated: idea.author.created,
30 | }
31 | : null,
32 | };
33 |
34 | if (responseObject.upvotes) {
35 | responseObject.upvotesAmount = idea.upvotes.length;
36 | }
37 |
38 | if (responseObject.downvotes) {
39 | responseObject.downvotesAmount = idea.downvotes.length;
40 | }
41 |
42 | return responseObject;
43 | }
44 |
45 | private ensureOwnership(idea: IdeaEntity, userId: string) {
46 | if (idea.author.id !== userId) {
47 | throw new HttpException("Incorrect user", HttpStatus.UNAUTHORIZED);
48 | }
49 | }
50 |
51 | private async vote(idea: IdeaEntity, user: UserEntity, vote: Votes) {
52 | const opposite = vote === Votes.UP ? Votes.DOWN : Votes.UP;
53 |
54 | if (
55 | idea[opposite].some(voter => voter.id === user.id) ||
56 | idea[vote].some(voter => voter.id === user.id)
57 | ) {
58 | idea[opposite] = idea[opposite].filter(voter => voter.id !== user.id);
59 | idea[vote] = idea[vote].filter(voter => voter.id !== user.id);
60 |
61 | await this.ideaRepository.save(idea);
62 | } else if (!idea[vote].some(voter => voter.id === user.id)) {
63 | idea[vote].push(user);
64 |
65 | await this.ideaRepository.save(idea);
66 | } else {
67 | throw new HttpException("Unable to cast vote", HttpStatus.BAD_REQUEST);
68 | }
69 |
70 | return idea;
71 | }
72 |
73 | public async showAll(page: number = 1, newest?: boolean) {
74 | const ideas = await this.ideaRepository.find({
75 | relations: ["author", "upvotes", "downvotes", "comments"],
76 | take: 25,
77 | skip: 25 * (page - 1),
78 | order: newest && { created: "DESC" },
79 | });
80 |
81 | return ideas.map(idea => this.toResponseObject(idea));
82 | }
83 |
84 | public async create(data: IdeaDTO, userId: string) {
85 | const user = await this.userRepository.findOne({ where: { id: userId } });
86 | const idea = await this.ideaRepository.create({ ...data, author: user });
87 |
88 | await this.ideaRepository.save(idea);
89 |
90 | this.geteway.wss.emit("newIdea", idea);
91 |
92 | return this.toResponseObject(idea);
93 | }
94 |
95 | public async read(id: string) {
96 | const idea = await this.ideaRepository.findOne({
97 | where: { id },
98 | relations: ["author", "upvotes", "downvotes", "comments"],
99 | });
100 |
101 | if (!idea) {
102 | throw new HttpException("Not found", HttpStatus.NOT_FOUND);
103 | }
104 |
105 | return this.toResponseObject(idea);
106 | }
107 |
108 | public async update(
109 | id: string,
110 | data: Partial,
111 | userId: string,
112 | ) {
113 | let idea = await this.ideaRepository.findOne({
114 | where: { id },
115 | relations: ["author"],
116 | });
117 |
118 | if (!idea) {
119 | throw new HttpException("Not found", HttpStatus.NOT_FOUND);
120 | }
121 |
122 | this.ensureOwnership(idea, userId);
123 |
124 | await this.ideaRepository.update(
125 | { id },
126 | { idea: data.idea, description: data.description },
127 | );
128 |
129 | // return the value updated
130 | idea = await this.ideaRepository.findOne({
131 | where: { id },
132 | relations: ["author"],
133 | });
134 |
135 | return this.toResponseObject(idea);
136 | }
137 |
138 | public async destroy(id: string, userId: string) {
139 | const idea = await this.ideaRepository.findOne({
140 | where: { id },
141 | relations: ["author"],
142 | });
143 |
144 | if (!idea) {
145 | throw new HttpException("Not found", HttpStatus.NOT_FOUND);
146 | }
147 |
148 | this.ensureOwnership(idea, userId);
149 |
150 | await this.ideaRepository.delete({ id });
151 |
152 | return this.toResponseObject(idea);
153 | }
154 |
155 | public async upvote(id: string, userId: string) {
156 | let idea = await this.ideaRepository.findOne({
157 | where: { id },
158 | relations: ["author", "upvotes", "downvotes", "comments"],
159 | });
160 | const user = await this.userRepository.findOne({ where: { id: userId } });
161 |
162 | idea = await this.vote(idea, user, Votes.UP);
163 |
164 | return this.toResponseObject(idea);
165 | }
166 |
167 | public async downvote(id: string, userId: string) {
168 | let idea = await this.ideaRepository.findOne({
169 | where: { id },
170 | relations: ["author", "upvotes", "downvotes", "comments"],
171 | });
172 | const user = await this.userRepository.findOne({ where: { id: userId } });
173 |
174 | idea = await this.vote(idea, user, Votes.DOWN);
175 |
176 | return this.toResponseObject(idea);
177 | }
178 |
179 | public async bookmark(id: string, userId: string) {
180 | const idea = await this.ideaRepository.findOne({ where: { id } });
181 | const user = await this.userRepository.findOne({
182 | where: { id: userId },
183 | relations: ["bookmarks"],
184 | });
185 |
186 | if (!user.bookmarks.some(bookmark => bookmark.id === idea.id)) {
187 | user.bookmarks.push(idea);
188 | await this.userRepository.save(user);
189 | } else {
190 | throw new HttpException(
191 | "Idea already bookmarked",
192 | HttpStatus.BAD_REQUEST,
193 | );
194 | }
195 |
196 | return user;
197 | }
198 |
199 | public async unbookmark(id: string, userId: string) {
200 | const idea = await this.ideaRepository.findOne({ where: { id } });
201 | const user = await this.userRepository.findOne({
202 | where: { id: userId },
203 | relations: ["bookmarks"],
204 | });
205 |
206 | if (user.bookmarks.some(bookmark => bookmark.id === idea.id)) {
207 | user.bookmarks = user.bookmarks.filter(
208 | bookmark => bookmark.id !== idea.id,
209 | );
210 |
211 | await this.userRepository.save(user);
212 | } else {
213 | throw new HttpException(
214 | "Idea already bookmarked",
215 | HttpStatus.BAD_REQUEST,
216 | );
217 | }
218 |
219 | return user;
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import "dotenv/config";
2 | import { NestFactory } from "@nestjs/core";
3 |
4 | import { AppModule } from "./app.module";
5 |
6 | async function bootstrap() {
7 | const app = await NestFactory.create(AppModule);
8 | await app.listen(process.env.PORT || 3000);
9 | }
10 |
11 | bootstrap();
12 |
--------------------------------------------------------------------------------
/src/shared/auth.guard.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | CanActivate,
4 | ExecutionContext,
5 | HttpStatus,
6 | HttpException,
7 | } from "@nestjs/common";
8 | import * as jwt from "jsonwebtoken";
9 | import { GqlExecutionContext } from "@nestjs/graphql";
10 |
11 | @Injectable()
12 | export class AuthGuard implements CanActivate {
13 | async canActivate(context: ExecutionContext): Promise {
14 | const request = context.switchToHttp().getRequest();
15 |
16 | if (request) {
17 | if (!request.headers.authorization) {
18 | return false;
19 | }
20 |
21 | const decoded = await this.validateToken(request.headers.authorization);
22 | request.user = decoded;
23 |
24 | return true;
25 | } else {
26 | const ctx = GqlExecutionContext.create(context).getContext();
27 |
28 | if (!ctx.headers.authorization) {
29 | return false;
30 | }
31 |
32 | ctx.user = await this.validateToken(ctx.headers.authorization);
33 |
34 | return true;
35 | }
36 | }
37 |
38 | private async validateToken(auth: string) {
39 | const token = auth.split(" ");
40 |
41 | if (token[0] !== "Bearer") {
42 | throw new HttpException("Invalid token", HttpStatus.FORBIDDEN);
43 | }
44 |
45 | try {
46 | const decode = await jwt.verify(token[1], process.env.JWT_SECRET);
47 |
48 | return decode;
49 | } catch (err) {
50 | const message = "Token error: " + (err.message || err.name);
51 | throw new HttpException(message, HttpStatus.FORBIDDEN);
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/shared/http-error.filter.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Catch,
3 | ExceptionFilter,
4 | HttpException,
5 | ArgumentsHost,
6 | Logger,
7 | HttpStatus,
8 | } from "@nestjs/common";
9 | import { Request, Response } from "express";
10 | import { GqlArgumentsHost, GqlExceptionFilter } from "@nestjs/graphql";
11 | import { GraphQLResolveInfo } from "graphql";
12 |
13 | @Catch()
14 | export class HttpErrorFilter implements ExceptionFilter, GqlExceptionFilter {
15 | catch(exception: HttpException, host: ArgumentsHost) {
16 | const ctx = host.switchToHttp();
17 | const response = ctx.getResponse();
18 | const request = ctx.getRequest();
19 |
20 | const gqlHost = GqlArgumentsHost.create(host);
21 | const info = gqlHost.getInfo();
22 |
23 | const status = exception.getStatus
24 | ? exception.getStatus()
25 | : HttpStatus.INTERNAL_SERVER_ERROR;
26 |
27 | if (status === HttpStatus.INTERNAL_SERVER_ERROR) {
28 | // tslint:disable-next-line: no-console
29 | console.error(exception);
30 | }
31 |
32 | const errorResponse = {
33 | statusCode: status,
34 | timestamp: new Date().toLocaleDateString(),
35 | error:
36 | status !== HttpStatus.INTERNAL_SERVER_ERROR
37 | ? exception.message.error || exception.message || null
38 | : "Internal server error",
39 | };
40 |
41 | // This is for REST petitions
42 | if (request) {
43 | const error = {
44 | ...errorResponse,
45 | path: request.url,
46 | method: request.method,
47 | };
48 |
49 | Logger.error(
50 | `${request.method} ${request.url}`,
51 | JSON.stringify(error),
52 | "ExceptionFilter",
53 | );
54 |
55 | response.status(status).json(errorResponse);
56 | } else {
57 | // This is for GRAPHQL petitions
58 | const error = {
59 | ...errorResponse,
60 | type: info.parentType,
61 | field: info.fieldName,
62 | };
63 |
64 | Logger.error(
65 | `${info.parentType} ${info.fieldName}`,
66 | JSON.stringify(error),
67 | "ExceptionFilter",
68 | );
69 |
70 | return exception;
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/shared/logging.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | NestInterceptor,
4 | ExecutionContext,
5 | CallHandler,
6 | Logger,
7 | } from "@nestjs/common";
8 | import { Request } from "express";
9 | import { Observable } from "rxjs";
10 | import { tap } from "rxjs/operators";
11 | import { GqlExecutionContext } from "@nestjs/graphql";
12 |
13 | @Injectable()
14 | export class LoggingInterceptor implements NestInterceptor {
15 | intercept(context: ExecutionContext, next: CallHandler): Observable {
16 | const request = context.switchToHttp().getRequest();
17 | const now = Date.now();
18 |
19 | // This is for REST petitions
20 | if (request) {
21 | return next
22 | .handle()
23 | .pipe(
24 | tap(() =>
25 | Logger.log(
26 | `${request.method} ${request.url} ${Date.now() - now}ms`,
27 | `${context.getClass().name}.${context.getHandler().name}`,
28 | ),
29 | ),
30 | );
31 | } else {
32 | // This is for GRAPHQL petitions
33 | const ctx: any = GqlExecutionContext.create(context);
34 | const resolverName = ctx.constructorRef.name;
35 | const info = ctx.getInfo();
36 |
37 | return next
38 | .handle()
39 | .pipe(
40 | tap(() =>
41 | Logger.log(
42 | `${info.parentType} "${info.fieldName}" ${Date.now() - now}ms`,
43 | resolverName,
44 | ),
45 | ),
46 | );
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/shared/validation.pipe.ts:
--------------------------------------------------------------------------------
1 | import {
2 | PipeTransform,
3 | Injectable,
4 | ArgumentMetadata,
5 | HttpException,
6 | HttpStatus,
7 | } from "@nestjs/common";
8 | import { validate, ValidationError } from "class-validator";
9 | import { plainToClass } from "class-transformer";
10 |
11 | @Injectable()
12 | export class ValidationPipe implements PipeTransform {
13 | async transform(value: any, { metatype }: ArgumentMetadata) {
14 | if (value instanceof Object && this.isEmpty(value)) {
15 | throw new HttpException(
16 | "Validation failed: No body submitted",
17 | HttpStatus.BAD_REQUEST,
18 | );
19 | }
20 |
21 | if (!metatype || !this.toValidate(metatype)) {
22 | return value;
23 | }
24 |
25 | const object = plainToClass(metatype, value);
26 | const errors = await validate(object);
27 |
28 | if (errors.length > 0) {
29 | throw new HttpException(
30 | this.formatErrors(errors),
31 | HttpStatus.BAD_REQUEST,
32 | );
33 | }
34 |
35 | return value;
36 | }
37 |
38 | private toValidate(metatype: Function): boolean {
39 | const types: Function[] = [String, Boolean, Number, Array, Object];
40 |
41 | return !types.includes(metatype);
42 | }
43 |
44 | private formatErrors(errors: ValidationError[]) {
45 | return errors.map(err => {
46 | for (const property in err.constraints) {
47 | if (err.constraints.hasOwnProperty(property)) {
48 | return {
49 | path: err.property,
50 | message: err.constraints[property],
51 | };
52 | }
53 | }
54 | });
55 | }
56 |
57 | private isEmpty(value: any) {
58 | if (Object.keys(value).length > 0) {
59 | return false;
60 | }
61 |
62 | return true;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/shared/votes.enum.ts:
--------------------------------------------------------------------------------
1 | export enum Votes {
2 | UP = "upvotes",
3 | DOWN = "downvotes",
4 | }
5 |
--------------------------------------------------------------------------------
/src/user/Auth.response.ts:
--------------------------------------------------------------------------------
1 | import { ObjectType, Field } from "type-graphql";
2 |
3 | @ObjectType()
4 | export class Auth {
5 | @Field()
6 | username: string;
7 |
8 | @Field({ nullable: true })
9 | token?: string;
10 | }
11 |
--------------------------------------------------------------------------------
/src/user/dto/user.dto.ts:
--------------------------------------------------------------------------------
1 | import { InputType, Field } from "type-graphql";
2 | import { IsNotEmpty } from "class-validator";
3 |
4 | @InputType()
5 | export class UserDTO {
6 | @Field()
7 | @IsNotEmpty()
8 | username: string;
9 |
10 | @Field()
11 | @IsNotEmpty()
12 | password: string;
13 | }
14 |
--------------------------------------------------------------------------------
/src/user/user.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { UserController } from './user.controller';
3 |
4 | describe('User Controller', () => {
5 | let controller: UserController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [UserController],
10 | }).compile();
11 |
12 | controller = module.get(UserController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/user/user.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Get, Body, UseGuards, Query } from "@nestjs/common";
2 |
3 | import { UserService } from "./user.service";
4 | import { UserDTO } from "./dto/user.dto";
5 | import { AuthGuard } from "../shared/auth.guard";
6 | import { User } from "./user.decorator";
7 |
8 | @Controller()
9 | export class UserController {
10 | constructor(private readonly userService: UserService) {}
11 |
12 | @Get("users")
13 | @UseGuards(new AuthGuard())
14 | showAllusers(@User("id") user, @Query("page") page: number) {
15 | // tslint:disable-next-line: no-console
16 | console.log(user);
17 | return this.userService.showAll(page);
18 | }
19 |
20 | @Post("login")
21 | login(@Body() data: UserDTO) {
22 | return this.userService.login(data);
23 | }
24 |
25 | @Post("register")
26 | register(@Body() data: UserDTO) {
27 | return this.userService.register(data);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/user/user.decorator.ts:
--------------------------------------------------------------------------------
1 | import { createParamDecorator } from "@nestjs/common";
2 |
3 | export const User = createParamDecorator((data, request) => {
4 | return data ? request.user[data] : request.user;
5 | });
6 |
--------------------------------------------------------------------------------
/src/user/user.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | CreateDateColumn,
5 | Column,
6 | BeforeInsert,
7 | OneToMany,
8 | UpdateDateColumn,
9 | ManyToMany,
10 | JoinTable,
11 | } from "typeorm";
12 | import { ObjectType, Field, ID } from "type-graphql";
13 | import * as argon2 from "argon2";
14 |
15 | import { IdeaEntity } from "../idea/idea.entity";
16 | import { CommentEntity } from "../comment/comment.entity";
17 |
18 | @ObjectType()
19 | @Entity("user")
20 | export class UserEntity {
21 | @Field(() => ID)
22 | @PrimaryGeneratedColumn()
23 | id: string;
24 |
25 | @Field(() => Date)
26 | @CreateDateColumn()
27 | created: Date;
28 |
29 | @Field(() => Date)
30 | @UpdateDateColumn()
31 | updated: Date;
32 |
33 | @Field()
34 | @Column({
35 | type: "text",
36 | unique: true,
37 | })
38 | username: string;
39 |
40 | @Column("text")
41 | password: string;
42 |
43 | @Field(() => [IdeaEntity])
44 | @OneToMany(() => IdeaEntity, idea => idea.author)
45 | ideas: IdeaEntity[];
46 |
47 | @Field(() => [IdeaEntity])
48 | @ManyToMany(() => IdeaEntity, { cascade: true })
49 | @JoinTable()
50 | bookmarks: IdeaEntity[];
51 |
52 | @Field(() => [CommentEntity])
53 | comments: CommentEntity[];
54 |
55 | @BeforeInsert()
56 | async hashPassword() {
57 | this.password = await argon2.hash(this.password);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/user/user.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from "@nestjs/common";
2 | import { TypeOrmModule } from "@nestjs/typeorm";
3 |
4 | import { UserController } from "./user.controller";
5 | import { UserService } from "./user.service";
6 | import { UserEntity } from "./user.entity";
7 | import { UserResolver } from "./user.resolver";
8 | import { CommentEntity } from "../comment/comment.entity";
9 | import { CommentService } from "../comment/comment.service";
10 | import { IdeaEntity } from "../idea/idea.entity";
11 |
12 | @Module({
13 | imports: [TypeOrmModule.forFeature([UserEntity, IdeaEntity, CommentEntity])],
14 | controllers: [UserController],
15 | providers: [UserService, CommentService, UserResolver],
16 | })
17 | export class UserModule {}
18 |
--------------------------------------------------------------------------------
/src/user/user.resolver.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { UserResolver } from './user.resolver';
3 |
4 | describe('UserResolver', () => {
5 | let resolver: UserResolver;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [UserResolver],
10 | }).compile();
11 |
12 | resolver = module.get(UserResolver);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(resolver).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/user/user.resolver.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Resolver,
3 | Query,
4 | Args,
5 | ResolveProperty,
6 | Parent,
7 | Mutation,
8 | Context,
9 | } from "@nestjs/graphql";
10 | import { UseGuards } from "@nestjs/common";
11 | import { Int } from "type-graphql";
12 |
13 | import { UserEntity } from "./user.entity";
14 | import { UserService } from "./user.service";
15 | import { CommentService } from "../comment/comment.service";
16 | import { UserDTO } from "./dto/user.dto";
17 | import { Auth } from "./Auth.response";
18 | import { AuthGuard } from "../shared/auth.guard";
19 |
20 | @Resolver(() => UserEntity)
21 | export class UserResolver {
22 | constructor(
23 | private readonly userService: UserService,
24 | private readonly commentService: CommentService,
25 | ) {}
26 |
27 | @Query(() => [UserEntity])
28 | public async users(
29 | @Args({ name: "page", type: () => Int, nullable: true }) page?: number,
30 | ) {
31 | const users = await this.userService.showAll(page);
32 |
33 | return users;
34 | }
35 |
36 | @Query(() => UserEntity)
37 | public user(@Args("username") username: string) {
38 | return this.userService.read(username);
39 | }
40 |
41 | @Query(() => String)
42 | @UseGuards(new AuthGuard())
43 | public whoami(@Context("user") { username }) {
44 | return username;
45 | }
46 |
47 | @Mutation(() => Auth)
48 | public async login(@Args("input") input: UserDTO) {
49 | const response = await this.userService.login(input);
50 |
51 | return {
52 | token: response.token,
53 | username: response.user.username,
54 | };
55 | }
56 |
57 | @Mutation(() => Auth)
58 | public async register(@Args("input") input: UserDTO) {
59 | const response = await this.userService.register(input);
60 |
61 | return {
62 | username: response.username,
63 | };
64 | }
65 |
66 | @ResolveProperty("comments")
67 | public comments(@Parent() { id }) {
68 | return this.commentService.showByUser(id);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/user/user.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { UserService } from './user.service';
3 |
4 | describe('UserService', () => {
5 | let service: UserService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [UserService],
10 | }).compile();
11 |
12 | service = module.get(UserService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/user/user.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, HttpException, HttpStatus } from "@nestjs/common";
2 | import { InjectRepository } from "@nestjs/typeorm";
3 | import { Repository } from "typeorm";
4 | import * as argon2 from "argon2";
5 | import * as jwt from "jsonwebtoken";
6 |
7 | import { UserEntity } from "./user.entity";
8 | import { UserDTO } from "./dto/user.dto";
9 |
10 | @Injectable()
11 | export class UserService {
12 | constructor(
13 | @InjectRepository(UserEntity)
14 | private readonly userRepository: Repository,
15 | ) {}
16 |
17 | async showAll(page: number = 1) {
18 | const users = await this.userRepository.find({
19 | select: ["id", "username", "created"],
20 | relations: ["ideas", "bookmarks"],
21 | take: 25,
22 | skip: 25 * (page - 1),
23 | });
24 |
25 | return users;
26 | }
27 |
28 | async read(username: string) {
29 | const user = await this.userRepository.findOne({
30 | where: { username },
31 | relations: ["ideas", "bookmarks"],
32 | });
33 |
34 | return user;
35 | }
36 |
37 | async login({ username, password }: UserDTO) {
38 | const user = await this.userRepository.findOne({ where: { username } });
39 |
40 | if (!user) {
41 | throw new HttpException(
42 | { path: "user", message: "the user is wrong" },
43 | HttpStatus.BAD_REQUEST,
44 | );
45 | }
46 |
47 | const isPasswordCorrect = await argon2.verify(user.password, password);
48 |
49 | if (!isPasswordCorrect) {
50 | throw new HttpException(
51 | { path: "password", message: "the password is wrong" },
52 | HttpStatus.BAD_REQUEST,
53 | );
54 | }
55 |
56 | const token = jwt.sign(
57 | { id: user.id, username: user.username },
58 | process.env.JWT_SECRET,
59 | { expiresIn: "7d" },
60 | );
61 |
62 | return {
63 | token,
64 | user: {
65 | id: user.id,
66 | username: user.username,
67 | created: user.created,
68 | },
69 | };
70 | }
71 |
72 | async register(data: UserDTO) {
73 | let user = await this.userRepository.findOne({
74 | where: { username: data.username },
75 | });
76 |
77 | if (user) {
78 | throw new HttpException(
79 | "the user already exists",
80 | HttpStatus.BAD_REQUEST,
81 | );
82 | }
83 |
84 | user = await this.userRepository.create(data);
85 | await this.userRepository.save(user);
86 |
87 | return user;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import * as request from 'supertest';
3 | import { AppModule } from './../src/app.module';
4 |
5 | describe('AppController (e2e)', () => {
6 | let app;
7 |
8 | beforeEach(async () => {
9 | const moduleFixture: TestingModule = await Test.createTestingModule({
10 | imports: [AppModule],
11 | }).compile();
12 |
13 | app = moduleFixture.createNestApplication();
14 | await app.init();
15 | });
16 |
17 | it('/ (GET)', () => {
18 | return request(app.getHttpServer())
19 | .get('/')
20 | .expect(200)
21 | .expect('Hello World!');
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "target": "es2017",
9 | "sourceMap": true,
10 | "outDir": "./dist",
11 | "baseUrl": "./",
12 | "incremental": true
13 | },
14 | "exclude": ["node_modules", "dist"]
15 | }
16 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": ["tslint:recommended"],
4 | "jsRules": {
5 | "no-unused-expression": true
6 | },
7 | "rules": {
8 | "member-access": [false],
9 | "ordered-imports": [false],
10 | "max-line-length": [true, 150],
11 | "member-ordering": [false],
12 | "interface-name": [false],
13 | "arrow-parens": false,
14 | "object-literal-sort-keys": false,
15 | "ban-types": false,
16 | "max-classes-per-file": false
17 | },
18 | "rulesDirectory": []
19 | }
20 |
--------------------------------------------------------------------------------