├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── nest-cli.json
├── package-lock.json
├── package.json
├── src
├── app.module.ts
├── auth
│ ├── auth.module.ts
│ ├── auth.service.ts
│ ├── jwt.strategy.ts
│ └── local.strategy.ts
├── main.ts
├── post
│ ├── dto
│ │ └── post.create-dto.ts
│ ├── entity
│ │ └── post.entity.ts
│ ├── post.controller.ts
│ ├── post.module.ts
│ └── post.service.ts
└── user
│ ├── dto
│ ├── user.create-dto.ts
│ ├── user.login-dto.ts
│ └── user.update-dto.ts
│ ├── entity
│ ├── address.entity.ts
│ └── user.entity.ts
│ ├── user.controller.ts
│ ├── user.module.ts
│ ├── user.service.ts
│ └── user.subscriber.ts
├── tsconfig.build.json
└── tsconfig.json
/.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/eslint-recommended',
10 | 'plugin:@typescript-eslint/recommended',
11 | 'prettier',
12 | 'prettier/@typescript-eslint',
13 | ],
14 | root: true,
15 | env: {
16 | node: true,
17 | jest: true,
18 | },
19 | rules: {
20 | '@typescript-eslint/interface-name-prefix': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/explicit-module-boundary-types': 'off',
23 | '@typescript-eslint/no-explicit-any': 'off',
24 | 'semi': 'warn',
25 | 'quotes': ['warn', 'single'],
26 | 'indent': ['warn', 4],
27 | 'no-unused-vars': 'warn',
28 | 'no-var': 'warn',
29 | 'keyword-spacing': 'warn',
30 | 'space-before-function-paren': 'warn',
31 | 'space-infix-ops': 'warn',
32 | 'comma-spacing': 'warn',
33 | 'operator-linebreak': 'warn',
34 | 'no-cond-assign': 'warn',
35 | 'block-spacing': 'warn',
36 | 'eol-last': 'warn',
37 | 'func-call-spacing': 'warn',
38 | 'new-parens': 'warn',
39 | 'no-const-assign': 'warn',
40 | 'no-new': 'warn',
41 | 'no-unneeded-ternary': 'warn',
42 | 'no-unreachable': 'warn',
43 | 'spaced-comment': 'warn'
44 | },
45 | };
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 |
13 | # OS
14 | .DS_Store
15 |
16 | # Tests
17 | /coverage
18 | /.nyc_output
19 |
20 | # IDEs and editors
21 | /.idea
22 | .project
23 | .classpath
24 | .c9/
25 | *.launch
26 | .settings/
27 | *.sublime-workspace
28 |
29 | # IDE - VSCode
30 | .vscode/*
31 | !.vscode/settings.json
32 | !.vscode/tasks.json
33 | !.vscode/launch.json
34 | !.vscode/extensions.json
35 |
36 | # env file
37 | .env
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "semi": true,
5 | "useTabs": true,
6 | "tabWidth": 4
7 | }
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master
6 | [travis-url]: https://travis-ci.org/nestjs/nest
7 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
8 | [linux-url]: https://travis-ci.org/nestjs/nest
9 |
10 | A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
26 |
27 | ## Description
28 |
29 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
30 |
31 | ## Installation
32 |
33 | ```bash
34 | $ npm install
35 | ```
36 |
37 | ## Running the app
38 |
39 | ```bash
40 | # development
41 | $ npm run start
42 |
43 | # watch mode
44 | $ npm run start:dev
45 |
46 | # production mode
47 | $ npm run start:prod
48 | ```
49 |
50 | ## Test
51 |
52 | ```bash
53 | # unit tests
54 | $ npm run test
55 |
56 | # e2e tests
57 | $ npm run test:e2e
58 |
59 | # test coverage
60 | $ npm run test:cov
61 | ```
62 |
63 | ## Support
64 |
65 | 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).
66 |
67 | ## Stay in touch
68 |
69 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
70 | - Website - [https://nestjs.com](https://nestjs.com/)
71 | - Twitter - [@nestframework](https://twitter.com/nestframework)
72 |
73 | ## License
74 |
75 | Nest is [MIT licensed](LICENSE).
76 |
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src"
4 | }
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nest-postgresql",
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\" \"test/**/*.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 | "@nestjs/common": "^7.0.0",
25 | "@nestjs/config": "^0.6.1",
26 | "@nestjs/core": "^7.0.0",
27 | "@nestjs/jwt": "^7.2.0",
28 | "@nestjs/mapped-types": "^0.1.1",
29 | "@nestjs/passport": "^7.1.5",
30 | "@nestjs/platform-express": "^7.0.0",
31 | "@nestjs/serve-static": "^2.1.4",
32 | "@nestjs/swagger": "^4.7.8",
33 | "@nestjs/typeorm": "^7.1.5",
34 | "@types/bcrypt": "^3.0.0",
35 | "bcrypt": "^5.0.0",
36 | "class-transformer": "^0.3.1",
37 | "class-validator": "^0.12.2",
38 | "morgan": "^1.10.0",
39 | "multer": "^1.4.2",
40 | "passport": "^0.4.1",
41 | "passport-jwt": "^4.0.0",
42 | "passport-local": "^1.0.0",
43 | "pg": "^8.5.1",
44 | "reflect-metadata": "^0.1.13",
45 | "rimraf": "^3.0.2",
46 | "rxjs": "^6.5.4",
47 | "swagger-ui-express": "^4.1.5",
48 | "typeorm": "^0.2.29"
49 | },
50 | "devDependencies": {
51 | "@nestjs/cli": "^7.0.0",
52 | "@nestjs/schematics": "^7.0.0",
53 | "@nestjs/testing": "^7.0.0",
54 | "@types/express": "^4.17.3",
55 | "@types/jest": "26.0.10",
56 | "@types/multer": "^1.4.5",
57 | "@types/node": "^13.9.1",
58 | "@types/passport-jwt": "^3.0.3",
59 | "@types/passport-local": "^1.0.33",
60 | "@types/supertest": "^2.0.8",
61 | "@typescript-eslint/eslint-plugin": "3.9.1",
62 | "@typescript-eslint/parser": "3.9.1",
63 | "eslint": "7.7.0",
64 | "eslint-config-prettier": "^6.10.0",
65 | "eslint-plugin-import": "^2.20.1",
66 | "jest": "26.4.2",
67 | "prettier": "^1.19.1",
68 | "supertest": "^4.0.2",
69 | "ts-jest": "26.2.0",
70 | "ts-loader": "^6.2.1",
71 | "ts-node": "9.0.0",
72 | "tsconfig-paths": "^3.9.0",
73 | "typescript": "^3.7.4"
74 | },
75 | "jest": {
76 | "moduleFileExtensions": [
77 | "js",
78 | "json",
79 | "ts"
80 | ],
81 | "rootDir": "src",
82 | "testRegex": ".spec.ts$",
83 | "transform": {
84 | "^.+\\.(t|j)s$": "ts-jest"
85 | },
86 | "coverageDirectory": "../coverage",
87 | "testEnvironment": "node"
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ConfigModule } from '@nestjs/config';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import { UserModule } from './user/user.module';
5 | import { AuthModule } from './auth/auth.module';
6 | import { PostModule } from './post/post.module';
7 | import { ServeStaticModule } from '@nestjs/serve-static';
8 | import { join } from 'path';
9 |
10 | @Module({
11 | imports: [
12 | ConfigModule.forRoot({
13 | isGlobal: true
14 | }),
15 | TypeOrmModule.forRoot({
16 | type: 'postgres',
17 | host: process.env.DATABASE_HOST,
18 | port: 5432,
19 | username: process.env.DATABASE_USERNAME,
20 | password: process.env.DATABASE_PASSWORD,
21 | database: process.env.DATABASE_NAME,
22 | autoLoadEntities: true,
23 | synchronize: true,
24 | entities: ['dist/**/*.entity{.ts,.js}'],
25 | }),
26 | ServeStaticModule.forRoot({
27 | rootPath: join(__dirname, '..', 'uploads'),
28 | serveRoot: '/static'
29 | }),
30 | UserModule,
31 | AuthModule,
32 | PostModule,
33 | ],
34 | })
35 | export class AppModule {}
36 |
--------------------------------------------------------------------------------
/src/auth/auth.module.ts:
--------------------------------------------------------------------------------
1 | import { forwardRef, Module } from '@nestjs/common';
2 | import { ConfigModule, ConfigService } from '@nestjs/config';
3 | import { JwtModule } from '@nestjs/jwt';
4 | import { PassportModule } from '@nestjs/passport';
5 | import { UserModule } from 'src/user/user.module';
6 | import { AuthService } from './auth.service';
7 | import { JwtStrategy } from './jwt.strategy';
8 | import { LocalStrategy } from './local.strategy';
9 |
10 | @Module({
11 | imports:[
12 | JwtModule.registerAsync({
13 | imports: [ConfigModule],
14 | inject: [ConfigService],
15 | useFactory: async (configService: ConfigService) => ({
16 | secret: configService.get('JWT_SECRET'),
17 | signOptions: { expiresIn: '7d' }
18 | })
19 | }),
20 | PassportModule,
21 | forwardRef(() => UserModule),
22 | ],
23 | providers: [AuthService, LocalStrategy, JwtStrategy],
24 | exports: [AuthService],
25 | })
26 | export class AuthModule {}
27 |
--------------------------------------------------------------------------------
/src/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, forwardRef } from '@nestjs/common';
2 | import { JwtService } from '@nestjs/jwt';
3 | import { UserEntity } from 'src/user/entity/user.entity';
4 | import * as bcrypt from 'bcrypt';
5 | import { UserService } from 'src/user/user.service';
6 | import { Inject } from '@nestjs/common/decorators';
7 |
8 | @Injectable()
9 | export class AuthService {
10 | constructor (
11 | @Inject(forwardRef(() => UserService))
12 | private readonly userService: UserService,
13 | private readonly jwtService: JwtService,
14 | ){}
15 |
16 | generateJWT (user: UserEntity): Promise {
17 | return this.jwtService.signAsync({user});
18 | }
19 |
20 | hashPassword (password: string): Promise {
21 | return bcrypt.hash(password, 12);
22 | }
23 |
24 | comparePassword (newPassword: string, passwordHash: string): boolean{
25 | return bcrypt.compareSync(newPassword, passwordHash);
26 | }
27 |
28 | async validateUser (email: string, password: string): Promise{
29 | try {
30 | const findUser: UserEntity = await this.userService.findUserByEmail(email);
31 | if (!this.comparePassword(password, findUser.password)) return null;
32 | return findUser;
33 | } catch (err) {
34 | console.log(err);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/auth/jwt.strategy.ts:
--------------------------------------------------------------------------------
1 | import { ExtractJwt, Strategy } from 'passport-jwt';
2 | import { PassportStrategy } from '@nestjs/passport';
3 | import { Injectable } from '@nestjs/common';
4 |
5 | @Injectable()
6 | export class JwtStrategy extends PassportStrategy(Strategy) {
7 | constructor () {
8 | super({
9 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
10 | ignoreExpiration: false,
11 | secretOrKey: process.env.JWT_SECRET,
12 | });
13 | }
14 |
15 | async validate (payload: any) {
16 | // payload는 user객체, jwt발급날짜시간, jwt만료날짜시간 으로 구성
17 | return payload.user;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/auth/local.strategy.ts:
--------------------------------------------------------------------------------
1 | import { Strategy } from 'passport-local';
2 | import { PassportStrategy } from '@nestjs/passport';
3 | import { Injectable, UnauthorizedException } from '@nestjs/common';
4 | import { AuthService } from './auth.service';
5 | import { UserEntity } from 'src/user/entity/user.entity';
6 |
7 | @Injectable()
8 | export class LocalStrategy extends PassportStrategy(Strategy) {
9 | constructor (private authService: AuthService) {
10 | super({ usernameField: 'email' });
11 | }
12 |
13 | async validate (email: string, password: string): Promise {
14 | const user: UserEntity = await this.authService.validateUser(email, password);
15 | if (!user) {
16 | throw new UnauthorizedException('password is wrong');
17 | }
18 | return user;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { ValidationPipe } from '@nestjs/common';
3 | import { AppModule } from './app.module';
4 | import * as morgan from 'morgan';
5 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
6 |
7 | async function bootstrap () {
8 | const app = await NestFactory.create(AppModule);
9 | const options = new DocumentBuilder()
10 | .setTitle('NestJS PostgreSQL example')
11 | .setDescription('API description')
12 | .setVersion('1.0')
13 | .build();
14 | const document = SwaggerModule.createDocument(app, options);
15 | SwaggerModule.setup('api', app, document);
16 | app.use(morgan('tiny'));
17 | app.useGlobalPipes(new ValidationPipe({
18 | whitelist: true,
19 | forbidNonWhitelisted: true, // 필요없는 변수는 필요없다라고 응답으로 주는거
20 | transform: true, // 컨트롤러에서 받는 리퀘스트 관련 데코레이터 변수를 자기가 지정타입으로 자동 변환시켜주는 애
21 | }));
22 | await app.listen(3000);
23 | console.log(`Application is running on: ${await app.getUrl()}`);
24 | }
25 | bootstrap();
26 |
--------------------------------------------------------------------------------
/src/post/dto/post.create-dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString } from 'class-validator';
2 |
3 | export class CreatePostDto {
4 | @IsString()
5 | readonly title: string;
6 | @IsString()
7 | readonly description: string;
8 | }
9 |
--------------------------------------------------------------------------------
/src/post/entity/post.entity.ts:
--------------------------------------------------------------------------------
1 | import { UserEntity } from 'src/user/entity/user.entity';
2 | import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm';
3 |
4 | @Entity({ name: 'post' })
5 | export class PostEntity {
6 | @PrimaryGeneratedColumn()
7 | id: number;
8 |
9 | @Column({ unique: true })
10 | title: string;
11 |
12 | @Column()
13 | description: string;
14 |
15 | @Column()
16 | imgUrl: string;
17 |
18 | @Column({ default: new Date() })
19 | createdAt: Date
20 |
21 | @Column({ default: new Date() })
22 | updatedAt: Date
23 |
24 | @ManyToOne(() => UserEntity, user => user.posts)
25 | @JoinColumn({ name: 'userId' })
26 | user: UserEntity;
27 | }
28 |
--------------------------------------------------------------------------------
/src/post/post.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Post, UseGuards, Request, Get, Param, UseInterceptors, UploadedFile } from '@nestjs/common';
2 | import { AuthGuard } from '@nestjs/passport';
3 | import { CreatePostDto } from './dto/post.create-dto';
4 | import { PostService } from './post.service';
5 | import { PostEntity } from './entity/post.entity';
6 | import { FileInterceptor } from '@nestjs/platform-express';
7 | import { diskStorage } from 'multer';
8 | import { extname } from 'path';
9 |
10 | @Controller('post')
11 | export class PostController {
12 | constructor (private readonly postService: PostService){}
13 |
14 | @Post()
15 | @UseGuards(AuthGuard('jwt'))
16 | @UseInterceptors(FileInterceptor('postImage', {
17 | storage: diskStorage({
18 | destination: './uploads/postImages',
19 | filename: (req, file, cb) => {
20 | const randomName = Array(32).fill(null).map(() => (Math.round(Math.random() * 16)).toString(16)).join('');
21 | cb(null, `${randomName}${extname(file.originalname)}`);
22 | }
23 | })
24 | }))
25 | createPost (@Request() req, @Body() post: CreatePostDto, @UploadedFile() file: Express.Multer.File): Promise {
26 | return this.postService.createPost(req.user, post, file);
27 | }
28 |
29 | @Get('/list')
30 | findAllPost (): Promise {
31 | return this.postService.findAllPost();
32 | }
33 |
34 | @Get('/loginList')
35 | @UseGuards(AuthGuard('jwt'))
36 | findLoginedAllPost (@Request() req): Promise {
37 | return this.postService.findLoginedAllPost(req.user);
38 | }
39 |
40 | @Get('/:postId')
41 | findPost (@Param('postId') postId: number): Promise {
42 | return this.postService.findPost(postId);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/post/post.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm/dist/typeorm.module';
3 | import { PostEntity } from './entity/post.entity';
4 | import { PostController } from './post.controller';
5 | import { PostService } from './post.service';
6 |
7 | @Module({
8 | imports: [
9 | TypeOrmModule.forFeature([PostEntity]),
10 | ],
11 | controllers: [PostController],
12 | providers: [PostService]
13 | })
14 | export class PostModule {}
15 |
--------------------------------------------------------------------------------
/src/post/post.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { UserEntity } from 'src/user/entity/user.entity';
4 | import { Repository } from 'typeorm';
5 | import { CreatePostDto } from './dto/post.create-dto';
6 | import { PostEntity } from './entity/post.entity';
7 |
8 | @Injectable()
9 | export class PostService {
10 | constructor (
11 | @InjectRepository(PostEntity)
12 | private postRepository: Repository,
13 | ){}
14 |
15 | // POST -> 게시물 생성하기
16 | async createPost (user: UserEntity, post: CreatePostDto, file: Express.Multer.File): Promise {
17 | return await this.postRepository.save({...post, imgUrl: `/static/postImages/${file.filename}`, user});
18 | }
19 |
20 | // GET -> 모든 게시물 조회하기
21 | async findAllPost (): Promise{
22 | return await this.postRepository.find();
23 | }
24 |
25 | // GET -> 로그인된 사용자가 게시한 모든 게시물 조회하기
26 | async findLoginedAllPost (user: UserEntity): Promise{
27 | return await this.postRepository.find({ user });
28 | }
29 |
30 | // GET -> 특정 게시물 조회하기
31 | async findPost (postId: number): Promise{
32 | // return await this.postRepository
33 | // .createQueryBuilder('post')
34 | // .leftJoinAndSelect('post.user', 'user')
35 | // .where('post.id = :id', { id: postId })
36 | // .getOne();
37 | return await this.postRepository.findOne({ id: postId }, { relations: ['user'] });
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/user/dto/user.create-dto.ts:
--------------------------------------------------------------------------------
1 | import { IsNumber, IsString } from 'class-validator';
2 |
3 | export class CreateUserDto {
4 | @IsString()
5 | readonly name: string;
6 | @IsNumber()
7 | readonly age: number;
8 | @IsString()
9 | readonly email: string;
10 | @IsString()
11 | readonly password: string;
12 | @IsString()
13 | readonly country: string
14 | @IsString()
15 | readonly city: string
16 | @IsString()
17 | readonly street: string
18 | @IsString()
19 | readonly zipCode: string
20 | }
21 |
--------------------------------------------------------------------------------
/src/user/dto/user.login-dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString } from 'class-validator';
2 |
3 | export class LoginUserDto {
4 | @IsString()
5 | readonly accessToken: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/user/dto/user.update-dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types';
2 | import { CreateUserDto } from './user.create-dto';
3 |
4 | export class UpdateUserDto extends PartialType(CreateUserDto) {}
5 |
--------------------------------------------------------------------------------
/src/user/entity/address.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
2 |
3 | @Entity({ name: 'address' })
4 | export class AddressEntity {
5 | @PrimaryGeneratedColumn()
6 | id: number;
7 |
8 | @Column()
9 | country: string;
10 |
11 | @Column()
12 | city: string;
13 |
14 | @Column()
15 | street: string;
16 |
17 | @Column()
18 | zipCode: string;
19 | }
20 |
--------------------------------------------------------------------------------
/src/user/entity/user.entity.ts:
--------------------------------------------------------------------------------
1 | import { PostEntity } from 'src/post/entity/post.entity';
2 | import { Entity, Column, PrimaryGeneratedColumn, BeforeInsert, OneToMany, JoinColumn, OneToOne } from 'typeorm';
3 | import { AddressEntity } from './address.entity';
4 |
5 | @Entity({ name: 'user' })
6 | export class UserEntity {
7 | @PrimaryGeneratedColumn()
8 | id: number;
9 |
10 | @Column()
11 | name: string;
12 |
13 | @Column()
14 | age: number;
15 |
16 | @Column({ unique: true })
17 | email: string;
18 |
19 | @Column()
20 | password: string;
21 |
22 | @Column({ default: true })
23 | isActive: boolean;
24 |
25 | @OneToMany(() => PostEntity, (post: PostEntity) => post.user)
26 | posts: PostEntity[]
27 |
28 | @OneToOne(() => AddressEntity)
29 | @JoinColumn({ name: 'addressId' })
30 | public address: AddressEntity;
31 |
32 | @BeforeInsert()
33 | emailToLowerCase () {
34 | this.email = this.email.toLowerCase();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/user/user.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards, Request } from '@nestjs/common';
2 | import { AuthGuard } from '@nestjs/passport';
3 | import { UpdateResult } from 'typeorm';
4 | import { CreateUserDto } from './dto/user.create-dto';
5 | import { LoginUserDto } from './dto/user.login-dto';
6 | import { UpdateUserDto } from './dto/user.update-dto';
7 | import { UserEntity } from './entity/user.entity';
8 | import { UserService } from './user.service';
9 |
10 | @Controller('user')
11 | export class UserController {
12 | constructor (private readonly userService: UserService){}
13 |
14 | @Post('/list')
15 | createManyUser (@Body() userList: CreateUserDto[]): Promise {
16 | return this.userService.createManyUser(userList);
17 | }
18 |
19 | @Post()
20 | createUser (@Body() user: CreateUserDto): Promise {
21 | return this.userService.createUser(user);
22 | }
23 |
24 | @Post('/login')
25 | @UseGuards(AuthGuard('local'))
26 | async login (@Request() req): Promise {
27 | return this.userService.login(req.user);
28 | }
29 |
30 | @Get('/list')
31 | findUserList (): Promise{
32 | return this.userService.findAll();
33 | }
34 |
35 | @Get('/search')
36 | findUserByEmail (@Query('email') email: string): Promise{
37 | return this.userService.findUserByEmail(email);
38 | }
39 |
40 | @Get('/:userId')
41 | findUserById (@Param('userId') userId: number): Promise{
42 | return this.userService.findUserById(userId);
43 | }
44 |
45 | @Patch()
46 | @UseGuards(AuthGuard('jwt'))
47 | updateUserById (@Request() req, @Body() updateUserDto: UpdateUserDto): Promise {
48 | return this.userService.updateUserById(req.user.id, updateUserDto);
49 | }
50 |
51 | @Delete()
52 | @UseGuards(AuthGuard('jwt'))
53 | deleteUser (@Request() req): Promise {
54 | return this.userService.removeUserById(req.user.id);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/user/user.module.ts:
--------------------------------------------------------------------------------
1 | import { forwardRef, Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { UserController } from './user.controller';
4 | import { UserEntity } from './entity/user.entity';
5 | import { UserService } from './user.service';
6 | import { UserSubscriber } from './user.subscriber';
7 | import { AuthModule } from 'src/auth/auth.module';
8 | import { AddressEntity } from './entity/address.entity';
9 |
10 | @Module({
11 | imports: [
12 | TypeOrmModule.forFeature([UserEntity, AddressEntity]),
13 | forwardRef(() => AuthModule),
14 | ],
15 | controllers: [UserController],
16 | providers: [UserService, UserSubscriber],
17 | exports: [UserService]
18 | })
19 | export class UserModule {}
20 |
--------------------------------------------------------------------------------
/src/user/user.service.ts:
--------------------------------------------------------------------------------
1 | import { ConflictException, NotFoundException, Injectable } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { AuthService } from 'src/auth/auth.service';
4 | import { Connection, Repository, UpdateResult } from 'typeorm';
5 | import { CreateUserDto } from './dto/user.create-dto';
6 | import { LoginUserDto } from './dto/user.login-dto';
7 | import { UpdateUserDto } from './dto/user.update-dto';
8 | import { AddressEntity } from './entity/address.entity';
9 | import { UserEntity } from './entity/user.entity';
10 |
11 | @Injectable()
12 | export class UserService {
13 | constructor (
14 | @InjectRepository(UserEntity)
15 | private userRepository: Repository,
16 | @InjectRepository(AddressEntity)
17 | private addressRepository: Repository,
18 | private authService: AuthService,
19 | private connection: Connection, // transaction을 위해 필요
20 | ){}
21 |
22 | // POST -> 특정 사용자 생성하기
23 | async createUser (user: CreateUserDto): Promise {
24 | const { email, password, name, age, country, city, street, zipCode } = user;
25 | const findUser: UserEntity = await this.userRepository.findOne({ email });
26 | if (findUser) throw new ConflictException(`${email} is already created user. Create another user.`);
27 | const hashPassword: string = await this.authService.hashPassword(password);
28 | const saveAddress: AddressEntity = await this.addressRepository.save({ country, city, street, zipCode });
29 | return this.userRepository.save({ email, name, age, password: hashPassword, address: saveAddress });
30 | }
31 |
32 | // POST -> 사용자 로그인 하기
33 | async login (loginUser: UserEntity): Promise{
34 | const accessToken: string = await this.authService.generateJWT(loginUser);
35 | return { accessToken };
36 | }
37 |
38 | // POST -> 다수의 사용자 생성하기 (+트랜젝션 처리)
39 | async createManyUser (users: CreateUserDto[]): Promise{
40 | const queryRunner = this.connection.createQueryRunner();
41 | await queryRunner.connect();
42 | await queryRunner.startTransaction();
43 | try {
44 | for (const user of users) {
45 | await queryRunner.manager.save(user);
46 | }
47 | await queryRunner.commitTransaction();
48 | } catch (err) {
49 | await queryRunner.rollbackTransaction();
50 | } finally {
51 | await queryRunner.release();
52 | }
53 | }
54 |
55 | // GET -> 전체 사용자 정보 조회하기
56 | async findAll (): Promise{
57 | return await this.userRepository.find({ relations: ['address'] });
58 | }
59 |
60 | // GET -> 특정 아이디로 사용자 정보 조회하기
61 | async findUserById (id: number): Promise{
62 | const selectedUser: UserEntity = await this.userRepository.findOne({ id }, { relations: ['address'] });
63 | if (!selectedUser) throw new NotFoundException(`there is no user with ID ${id}`);
64 | return selectedUser;
65 | }
66 |
67 | // GET -> 특정 키워드로 사용자의 정보 조회하기
68 | async findUserByEmail (email: string): Promise{
69 | const selectedUser: UserEntity = await this.userRepository.findOne({ email }, { relations: ['address'] });
70 | if (!selectedUser) throw new NotFoundException(`there is no user with email->(${email})`);
71 | return selectedUser;
72 | }
73 |
74 | // PATCH -> 특정 아이디로 사용자의 정보 수정하기
75 | async updateUserById (userId: number, updateUserDto: UpdateUserDto): Promise {
76 | return await this.userRepository.update(userId, updateUserDto);
77 | }
78 |
79 | // DELETE -> 특정 아이디로 사용자 정보 삭제하기
80 | async removeUserById (userId: number): Promise{
81 | const user: UserEntity = await this.findUserById(userId);
82 | await this.userRepository.delete(userId);
83 |
84 | // user테이블에 외래키로 있는 addressId가 먼저 지워져야지만 address테이블의 해당 로우가 정상삭제된다.
85 | this.addressRepository.delete(user.address.id);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/user/user.subscriber.ts:
--------------------------------------------------------------------------------
1 | import { Connection, EntitySubscriberInterface, EventSubscriber, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm';
2 | import { UserEntity } from './entity/user.entity';
3 |
4 | @EventSubscriber()
5 | export class UserSubscriber implements EntitySubscriberInterface {
6 | constructor (connection: Connection){
7 | connection.subscribers.push(this);
8 | }
9 |
10 | listenTo (){
11 | return UserEntity;
12 | }
13 |
14 | beforeInsert (event: InsertEvent){
15 | console.log('BEFORE USER INSERTED: ', event.entity);
16 | }
17 |
18 | beforeUpdate (event: UpdateEvent) {
19 | console.log('BEFORE ENTITY UPDATED: ', event.entity);
20 | }
21 |
22 | beforeRemove (event: RemoveEvent) {
23 | console.log(`BEFORE ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity);
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/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 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------