├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── compose.yml ├── nest-cli.json ├── ormconfig.js ├── package-lock.json ├── package.json ├── readme.md ├── src ├── app.controller.ts ├── app.module.ts ├── blogs │ ├── blogs.entity.ts │ └── blogs.module.ts ├── common │ ├── decorators │ │ ├── client-real-ip.decorator.ts │ │ └── current-user.decorator.ts │ ├── entities │ │ └── common.entity.ts │ ├── exceptions │ │ └── http-api-exception.filter.ts │ ├── interceptors │ │ ├── only-admin.interceptor.ts │ │ └── only-private.interceptor.ts │ └── utils │ │ └── jwtExtractorFromCookies.ts ├── main.ts ├── profiles │ ├── profiles.entity.ts │ └── profiles.module.ts ├── tags │ ├── tags.entity.ts │ └── tags.module.ts ├── typing.d.ts ├── users │ ├── dtos │ │ ├── user-login.dto.ts │ │ ├── user-register.dto.ts │ │ └── user.dto.ts │ ├── jwt │ │ ├── jwt.guard.ts │ │ ├── jwt.payload.ts │ │ └── jwt.strategy.ts │ ├── users.controller.ts │ ├── users.entity.ts │ ├── users.module.ts │ ├── users.service.spec.ts │ └── users.service.ts └── visitors │ ├── dto │ ├── create-visitor.dto.ts │ └── update-visitor.dto.ts │ ├── visitors.controller.spec.ts │ ├── visitors.controller.ts │ ├── visitors.entity.ts │ ├── visitors.module.ts │ ├── visitors.service.spec.ts │ └── visitors.service.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── 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/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js', 'node_modules', 'dist', '*.d.ts'], 18 | rules: { 19 | 'no-console': 'warn', 20 | '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], 21 | '@typescript-eslint/interface-name-prefix': 'off', 22 | '@typescript-eslint/explicit-function-return-type': 'off', 23 | '@typescript-eslint/explicit-module-boundary-types': 'off', 24 | '@typescript-eslint/no-explicit-any': 'off', 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | env_files/* 3 | db_data 4 | 5 | 6 | 7 | 8 | # compiled output 9 | /dist 10 | /node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | pnpm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | lerna-debug.log* 20 | 21 | # OS 22 | .DS_Store 23 | 24 | # Tests 25 | /coverage 26 | /.nyc_output 27 | 28 | # IDEs and editors 29 | /.idea 30 | .project 31 | .classpath 32 | .c9/ 33 | *.launch 34 | .settings/ 35 | *.sublime-workspace 36 | 37 | # IDE - VSCode 38 | .vscode/* 39 | !.vscode/settings.json 40 | !.vscode/tasks.json 41 | !.vscode/launch.json 42 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | 5 | "trailingComma": "all", 6 | "overrides": [ 7 | { 8 | "files": "*.hbs", 9 | "options": { 10 | "singleQuote": false 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.codeAction.showDocumentation": { 3 | "enable": true 4 | }, 5 | 6 | "eslint.alwaysShowStatus": true, 7 | "editor.formatOnSave": true, 8 | "editor.defaultFormatter": "esbenp.prettier-vscode", 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll": true 11 | }, 12 | "npm.packageManager": "npm" 13 | } 14 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | db: 5 | image: postgres:9.6.23 6 | restart: always 7 | env_file: 8 | - ./.env 9 | ports: 10 | - 5433:5432 11 | volumes: 12 | - ./db_data:/var/lib/postgresql/data 13 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /ormconfig.js: -------------------------------------------------------------------------------- 1 | require('dotenv/config') 2 | /* eslint @typescript-eslint/no-var-requires: 0 */ 3 | const { SnakeNamingStrategy } = require('typeorm-naming-strategies') 4 | 5 | module.exports = { 6 | type: 'postgres', 7 | database: process.env.DB_NAME, 8 | host: process.env.DB_HOST, 9 | port: process.env.DB_PORT, 10 | username: process.env.DB_USERNAME, 11 | password: process.env.DB_PASSWORD, 12 | synchronize: false, 13 | autoLoadEntities: true, 14 | keepConnectionAlive: true, 15 | logging: true, 16 | namingStrategy: new SnakeNamingStrategy(), 17 | // entities: [UserEntity], 18 | entities: ['src/**/*.entity.ts'], 19 | migrations: ['migrations/**/*.ts'], 20 | cli: { migrationsDir: 'migrations' }, 21 | seeds: ['src/seeds/**/*.ts'], 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amamov.com", 3 | "version": "1.8.0", 4 | "author": "yoon sang seok", 5 | "license": "MIT", 6 | "scripts": { 7 | "prebuild": "rimraf dist", 8 | "build": "nest build", 9 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 10 | "start": "nest start", 11 | "start:dev": "nest start --watch", 12 | "start:debug": "nest start --debug --watch", 13 | "start:prod": "node dist/main", 14 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 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 | "prettier": "npx prettier -w .", 21 | "prettier:fix": "npx prettier -c .", 22 | "orm": "ts-node ./node_modules/typeorm/cli.js" 23 | }, 24 | "dependencies": { 25 | "@nestjs/common": "^8.0.0", 26 | "@nestjs/config": "^1.0.2", 27 | "@nestjs/core": "^8.0.0", 28 | "@nestjs/jwt": "^8.0.0", 29 | "@nestjs/passport": "^8.0.1", 30 | "@nestjs/platform-express": "^8.0.11", 31 | "@nestjs/swagger": "^5.1.0", 32 | "@nestjs/typeorm": "^8.0.2", 33 | "@types/multer": "^1.4.7", 34 | "aws-sdk": "^2.1007.0", 35 | "bcrypt": "^5.0.1", 36 | "class-transformer": "^0.4.0", 37 | "class-validator": "^0.13.1", 38 | "cookie-parser": "^1.4.5", 39 | "dotenv": "^10.0.0", 40 | "express-basic-auth": "^1.2.0", 41 | "hbs": "^4.1.2", 42 | "joi": "^17.4.2", 43 | "multer": "^1.4.3", 44 | "nestjs-typeorm-paginate": "^3.1.3", 45 | "passport-jwt": "^4.0.0", 46 | "pg": "^8.7.1", 47 | "pug": "^3.0.2", 48 | "react": "^17.0.2", 49 | "react-dom": "^17.0.2", 50 | "reflect-metadata": "^0.1.13", 51 | "request-ip": "^2.1.3", 52 | "rimraf": "^3.0.2", 53 | "rxjs": "^7.2.0", 54 | "sharp": "^0.29.1", 55 | "swagger-ui-express": "^4.1.6", 56 | "typeorm": "^0.2.38", 57 | "typeorm-naming-strategies": "^2.0.0", 58 | "typeorm-seeding": "^1.6.1", 59 | "uuid": "^8.3.2" 60 | }, 61 | "devDependencies": { 62 | "@nestjs/cli": "^8.0.0", 63 | "@nestjs/schematics": "^8.0.0", 64 | "@nestjs/testing": "^8.0.0", 65 | "@types/bcrypt": "^5.0.0", 66 | "@types/cookie-parser": "^1.4.2", 67 | "@types/dotenv": "^8.2.0", 68 | "@types/express": "^4.17.13", 69 | "@types/jest": "^27.0.1", 70 | "@types/node": "^16.0.0", 71 | "@types/passport-jwt": "^3.0.6", 72 | "@types/request-ip": "^0.0.37", 73 | "@types/sharp": "^0.29.2", 74 | "@types/supertest": "^2.0.11", 75 | "@types/uuid": "^8.3.1", 76 | "@typescript-eslint/eslint-plugin": "^4.28.2", 77 | "@typescript-eslint/parser": "^4.28.2", 78 | "eslint": "^7.30.0", 79 | "eslint-config-prettier": "^8.3.0", 80 | "eslint-plugin-prettier": "^3.4.0", 81 | "jest": "^27.0.6", 82 | "prettier": "^2.4.1", 83 | "supertest": "^6.1.3", 84 | "ts-jest": "^27.0.3", 85 | "ts-loader": "^9.2.3", 86 | "ts-node": "^10.0.0", 87 | "tsconfig-paths": "^3.10.1", 88 | "typescript": "^4.3.5" 89 | }, 90 | "jest": { 91 | "moduleFileExtensions": [ 92 | "js", 93 | "json", 94 | "ts" 95 | ], 96 | "rootDir": "src", 97 | "testRegex": ".*\\.spec\\.ts$", 98 | "transform": { 99 | "^.+\\.(t|j)s$": "ts-jest" 100 | }, 101 | "collectCoverageFrom": [ 102 | "**/*.(t|j)s" 103 | ], 104 | "coverageDirectory": "../coverage", 105 | "testEnvironment": "node" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # TypeORM In The Nest 2 | 3 | ## Config .env 4 | 5 | ```.env 6 | # app 7 | NODE_ENV=development 8 | PORT=5000 9 | ADMIN_USER=... 10 | ADMIN_PASSWORD=... 11 | SECRET_KEY=... 12 | DB_USERNAME=... 13 | DB_PASSWORD=... 14 | DB_HOST=localhost 15 | DB_PORT=5433 16 | DB_NAME=... 17 | 18 | # db 19 | POSTGRES_DB=... 20 | POSTGRES_USER=... 21 | POSTGRES_PASSWORD=... 22 | 23 | # db admin 24 | PGADMIN_DEFAULT_EMAIL=... 25 | PGADMIN_DEFAULT_PASSWORD=... 26 | ``` 27 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common' 2 | 3 | @Controller() 4 | export class AppController { 5 | @Get() 6 | getRoot() { 7 | return 'typeorm in nest, just coding' 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ConfigModule, ConfigService } from '@nestjs/config' 3 | import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm' 4 | import * as Joi from 'joi' 5 | import { SnakeNamingStrategy } from 'typeorm-naming-strategies' 6 | import { AppController } from './app.controller' 7 | import { UserEntity } from './users/users.entity' 8 | import { UsersModule } from './users/users.module' 9 | import { BlogsModule } from './blogs/blogs.module' 10 | import { TagsModule } from './tags/tags.module' 11 | import { VisitorsModule } from './visitors/visitors.module' 12 | import { ProfilesModule } from './profiles/profiles.module' 13 | import { ProfileEntity } from './profiles/profiles.entity' 14 | import { BlogEntity } from './blogs/blogs.entity' 15 | import { VisitorEntity } from './visitors/visitors.entity' 16 | import { TagEntity } from './tags/tags.entity' 17 | import { VisitorsModule } from './visitors/visitors.module'; 18 | import { VisitorsModule } from './visitors/visitors.module'; 19 | 20 | const typeOrmModuleOptions = { 21 | useFactory: async ( 22 | configService: ConfigService, 23 | ): Promise => ({ 24 | namingStrategy: new SnakeNamingStrategy(), 25 | type: 'postgres', 26 | host: configService.get('DB_HOST'), // process.env.DB_HOST 27 | port: configService.get('DB_PORT'), 28 | username: configService.get('DB_USERNAME'), 29 | password: configService.get('DB_PASSWORD'), 30 | database: configService.get('DB_NAME'), 31 | entities: [UserEntity, ProfileEntity, BlogEntity, VisitorEntity, TagEntity], 32 | synchronize: true, //! set 'false' in production 33 | autoLoadEntities: true, 34 | logging: true, 35 | keepConnectionAlive: true, 36 | }), 37 | inject: [ConfigService], 38 | } 39 | 40 | @Module({ 41 | imports: [ 42 | ConfigModule.forRoot({ 43 | isGlobal: true, 44 | validationSchema: Joi.object({ 45 | NODE_ENV: Joi.string() 46 | .valid('development', 'production', 'test', 'provision') 47 | .default('development'), 48 | PORT: Joi.number().default(5000), 49 | SECRET_KEY: Joi.string().required(), 50 | ADMIN_USER: Joi.string().required(), 51 | ADMIN_PASSWORD: Joi.string().required(), 52 | DB_USERNAME: Joi.string().required(), 53 | DB_PASSWORD: Joi.string().required(), 54 | DB_HOST: Joi.string().required(), 55 | DB_PORT: Joi.number().required(), 56 | DB_NAME: Joi.string().required(), 57 | }), 58 | }), 59 | TypeOrmModule.forRootAsync(typeOrmModuleOptions), 60 | UsersModule, 61 | BlogsModule, 62 | TagsModule, 63 | VisitorsModule, 64 | ProfilesModule, 65 | ], 66 | controllers: [AppController], 67 | }) 68 | export class AppModule {} 69 | -------------------------------------------------------------------------------- /src/blogs/blogs.entity.ts: -------------------------------------------------------------------------------- 1 | import { CommonEntity } from '../common/entities/common.entity' // ormconfig.json에서 파싱 가능하도록 상대 경로로 지정 2 | import { 3 | Column, 4 | Entity, 5 | JoinColumn, 6 | JoinTable, 7 | ManyToMany, 8 | ManyToOne, 9 | OneToMany, 10 | } from 'typeorm' 11 | import { VisitorEntity } from '../visitors/visitors.entity' 12 | import { UserEntity } from 'src/users/users.entity' 13 | import { TagEntity } from 'src/tags/tags.entity' 14 | 15 | @Entity({ 16 | name: 'BLOG', 17 | }) 18 | export class BlogEntity extends CommonEntity { 19 | @Column({ type: 'varchar', nullable: false }) 20 | title: string 21 | 22 | @Column({ type: 'varchar', nullable: true }) 23 | description: string 24 | 25 | @Column({ type: 'text', nullable: true }) 26 | contents: string 27 | 28 | //* Relation */ 29 | 30 | @ManyToOne(() => UserEntity, (author: UserEntity) => author.blogs, { 31 | onDelete: 'CASCADE', // 사용자가 삭제되면 블로그도 삭제된다. 32 | }) 33 | @JoinColumn([ 34 | // foreignkey 정보들 35 | { 36 | name: 'author_id' /* db에 저장되는 필드 이름 */, 37 | referencedColumnName: 'id' /* USER의 id */, 38 | }, 39 | ]) 40 | author: UserEntity 41 | 42 | @ManyToMany(() => TagEntity, (tag: TagEntity) => tag.blogs, { 43 | cascade: true, // 블로그를 통해 태그가 추가, 수정, 삭제되고 블로그를 저장하면 태그도 저장된다. 44 | }) 45 | @JoinTable({ 46 | // table 47 | name: 'BLOG_TAG', 48 | joinColumn: { 49 | name: 'blog_id', 50 | referencedColumnName: 'id', 51 | }, 52 | inverseJoinColumn: { 53 | name: 'tag_id', 54 | referencedColumnName: 'id', 55 | }, 56 | }) 57 | tags: TagEntity[] 58 | 59 | @OneToMany(() => VisitorEntity, (visitor: VisitorEntity) => visitor.blog, { 60 | cascade: true, 61 | }) 62 | visitors: VisitorEntity[] 63 | } 64 | -------------------------------------------------------------------------------- /src/blogs/blogs.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | @Module({}) 4 | export class BlogsModule {} 5 | -------------------------------------------------------------------------------- /src/common/decorators/client-real-ip.decorator.ts: -------------------------------------------------------------------------------- 1 | import * as requestIp from 'request-ip' 2 | import { createParamDecorator, ExecutionContext } from '@nestjs/common' 3 | import { Request } from 'express' 4 | 5 | export const ClientIp = createParamDecorator( 6 | (data: unknown, ctx: ExecutionContext) => { 7 | const request: Request = ctx.switchToHttp().getRequest() 8 | 9 | if (request.headers['cf-connecting-ip']) 10 | //* cloudflare origin ip */ 11 | return request.headers['cf-connecting-ip'] 12 | else return requestIp.getClientIp(request) 13 | }, 14 | ) 15 | -------------------------------------------------------------------------------- /src/common/decorators/current-user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common' 2 | 3 | export const CurrentUser = createParamDecorator( 4 | (data: unknown, ctx: ExecutionContext) => { 5 | const request = ctx.switchToHttp().getRequest() 6 | if (request.user) return request.user 7 | else null 8 | }, 9 | ) 10 | -------------------------------------------------------------------------------- /src/common/entities/common.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateDateColumn, 3 | DeleteDateColumn, 4 | PrimaryGeneratedColumn, 5 | UpdateDateColumn, 6 | } from 'typeorm' 7 | import { IsUUID } from 'class-validator' 8 | import { Exclude } from 'class-transformer' 9 | 10 | export abstract class CommonEntity { 11 | @IsUUID() 12 | @PrimaryGeneratedColumn('uuid') 13 | id: string 14 | 15 | // 해당 열이 추가된 시각을 자동으로 기록 16 | // 만일 Postgres의 time zone이 'UTC'라면 UTC 기준으로 출력하고 'Asia/Seoul'라면 서울 기준으로 출력한다. 17 | // DB SQL QUERY : set time zone 'Asia/Seoul'; set time zone 'UTC'; show timezone; 18 | @CreateDateColumn({ 19 | type: 'timestamptz' /* timestamp with time zone */, 20 | }) 21 | createdAt: Date 22 | 23 | @UpdateDateColumn({ type: 'timestamptz' }) 24 | updatedAt: Date 25 | 26 | // Soft Delete : 기존에는 null, 삭제시에 timestamp를 찍는다. 27 | @Exclude() 28 | @DeleteDateColumn({ type: 'timestamptz' }) 29 | deletedAt?: Date | null 30 | } 31 | -------------------------------------------------------------------------------- /src/common/exceptions/http-api-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | Catch, 4 | ArgumentsHost, 5 | HttpException, 6 | Logger, 7 | } from '@nestjs/common' 8 | import { Response } from 'express' 9 | 10 | @Catch(HttpException) 11 | export class HttpApiExceptionFilter implements ExceptionFilter { 12 | private readonly logger = new Logger(HttpApiExceptionFilter.name) 13 | 14 | catch(exception: HttpException, host: ArgumentsHost) { 15 | const ctx = host.switchToHttp() 16 | const response = ctx.getResponse() 17 | const status = exception.getStatus() 18 | const error = exception.getResponse() as 19 | | string 20 | | { error: string; statusCode: number; message: string[] } 21 | this.logger.error(error) 22 | if (typeof error === 'string') { 23 | response 24 | .status(status) 25 | .json({ success: true, statusCode: status, message: error }) 26 | } else { 27 | response.status(status).json({ success: false, ...error }) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/common/interceptors/only-admin.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NestInterceptor, 4 | ExecutionContext, 5 | CallHandler, 6 | UnauthorizedException, 7 | } from '@nestjs/common' 8 | import { Request } from 'express' 9 | import { map, Observable } from 'rxjs' 10 | 11 | @Injectable() 12 | export class OnlyAdminInterceptor implements NestInterceptor { 13 | intercept(context: ExecutionContext, next: CallHandler): Observable { 14 | const request: Request = context.switchToHttp().getRequest() 15 | const user = request.user 16 | if (user && user.isAdmin) return next.handle().pipe(map((data) => data)) 17 | else throw new UnauthorizedException('인증에 문제가 있습니다.') 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/common/interceptors/only-private.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NestInterceptor, 4 | ExecutionContext, 5 | CallHandler, 6 | UnauthorizedException, 7 | } from '@nestjs/common' 8 | import { Request } from 'express' 9 | import { map, Observable } from 'rxjs' 10 | 11 | @Injectable() 12 | export class OnlyPrivateInterceptor implements NestInterceptor { 13 | intercept(context: ExecutionContext, next: CallHandler): Observable { 14 | const request: Request = context.switchToHttp().getRequest() 15 | const user = request.user 16 | if (user) return next.handle().pipe(map((data) => data)) 17 | else throw new UnauthorizedException('인증에 문제가 있습니다.') 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/common/utils/jwtExtractorFromCookies.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express' 2 | import { JwtFromRequestFunction } from 'passport-jwt' 3 | 4 | export const jwtExtractorFromCookies: JwtFromRequestFunction = ( 5 | request: Request, 6 | ): string | null => { 7 | try { 8 | const jwt = request.cookies['jwt'] 9 | return jwt 10 | } catch (error) { 11 | return null 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory, Reflector } from '@nestjs/core' 2 | import { AppModule } from './app.module' 3 | import { 4 | ClassSerializerInterceptor, 5 | Logger, 6 | ValidationPipe, 7 | } from '@nestjs/common' 8 | import { NestExpressApplication } from '@nestjs/platform-express' 9 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger' 10 | import * as expressBasicAuth from 'express-basic-auth' 11 | import * as passport from 'passport' 12 | import * as cookieParser from 'cookie-parser' 13 | import { HttpApiExceptionFilter } from './common/exceptions/http-api-exception.filter' 14 | 15 | class Application { 16 | private logger = new Logger(Application.name) 17 | private DEV_MODE: boolean 18 | private PORT: string 19 | private corsOriginList: string[] 20 | private ADMIN_USER: string 21 | private ADMIN_PASSWORD: string 22 | 23 | constructor(private server: NestExpressApplication) { 24 | this.server = server 25 | 26 | if (!process.env.SECRET_KEY) this.logger.error('Set "SECRET" env') 27 | this.DEV_MODE = process.env.NODE_ENV === 'production' ? false : true 28 | this.PORT = process.env.PORT || '5000' 29 | this.corsOriginList = process.env.CORS_ORIGIN_LIST 30 | ? process.env.CORS_ORIGIN_LIST.split(',').map((origin) => origin.trim()) 31 | : ['*'] 32 | this.ADMIN_USER = process.env.ADMIN_USER || 'amamov' 33 | this.ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || '1205' 34 | } 35 | 36 | private setUpBasicAuth() { 37 | this.server.use( 38 | ['/docs', '/docs-json'], 39 | expressBasicAuth({ 40 | challenge: true, 41 | users: { 42 | [this.ADMIN_USER]: this.ADMIN_PASSWORD, 43 | }, 44 | }), 45 | ) 46 | } 47 | 48 | private setUpOpenAPIMidleware() { 49 | SwaggerModule.setup( 50 | 'docs', 51 | this.server, 52 | SwaggerModule.createDocument( 53 | this.server, 54 | new DocumentBuilder() 55 | .setTitle('Yoon Sang Seok - API') 56 | .setDescription('TypeORM In Nest') 57 | .setVersion('0.0.1') 58 | .build(), 59 | ), 60 | ) 61 | } 62 | 63 | private async setUpGlobalMiddleware() { 64 | this.server.enableCors({ 65 | origin: this.corsOriginList, 66 | credentials: true, 67 | }) 68 | this.server.use(cookieParser()) 69 | this.setUpBasicAuth() 70 | this.setUpOpenAPIMidleware() 71 | this.server.useGlobalPipes( 72 | new ValidationPipe({ 73 | transform: true, 74 | }), 75 | ) 76 | this.server.use(passport.initialize()) 77 | this.server.use(passport.session()) 78 | this.server.useGlobalInterceptors( 79 | new ClassSerializerInterceptor(this.server.get(Reflector)), 80 | ) 81 | this.server.useGlobalFilters(new HttpApiExceptionFilter()) 82 | } 83 | 84 | async boostrap() { 85 | await this.setUpGlobalMiddleware() 86 | await this.server.listen(this.PORT) 87 | } 88 | 89 | startLog() { 90 | if (this.DEV_MODE) { 91 | this.logger.log(`✅ Server on http://localhost:${this.PORT}`) 92 | } else { 93 | this.logger.log(`✅ Server on port ${this.PORT}...`) 94 | } 95 | } 96 | 97 | errorLog(error: string) { 98 | this.logger.error(`🆘 Server error ${error}`) 99 | } 100 | } 101 | 102 | async function init(): Promise { 103 | const server = await NestFactory.create(AppModule) 104 | const app = new Application(server) 105 | await app.boostrap() 106 | app.startLog() 107 | } 108 | 109 | init().catch((error) => { 110 | new Logger('init').error(error) 111 | }) 112 | -------------------------------------------------------------------------------- /src/profiles/profiles.entity.ts: -------------------------------------------------------------------------------- 1 | import { CommonEntity } from '../common/entities/common.entity' // ormconfig.json에서 파싱 가능하도록 상대 경로로 지정 2 | import { Column, Entity } from 'typeorm' 3 | 4 | @Entity({ 5 | name: 'USER_PROFILE', 6 | }) 7 | export class ProfileEntity extends CommonEntity { 8 | @Column({ type: 'varchar', nullable: true }) 9 | bio: string 10 | 11 | @Column({ type: 'varchar', nullable: true }) 12 | site: string 13 | } 14 | -------------------------------------------------------------------------------- /src/profiles/profiles.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | @Module({}) 4 | export class ProfilesModule {} 5 | -------------------------------------------------------------------------------- /src/tags/tags.entity.ts: -------------------------------------------------------------------------------- 1 | import { CommonEntity } from '../common/entities/common.entity' // ormconfig.json에서 파싱 가능하도록 상대 경로로 지정 2 | import { Column, Entity, ManyToMany } from 'typeorm' 3 | import { BlogEntity } from '../blogs/blogs.entity' 4 | 5 | @Entity({ 6 | name: 'TAG', 7 | }) 8 | export class TagEntity extends CommonEntity { 9 | @Column({ type: 'varchar', nullable: true }) 10 | name: string 11 | 12 | //* Relation */ 13 | 14 | @ManyToMany(() => BlogEntity, (blog: BlogEntity) => blog.tags) 15 | blogs: BlogEntity[] 16 | } 17 | -------------------------------------------------------------------------------- /src/tags/tags.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | @Module({}) 4 | export class TagsModule {} 5 | -------------------------------------------------------------------------------- /src/typing.d.ts: -------------------------------------------------------------------------------- 1 | import { UserDTO } from 'src/users/dtos/user.dto' 2 | 3 | declare global { 4 | namespace Express { 5 | interface User extends UserDTO {} 6 | 7 | interface Request { 8 | token?: string 9 | user?: any 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/users/dtos/user-login.dto.ts: -------------------------------------------------------------------------------- 1 | import { PickType } from '@nestjs/swagger' 2 | import { IsNotEmpty, IsString } from 'class-validator' 3 | import { UserEntity } from '../users.entity' 4 | 5 | export class UserLogInDTO extends PickType(UserEntity, ['email'] as const) { 6 | @IsString() 7 | @IsNotEmpty({ message: '비밀번호를 작성해주세요.' }) 8 | password: string 9 | } 10 | -------------------------------------------------------------------------------- /src/users/dtos/user-register.dto.ts: -------------------------------------------------------------------------------- 1 | import { PickType } from '@nestjs/swagger' 2 | import { IsNotEmpty, IsString } from 'class-validator' 3 | import { UserEntity } from '../users.entity' 4 | 5 | export class UserRegisterDTO extends PickType(UserEntity, [ 6 | 'email', 7 | 'username', 8 | ] as const) { 9 | @IsString() 10 | @IsNotEmpty({ message: '비밀번호를 작성해주세요.' }) 11 | password: string 12 | } 13 | -------------------------------------------------------------------------------- /src/users/dtos/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { OmitType } from '@nestjs/swagger' 2 | import { UserEntity } from '../users.entity' 3 | 4 | export class UserDTO extends OmitType(UserEntity, ['password'] as const) {} 5 | -------------------------------------------------------------------------------- /src/users/jwt/jwt.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExecutionContext, 3 | Injectable, 4 | // UnauthorizedException, 5 | } from '@nestjs/common' 6 | import { AuthGuard } from '@nestjs/passport' 7 | 8 | @Injectable() 9 | export class JwtAuthGuard extends AuthGuard('jwt') { 10 | canActivate(context: ExecutionContext) { 11 | return super.canActivate(context) 12 | } 13 | 14 | handleRequest(err: any, user: any, info: any) { 15 | if (err || !user) { 16 | // throw err || new UnauthorizedException('인증 문제가 있습니다.') 17 | } 18 | return user 19 | } 20 | } 21 | 22 | //* guard -> strategy 23 | -------------------------------------------------------------------------------- /src/users/jwt/jwt.payload.ts: -------------------------------------------------------------------------------- 1 | export class JwtPayload { 2 | sub: string 3 | iat?: number 4 | exp?: number 5 | } 6 | -------------------------------------------------------------------------------- /src/users/jwt/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { JwtPayload } from './jwt.payload' 2 | import { UsersService } from '../users.service' 3 | import { ExtractJwt, Strategy } from 'passport-jwt' 4 | import { PassportStrategy } from '@nestjs/passport' 5 | import { Injectable, UnauthorizedException } from '@nestjs/common' 6 | import { jwtExtractorFromCookies } from '../../common/utils/jwtExtractorFromCookies' 7 | import { ConfigService } from '@nestjs/config' 8 | 9 | @Injectable() 10 | export class JwtStrategy extends PassportStrategy(Strategy) { 11 | constructor( 12 | private readonly usersService: UsersService, 13 | private readonly configService: ConfigService, 14 | ) { 15 | super({ 16 | jwtFromRequest: ExtractJwt.fromExtractors([jwtExtractorFromCookies]), 17 | secretOrKey: configService.get('SECRET_KEY'), 18 | ignoreExpiration: false, 19 | }) 20 | } 21 | 22 | async validate(payload: JwtPayload) { 23 | try { 24 | const user = await this.usersService.findUserById(payload.sub) 25 | if (user) { 26 | return user 27 | } else { 28 | throw new Error('해당하는 유저는 없습니다.') 29 | } 30 | } catch (error) { 31 | throw new UnauthorizedException(error) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | Logger, 6 | Post, 7 | Res, 8 | UseGuards, 9 | UseInterceptors, 10 | } from '@nestjs/common' 11 | import { Response } from 'express' 12 | import { UsersService } from './users.service' 13 | import { UserLogInDTO } from './dtos/user-login.dto' 14 | import { UserRegisterDTO } from './dtos/user-register.dto' 15 | import { InjectRepository } from '@nestjs/typeorm' 16 | import { UserEntity } from './users.entity' 17 | import { Repository } from 'typeorm' 18 | import { OnlyPrivateInterceptor } from '../common/interceptors/only-private.interceptor' 19 | import { CurrentUser } from '../common/decorators/current-user.decorator' 20 | import { UserDTO } from './dtos/user.dto' 21 | import { JwtAuthGuard } from './jwt/jwt.guard' 22 | 23 | @Controller('users') 24 | export class UsersController { 25 | private readonly logger = new Logger(UsersController.name) 26 | 27 | constructor( 28 | private readonly usersService: UsersService, 29 | @InjectRepository(UserEntity) 30 | private readonly usersRepository: Repository, 31 | ) {} 32 | 33 | @Get() 34 | @UseGuards(JwtAuthGuard) 35 | @UseInterceptors(OnlyPrivateInterceptor) 36 | async getCurrentUser(@CurrentUser() currentUser: UserDTO) { 37 | return currentUser 38 | } 39 | 40 | @Post() 41 | async signUp(@Body() userRegisterDTO: UserRegisterDTO) { 42 | return await this.usersService.registerUser(userRegisterDTO) 43 | } 44 | 45 | @Post('login') 46 | async logIn( 47 | @Body() userLoginDTO: UserLogInDTO, 48 | @Res({ passthrough: true }) response: Response, 49 | ) { 50 | const { jwt, user } = await this.usersService.verifyUserAndSignJwt( 51 | userLoginDTO.email, 52 | userLoginDTO.password, 53 | ) 54 | response.cookie('jwt', jwt, { httpOnly: true }) 55 | return user 56 | } 57 | 58 | @Post('logout') 59 | async logOut(@Res({ passthrough: true }) response: Response) { 60 | response.clearCookie('jwt') 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/users/users.entity.ts: -------------------------------------------------------------------------------- 1 | import { IsBoolean, IsEmail, IsNotEmpty, IsString } from 'class-validator' 2 | import { CommonEntity } from '../common/entities/common.entity' // ormconfig.json에서 파싱 가능하도록 상대 경로로 지정 3 | import { Column, Entity, Index, JoinColumn, OneToMany, OneToOne } from 'typeorm' 4 | import { Exclude } from 'class-transformer' 5 | import { BlogEntity } from '../blogs/blogs.entity' 6 | import { ProfileEntity } from '../profiles/profiles.entity' 7 | 8 | @Index('email', ['email'], { unique: true }) 9 | @Entity({ 10 | name: 'USER', 11 | }) // USER : 테이블 명 12 | export class UserEntity extends CommonEntity { 13 | @IsEmail({}, { message: '올바른 이메일을 작성해주세요.' }) 14 | @IsNotEmpty({ message: '이메일을 작성해주세요.' }) 15 | @Column({ type: 'varchar', unique: true, nullable: false }) 16 | email: string 17 | 18 | @IsString() 19 | @IsNotEmpty({ message: '이름을 작성해주세요.' }) 20 | @Column({ type: 'varchar', nullable: false }) 21 | username: string 22 | 23 | @Exclude() 24 | @Column({ type: 'varchar', nullable: false }) 25 | password: string 26 | 27 | @IsBoolean() 28 | @Column({ type: 'boolean', default: false }) 29 | isAdmin: boolean 30 | 31 | //* Relation */ 32 | 33 | @OneToOne(() => ProfileEntity) // 단방향 연결, 양방향도 가능 34 | @JoinColumn({ name: 'profile_id', referencedColumnName: 'id' }) 35 | profile: ProfileEntity 36 | 37 | @OneToMany(() => BlogEntity, (blog: BlogEntity) => blog.author, { 38 | cascade: true, // 사용자를 통해 블로그가 추가, 수정, 삭제되고 사용자가 저장되면 추가된 블로그도 저장된다. 39 | }) 40 | blogs: BlogEntity[] 41 | } 42 | 43 | /* 44 | const author = await User.findOne( { id: '...' } ) 45 | author.blogs.push(new BlogEntity(...)) 46 | await author.save() 47 | */ 48 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { UsersController } from './users.controller' 3 | import { UserEntity } from './users.entity' 4 | import { PassportModule } from '@nestjs/passport' 5 | import { JwtModule } from '@nestjs/jwt' 6 | import { JwtStrategy } from './jwt/jwt.strategy' 7 | import { TypeOrmModule } from '@nestjs/typeorm' 8 | import { UsersService } from './users.service' 9 | 10 | @Module({ 11 | imports: [ 12 | TypeOrmModule.forFeature([UserEntity]), 13 | PassportModule.register({ defaultStrategy: 'jwt', session: true }), 14 | JwtModule.register({ 15 | secret: process.env.SECRET_KEY, 16 | secretOrPrivateKey: process.env.SECRET_KEY, 17 | signOptions: { expiresIn: '1d' }, 18 | }), 19 | ], 20 | providers: [JwtStrategy, UsersService], 21 | controllers: [UsersController], 22 | exports: [UsersService], 23 | }) 24 | export class UsersModule {} 25 | -------------------------------------------------------------------------------- /src/users/users.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@nestjs/config' 2 | import { JwtService } from '@nestjs/jwt' 3 | import { Test, TestingModule } from '@nestjs/testing' 4 | import { getRepositoryToken } from '@nestjs/typeorm' 5 | import * as bcrypt from 'bcrypt' 6 | import { UserRegisterDTO } from './dtos/user-register.dto' 7 | import { UserEntity } from './users.entity' 8 | import { UsersService } from './users.service' 9 | 10 | const mockUserId = 'c1f0e942-af78-4460-b3cc-4b0f6bfc1174' 11 | const mockJwt = 'thisisjwt' 12 | 13 | const password = '1205' 14 | 15 | const mockUser = { 16 | id: mockUserId, 17 | email: 'me@amamov.com', 18 | username: 'amamov', 19 | password: bcrypt.hashSync(password, 10), 20 | isAdmin: false, 21 | } 22 | 23 | class MockUsersRepository { 24 | save = jest.fn().mockResolvedValue(mockUser) 25 | 26 | async findOne(where: { id?: string; email?: string }) { 27 | if (where?.id === mockUserId) return mockUser 28 | else if (where?.email === mockUser.email) return mockUser 29 | else return null 30 | } 31 | } 32 | 33 | const mockJwtService = () => ({ 34 | signAsync: jest.fn().mockResolvedValue(mockJwt), 35 | }) 36 | 37 | const mockConfigService = () => ({ 38 | get: jest.fn().mockReturnValue('thisissecretkey'), 39 | }) 40 | 41 | describe('UsersService', () => { 42 | let usersService: UsersService 43 | let usersRepository: MockUsersRepository 44 | let jwtService: JwtService 45 | 46 | beforeEach(async () => { 47 | const module: TestingModule = await Test.createTestingModule({ 48 | providers: [ 49 | UsersService, // { provide : UsersService, useClass : UsersService } 50 | { 51 | provide: getRepositoryToken(UserEntity), 52 | useClass: MockUsersRepository, 53 | }, 54 | { 55 | provide: JwtService, 56 | useValue: mockJwtService(), 57 | }, 58 | { 59 | provide: ConfigService, 60 | useValue: mockConfigService(), 61 | }, 62 | ], 63 | }).compile() 64 | 65 | usersService = module.get(UsersService) 66 | jwtService = module.get(JwtService) 67 | usersRepository = module.get(getRepositoryToken(UserEntity)) 68 | }) 69 | 70 | it('UsersService should be defined', () => { 71 | expect(usersService).toBeDefined() 72 | }) 73 | 74 | describe('registerUser function', () => { 75 | const newUser: UserRegisterDTO = { 76 | email: 'new@amamov.com', 77 | password: '1205', 78 | username: 'new', 79 | } 80 | const existedUser: UserRegisterDTO = { 81 | email: mockUser.email, 82 | password: mockUser.password, 83 | username: mockUser.username, 84 | } 85 | 86 | it('should be defined', () => { 87 | expect(usersService.registerUser).toBeDefined() 88 | }) 89 | 90 | it('유저 정보의 이메일은 유일해야 하므로 DB에서 유일성 검사를 한다.', async () => { 91 | expect(usersRepository.findOne).toBeDefined() 92 | expect(usersService.registerUser(newUser)).resolves 93 | await expect( 94 | usersService.registerUser(existedUser), 95 | ).rejects.toThrowError() 96 | }) 97 | 98 | it('유저 정보를 인자로 받고 새로운 유저를 생성하고 아무것도 반환하지 않는다.', async () => { 99 | await expect(usersService.registerUser(newUser)).resolves.toBeUndefined() 100 | }) 101 | }) 102 | 103 | describe('verifyUserAndSignJwt function', () => { 104 | it('should be defined', () => { 105 | expect(usersService.verifyUserAndSignJwt).toBeDefined() 106 | }) 107 | 108 | it('jwtService should be defined', () => { 109 | expect(jwtService.signAsync).toBeDefined() 110 | }) 111 | 112 | it('이메일로 유저를 찾고 없으면 400 에러를 발생시킨다.', async () => { 113 | try { 114 | await usersService.verifyUserAndSignJwt( 115 | 'nothing@amamov.com', 116 | mockUser.password, 117 | ) 118 | throw new Error('테스팅 에러') 119 | } catch (error) { 120 | expect(error.message).toBe('해당하는 이메일은 존재하지 않습니다.') 121 | } 122 | }) 123 | 124 | it('암호화된 비밀번호를 복호화하여 비교 한다.', async () => { 125 | try { 126 | await usersService.verifyUserAndSignJwt( 127 | mockUser.email, 128 | mockUser.password, 129 | ) 130 | throw new Error('테스팅 에러') 131 | } catch (error) { 132 | expect(error.message).toBe('로그인에 실패하였습니다.') 133 | } 134 | try { 135 | await usersService.verifyUserAndSignJwt(mockUser.email, password) 136 | } catch (error) { 137 | expect(error.message).toBeUndefined() 138 | } 139 | }) 140 | 141 | it('만일 비밀번호가 다르다면 에러를 발생시킨다.', async () => { 142 | try { 143 | await usersService.verifyUserAndSignJwt(mockUser.email, '1205!') 144 | throw new Error('테스팅 에러') 145 | } catch (error) { 146 | expect(error.message).toBe('로그인에 실패하였습니다.') 147 | } 148 | }) 149 | 150 | it('서명된 JWT와 UserDTO를 반환한다.', async () => { 151 | try { 152 | const user = await usersService.verifyUserAndSignJwt( 153 | mockUser.email, 154 | password, 155 | ) 156 | expect(user).toEqual({ 157 | jwt: mockJwt, 158 | user: mockUser, 159 | }) 160 | } catch (error) { 161 | expect(error).toBeUndefined() 162 | } 163 | }) 164 | }) 165 | 166 | describe('findUserById function', () => { 167 | it('should be defined', () => { 168 | expect(usersService.findUserById).toBeDefined() 169 | }) 170 | 171 | it('id로 DB에 존재하는 User를 찾는다.', async () => { 172 | await expect(usersService.findUserById(mockUserId)).resolves.toEqual( 173 | mockUser, 174 | ) 175 | }) 176 | 177 | it('id로 DB에 존재하지 않는 User를 찾는 경우, 400 에러를 발생시킨다.', async () => { 178 | await expect(usersService.findUserById('fakeid')).rejects.toThrowError() 179 | }) 180 | }) 181 | }) 182 | -------------------------------------------------------------------------------- /src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Injectable, 4 | Logger, 5 | UnauthorizedException, 6 | } from '@nestjs/common' 7 | import { InjectRepository } from '@nestjs/typeorm' 8 | import { JwtService } from '@nestjs/jwt' 9 | import { Repository } from 'typeorm' 10 | import { UserEntity } from './users.entity' 11 | import * as bcrypt from 'bcrypt' 12 | import { UserDTO } from './dtos/user.dto' 13 | import { UserLogInDTO } from './dtos/user-login.dto' 14 | import { UserRegisterDTO } from './dtos/user-register.dto' 15 | import { ConfigService } from '@nestjs/config' 16 | 17 | @Injectable() 18 | export class UsersService { 19 | private readonly logger = new Logger(UsersService.name) 20 | 21 | constructor( 22 | @InjectRepository(UserEntity) 23 | private readonly usersRepository: Repository, 24 | private readonly jwtService: JwtService, 25 | private readonly configService: ConfigService, 26 | ) {} 27 | 28 | async registerUser(userRegisterDTO: UserRegisterDTO): Promise { 29 | const { email, password } = userRegisterDTO 30 | const user = await this.usersRepository.findOne({ email }) 31 | if (user) { 32 | throw new UnauthorizedException('해당하는 이메일은 이미 존재합니다.') 33 | } 34 | const hashedPassword = await bcrypt.hash(password, 10) 35 | await this.usersRepository.save({ 36 | ...userRegisterDTO, 37 | password: hashedPassword, 38 | }) 39 | } 40 | 41 | async verifyUserAndSignJwt( 42 | email: UserLogInDTO['email'], 43 | password: UserLogInDTO['password'], 44 | ): Promise<{ jwt: string; user: UserDTO }> { 45 | const user = await this.usersRepository.findOne({ email }) 46 | if (!user) 47 | throw new UnauthorizedException('해당하는 이메일은 존재하지 않습니다.') 48 | if (!(await bcrypt.compare(password, user.password))) 49 | throw new UnauthorizedException('로그인에 실패하였습니다.') 50 | try { 51 | const jwt = await this.jwtService.signAsync( 52 | { sub: user.id }, 53 | { secret: this.configService.get('SECRET_KEY') }, 54 | ) 55 | return { jwt, user } 56 | } catch (error) { 57 | throw new BadRequestException(error.message) 58 | } 59 | } 60 | 61 | async findUserById(id: string) { 62 | try { 63 | const user = await this.usersRepository.findOne({ id }) 64 | if (!user) throw new Error() 65 | return user 66 | } catch (error) { 67 | throw new BadRequestException('해당하는 사용자를 찾을 수 없습니다.') 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/visitors/dto/create-visitor.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateVisitorDto {} 2 | -------------------------------------------------------------------------------- /src/visitors/dto/update-visitor.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { CreateVisitorDto } from './create-visitor.dto'; 3 | 4 | export class UpdateVisitorDto extends PartialType(CreateVisitorDto) {} 5 | -------------------------------------------------------------------------------- /src/visitors/visitors.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing' 2 | import { VisitorsController } from './visitors.controller' 3 | import { VisitorsService } from './visitors.service' 4 | 5 | describe('VisitorsController', () => { 6 | let controller: VisitorsController 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [VisitorsController], 11 | providers: [VisitorsService], 12 | }).compile() 13 | 14 | controller = module.get(VisitorsController) 15 | }) 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined() 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /src/visitors/visitors.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | } from '@nestjs/common' 10 | import { VisitorsService } from './visitors.service' 11 | import { CreateVisitorDto } from './dto/create-visitor.dto' 12 | import { UpdateVisitorDto } from './dto/update-visitor.dto' 13 | 14 | @Controller('visitors') 15 | export class VisitorsController { 16 | constructor(private readonly visitorsService: VisitorsService) {} 17 | 18 | @Post() 19 | create(@Body() createVisitorDto: CreateVisitorDto) { 20 | return this.visitorsService.create(createVisitorDto) 21 | } 22 | 23 | @Get() 24 | findAll() { 25 | return this.visitorsService.findAll() 26 | } 27 | 28 | @Get(':id') 29 | findOne(@Param('id') id: string) { 30 | return this.visitorsService.findOne(+id) 31 | } 32 | 33 | @Patch(':id') 34 | update(@Param('id') id: string, @Body() updateVisitorDto: UpdateVisitorDto) { 35 | return this.visitorsService.update(+id, updateVisitorDto) 36 | } 37 | 38 | @Delete(':id') 39 | remove(@Param('id') id: string) { 40 | return this.visitorsService.remove(+id) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/visitors/visitors.entity.ts: -------------------------------------------------------------------------------- 1 | import { CommonEntity } from '../common/entities/common.entity' // ormconfig.json에서 파싱 가능하도록 상대 경로로 지정 2 | import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm' 3 | import { IsIP, IsNotEmpty } from 'class-validator' 4 | import { BlogEntity } from '../blogs/blogs.entity' 5 | 6 | @Entity({ 7 | name: 'VISITOR', 8 | }) 9 | export class VisitorEntity extends CommonEntity { 10 | @IsIP() 11 | @IsNotEmpty() 12 | @Column({ type: 'inet', nullable: false }) 13 | ip: string 14 | 15 | //* Relation */ 16 | 17 | @ManyToOne(() => BlogEntity, (blog: BlogEntity) => blog.visitors, { 18 | onDelete: 'SET NULL', 19 | }) 20 | @JoinColumn([{ name: 'blog_id', referencedColumnName: 'id' }]) 21 | blog: BlogEntity 22 | } 23 | -------------------------------------------------------------------------------- /src/visitors/visitors.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { VisitorsService } from './visitors.service' 3 | import { VisitorsController } from './visitors.controller' 4 | 5 | @Module({ 6 | controllers: [VisitorsController], 7 | providers: [VisitorsService], 8 | }) 9 | export class VisitorsModule {} 10 | -------------------------------------------------------------------------------- /src/visitors/visitors.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing' 2 | import { VisitorsService } from './visitors.service' 3 | 4 | describe('VisitorsService', () => { 5 | let service: VisitorsService 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [VisitorsService], 10 | }).compile() 11 | 12 | service = module.get(VisitorsService) 13 | }) 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined() 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/visitors/visitors.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { CreateVisitorDto } from './dto/create-visitor.dto' 3 | import { UpdateVisitorDto } from './dto/update-visitor.dto' 4 | 5 | @Injectable() 6 | export class VisitorsService { 7 | create(createVisitorDto: CreateVisitorDto) { 8 | return 'This action adds a new visitor' 9 | } 10 | 11 | findAll() { 12 | return `This action returns all visitors` 13 | } 14 | 15 | findOne(id: number) { 16 | return `This action returns a #${id} visitor` 17 | } 18 | 19 | update(id: number, updateVisitorDto: UpdateVisitorDto) { 20 | return `This action updates a #${id} visitor` 21 | } 22 | 23 | remove(id: number) { 24 | return `This action removes a #${id} visitor` 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing' 2 | import { INestApplication } from '@nestjs/common' 3 | import * as request from 'supertest' 4 | import { AppModule } from './../src/app.module' 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile() 13 | 14 | app = moduleFixture.createNestApplication() 15 | await app.init() 16 | }) 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('typeorm in nest, just coding') 23 | }) 24 | 25 | describe('hello jest', () => { 26 | it('two plus two is four', () => { 27 | expect(2 + 2).toBe(4) 28 | }) 29 | }) 30 | 31 | describe('/users', () => { 32 | it('/users (GET)', async () => { 33 | const res = await request(app.getHttpServer()).get('/users') 34 | expect(res.statusCode).toBe(401) 35 | }) 36 | 37 | it('/users (POST)', async () => { 38 | const res = await request(app.getHttpServer()).post('/users').send({ 39 | email: 'test@amamov.com', 40 | password: '1205', 41 | username: 'test', 42 | }) 43 | 44 | expect(res.statusCode).toBe(401) 45 | // expect(res.body).toBe({ 46 | // email: 'test@amamov.com', 47 | // username: 'test', 48 | // id: '419cea5a-8826-41f3-bba0-69d9e4d09eaa', 49 | // createdAt: '2021-11-11T07:34:28.617Z', 50 | // updatedAt: '2021-11-11T07:34:28.617Z', 51 | // }) 52 | }) 53 | }) 54 | 55 | it('/users/login (POST)', async () => { 56 | const res = await request(app.getHttpServer()).post('/users/login').send({ 57 | email: 'test@amamov.com', 58 | password: '1205', 59 | }) 60 | expect(res.statusCode).toBe(200) // 201 61 | console.log(res.headers) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /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 | 4 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": false, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | --------------------------------------------------------------------------------