├── src ├── dto │ ├── README.md │ ├── BoardDTO.ts │ ├── CommentDTO.ts │ └── UserDTO.ts ├── provider │ ├── README.md │ ├── KakaoProvider.ts │ └── BaseProvider.ts ├── utils │ ├── README.md │ ├── env.ts │ ├── apiClient.ts │ ├── routingConfig.ts │ ├── swagger.ts │ └── Authenticate.ts ├── model │ ├── README.md │ ├── migration │ │ └── README.md │ ├── Enum.ts │ ├── index.ts │ ├── BaseComment.ts │ ├── BaseBoard.ts │ ├── ExampleBoards.ts │ ├── ExampleBoardDepthComments.ts │ ├── APIlogs.ts │ ├── UserAccounts.ts │ ├── BaseModel.ts │ ├── ExampleBoardComments.ts │ └── Users.ts ├── test │ ├── README.md │ ├── example.spec.ts │ ├── api │ │ └── example.spec.ts │ └── service │ │ └── example.spec.ts ├── database │ ├── README.md │ ├── index.ts │ ├── config.ts │ ├── NamingStrategy.ts │ └── cli.ts ├── service │ ├── README.md │ ├── index.ts │ ├── APILogService.ts │ ├── ExampleBoardCommentService.ts │ ├── ExampleBoardService.ts │ ├── ExampleBoardDepthCommentService.ts │ ├── BaseCommentService.ts │ ├── UserAccountService.ts │ ├── BaseBoardService.ts │ ├── UserService.ts │ └── BaseService.ts ├── controller │ ├── README.md │ ├── BaseController.ts │ ├── KaKaoAuthController.ts │ ├── BaseCommentController.ts │ ├── BaseAuthController.ts │ ├── UserController.ts │ ├── ExampleBoardController.ts │ ├── ExampleBoardCommentController.ts │ └── ExampleBoardDepthCommentController.ts ├── interceptor │ ├── README.md │ └── ResponseJsonInterceptor.ts ├── middleware │ ├── README.md │ ├── CheckJWT.ts │ └── WriteLog.ts ├── index.ts ├── view │ └── index.html └── server.ts ├── .gitignore ├── .env.example ├── nodemon.json ├── tslint.json ├── CHANGELOG.md ├── .dockerignore ├── .prettierrc ├── Dockerfile ├── dev.Dockerfile ├── .eslintrc.js ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── enhancement_request.md │ └── bug_report.md └── workflows │ └── aws.yml ├── tsconfig.json ├── docker-compose.yml ├── LICENSE ├── .vscode ├── launch.json └── task_definition.json ├── package.json ├── CONTRIBUTING.md ├── jest.config.js └── README.md /src/dto/README.md: -------------------------------------------------------------------------------- 1 | # dto -------------------------------------------------------------------------------- /src/provider/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/model/README.md: -------------------------------------------------------------------------------- 1 | # model -------------------------------------------------------------------------------- /src/test/README.md: -------------------------------------------------------------------------------- 1 | # test -------------------------------------------------------------------------------- /src/database/README.md: -------------------------------------------------------------------------------- 1 | # database -------------------------------------------------------------------------------- /src/service/README.md: -------------------------------------------------------------------------------- 1 | # service -------------------------------------------------------------------------------- /src/controller/README.md: -------------------------------------------------------------------------------- 1 | # controller -------------------------------------------------------------------------------- /src/interceptor/README.md: -------------------------------------------------------------------------------- 1 | # interceptor -------------------------------------------------------------------------------- /src/middleware/README.md: -------------------------------------------------------------------------------- 1 | # middleware -------------------------------------------------------------------------------- /src/model/migration/README.md: -------------------------------------------------------------------------------- 1 | # migration -------------------------------------------------------------------------------- /src/controller/BaseController.ts: -------------------------------------------------------------------------------- 1 | export abstract class BaseController {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env.development 3 | .env.production 4 | dist/ 5 | yarn-error.log 6 | .env -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DB_HOST= 2 | DB_USER= 3 | DB_PW= 4 | PORT= 5 | DB_PORT= 6 | DATABASE= 7 | TEST_TOKEN= 8 | SENTRY_DSN= 9 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src/*"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node ./src/index.ts" 6 | } 7 | -------------------------------------------------------------------------------- /src/test/example.spec.ts: -------------------------------------------------------------------------------- 1 | describe('test start', () => { 2 | test('example test', () => { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "gts/tslint.json", 3 | "linterOptions": { 4 | "exclude": [ 5 | "**/*.json" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/env.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | 3 | import { config } from 'dotenv'; 4 | 5 | config({ path: resolve(__dirname, `../../.env`) }); 6 | -------------------------------------------------------------------------------- /src/utils/apiClient.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | export const apiClient = (url: string, headers?: object) => { 3 | return axios.create({ 4 | baseURL: url, 5 | headers: headers || {}, 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /src/database/index.ts: -------------------------------------------------------------------------------- 1 | import { createConnection } from 'typeorm'; 2 | 3 | import { typeOrmConfig } from './config'; 4 | 5 | export async function connectDatabase() { 6 | return createConnection(typeOrmConfig); 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog and release notes 2 | 3 | ### 1.0.0 4 | 5 | #### Feature 6 | 7 | - Base API ( user, board ) 8 | - Db setting 9 | - Test 10 | - Error handling 11 | - Swagger 12 | - Redoc 13 | - action ( deploy ) 14 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | ### Git ### 2 | .git 3 | .gitignore 4 | 5 | ### not production code ### 6 | **/*.md 7 | LICENSE 8 | yarn-error.log 9 | src/test 10 | nodemon.json 11 | node_modules 12 | *.js 13 | 14 | ### VisualStudioCode ### 15 | .vscode/ 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "printWidth": 80, 8 | "arrowParens": "always", 9 | "bracketSpacing": true, 10 | "useTabs": false 11 | } 12 | -------------------------------------------------------------------------------- /src/dto/BoardDTO.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | import { IboardDTO } from '../service/BaseBoardService'; 3 | 4 | export class IboardDTOClass implements Pick { 5 | @IsString() 6 | title!: string; 7 | @IsString() 8 | content!: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/model/Enum.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | // you can put your enum here 3 | 4 | export enum Provider { 5 | GOOGLE = 'google', 6 | KAKAO = 'kakao', 7 | NAVER = 'naver', 8 | LOCAL = 'local', 9 | } 10 | 11 | export enum Method { 12 | GET = 'get', 13 | POST = 'post', 14 | PUT = 'put', 15 | DELETE = 'delete', 16 | } 17 | -------------------------------------------------------------------------------- /src/test/api/example.spec.ts: -------------------------------------------------------------------------------- 1 | import supertest from 'supertest'; 2 | import { app } from '../../server'; 3 | 4 | describe('test example feature', () => { 5 | test('GET /examples', async () => { 6 | const res = await supertest(app) 7 | .get('/examples') 8 | .expect(200); 9 | expect(res.body).toEqual({ data: 'examples' }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/model/index.ts: -------------------------------------------------------------------------------- 1 | // export module 2 | export { User } from './Users'; 3 | export { UserAccount } from './UserAccounts'; 4 | export { ExampleBoard } from './ExampleBoards'; 5 | export { ExampleBoardComment } from './ExampleBoardComments'; 6 | export { ExampleBoardDepthComment } from './ExampleBoardDepthComments'; 7 | export { APILog } from './APIlogs'; 8 | export { BaseComment } from './BaseComment'; 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.16.0-alpine 2 | 3 | ENV TZ="/usr/share/zoneinfo/Asia/Seoul" 4 | ENV HOST 0.0.0.0 5 | 6 | ARG PROJECT_DIR=/usr/src/app 7 | 8 | RUN npm -g install yarn 9 | 10 | WORKDIR ${PROJECT_DIR} 11 | 12 | COPY . ${PROJECT_DIR} 13 | RUN yarn install 14 | 15 | RUN yarn build 16 | 17 | # change this to your custom port 18 | EXPOSE 3000 19 | 20 | # RUN start 21 | CMD ["yarn", "start"] 22 | -------------------------------------------------------------------------------- /dev.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.16.0-alpine 2 | 3 | ENV TZ="/usr/share/zoneinfo/Asia/Seoul" 4 | ENV HOST 0.0.0.0 5 | 6 | ARG PROJECT_DIR=/usr/src/app 7 | 8 | RUN npm -g install yarn 9 | 10 | WORKDIR ${PROJECT_DIR} 11 | 12 | COPY . ${PROJECT_DIR} 13 | RUN yarn install 14 | 15 | RUN yarn build 16 | 17 | # change this to your custom port 18 | EXPOSE 3000 19 | 20 | # RUN start 21 | CMD ["yarn", "dev"] 22 | -------------------------------------------------------------------------------- /src/dto/CommentDTO.ts: -------------------------------------------------------------------------------- 1 | import { IcommentDTO, IdepthCommentDTO } from '../service/BaseCommentService'; 2 | 3 | export class IcommentDTOClass 4 | implements Pick { 5 | comment!: string; 6 | boardId!: number; 7 | } 8 | 9 | export class IdepthCommentDTOClass 10 | implements Pick { 11 | comment!: string; 12 | commentId!: number; 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: ['./node_modules/gts'], 8 | globals: { 9 | Atomics: 'readonly', 10 | SharedArrayBuffer: 'readonly', 11 | }, 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | ecmaVersion: 2018, 15 | sourceType: 'module', 16 | }, 17 | plugins: ['@typescript-eslint'], 18 | rules: {}, 19 | }; 20 | -------------------------------------------------------------------------------- /src/service/index.ts: -------------------------------------------------------------------------------- 1 | export { UserService } from './UserService'; 2 | export { UserAccountService } from './UserAccountService'; 3 | export { BaseBoardService } from './BaseBoardService'; 4 | export { ExampleBoardService } from './ExampleBoardService'; 5 | export { ExampleBoardCommentService } from './ExampleBoardCommentService'; 6 | export { ExampleBoardDepthCommentService } from './ExampleBoardDepthCommentService'; 7 | export { ApiLogService } from './ApiLogService'; 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## thank you for your pull request! Please write down according to form below 2 | 3 | ## Purpose 4 | 5 | - Write the purpose this task. if task has a issue, write the issue number. 6 | 7 | ## Why 8 | 9 | - Write the resonable backgrounds about this task. 10 | 11 | ## Related issue 12 | 13 | - Link related issues 14 | 15 | ## Additional context 16 | 17 | - Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /src/utils/routingConfig.ts: -------------------------------------------------------------------------------- 1 | import { Authentication } from './Authenticate'; 2 | 3 | export const routingControllerOptions = { 4 | cors: true, 5 | // You should put currentUserChecker to use CurrentUser() in Controller 6 | currentUserChecker: Authentication.currentUserChecker, 7 | controllers: [`${__dirname}/../controller/*.[jt]s`], 8 | middlewares: [`${__dirname}/../middleware/*.[jt]s`], 9 | interceptors: [`${__dirname}/../interceptor/*.[jt]s`], 10 | }; 11 | -------------------------------------------------------------------------------- /src/model/BaseComment.ts: -------------------------------------------------------------------------------- 1 | import { Column } from 'typeorm'; 2 | import { BaseModel } from './BaseModel'; 3 | import { IsString, MaxLength, IsInt } from 'class-validator'; 4 | 5 | // you can extends this class making child comment and add user 6 | 7 | export abstract class BaseComment extends BaseModel { 8 | @Column({ length: 50 }) 9 | @IsString() 10 | @MaxLength(50) 11 | comment!: string; 12 | 13 | @Column({ default: 0 }) 14 | @IsInt() 15 | reportCount!: number; 16 | } 17 | -------------------------------------------------------------------------------- /src/interceptor/ResponseJsonInterceptor.ts: -------------------------------------------------------------------------------- 1 | import { InterceptorInterface, Action } from 'routing-controllers'; 2 | 3 | interface ResultObject { 4 | result: boolean; 5 | content: object; 6 | } 7 | 8 | export class ResponseJosnInterceptor implements InterceptorInterface { 9 | intercept(_: Action, content: object) { 10 | const resultObject: Partial = {}; 11 | resultObject.result = true; 12 | resultObject.content = content; 13 | return resultObject; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/model/BaseBoard.ts: -------------------------------------------------------------------------------- 1 | import { Column } from 'typeorm'; 2 | import { IsString, IsInt } from 'class-validator'; 3 | import { BaseModel } from './BaseModel'; 4 | 5 | // you can extends this class making child board and add user 6 | 7 | export abstract class BaseBoard extends BaseModel { 8 | @Column({ length: 50 }) 9 | @IsString() 10 | title!: string; 11 | 12 | @IsString() 13 | @Column({ type: 'text' }) 14 | content!: string; 15 | 16 | @IsInt() 17 | @Column({ default: 0 }) 18 | reportCount!: number; 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es2018", "es2017", "es2016"], 6 | "outDir": "./dist", 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noImplicitReturns": true, 11 | "types": ["node", "jest"], 12 | "esModuleInterop": true, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "resolveJsonModule": true 16 | }, 17 | "include": ["./src/"], 18 | "exclude": ["./dist/"] 19 | } 20 | -------------------------------------------------------------------------------- /src/provider/KakaoProvider.ts: -------------------------------------------------------------------------------- 1 | import { BaseProvider } from './BaseProvider'; 2 | import { Service } from 'typedi'; 3 | 4 | @Service() 5 | export class KaKaoProvider extends BaseProvider { 6 | constructor() { 7 | super(); 8 | } 9 | 10 | async getClient_id(accessToken: string) { 11 | this.setInstance('https://kapi.kakao.com', { 12 | Authorization: `Bearer ${accessToken}`, 13 | }); 14 | const response = await this.getInstance()?.get( 15 | '/v1/user/access_token_info', 16 | ); 17 | 18 | return response?.data.id; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { runServer } from './server'; 2 | import { connectDatabase } from './database'; 3 | 4 | const PORT = Number(process.env.PORT) || 3000; //default port 5 | const HOST = process.env.HOST || 'localhost'; 6 | 7 | async function startApplication() { 8 | try { 9 | await connectDatabase(); 10 | console.log('database is connected successfully'); 11 | await runServer(HOST, PORT); 12 | console.log(`server is running on ${PORT}`); 13 | } catch (err) { 14 | console.error(err); 15 | throw err; 16 | } 17 | } 18 | 19 | startApplication(); 20 | -------------------------------------------------------------------------------- /src/model/ExampleBoards.ts: -------------------------------------------------------------------------------- 1 | import { Entity, OneToMany, ManyToOne } from 'typeorm'; 2 | import { BaseBoard } from './BaseBoard'; 3 | import { ExampleBoardComment } from './ExampleBoardComments'; 4 | import { User } from './Users'; 5 | import { IsObject } from 'class-validator'; 6 | 7 | @Entity() 8 | export class ExampleBoard extends BaseBoard { 9 | @IsObject() 10 | @ManyToOne( 11 | (_) => User, 12 | (user) => user.id, 13 | { nullable: false }, 14 | ) 15 | user!: User; 16 | 17 | @IsObject() 18 | @OneToMany( 19 | (_) => ExampleBoardComment, 20 | (comment) => comment.exampleBoard, 21 | ) 22 | comments!: ExampleBoardComment[]; 23 | } 24 | -------------------------------------------------------------------------------- /src/model/ExampleBoardDepthComments.ts: -------------------------------------------------------------------------------- 1 | import { Entity, ManyToOne } from 'typeorm'; 2 | import { BaseComment } from './BaseComment'; 3 | import { ExampleBoardComment } from './ExampleBoardComments'; 4 | import { User } from './Users'; 5 | import { IsObject } from 'class-validator'; 6 | 7 | @Entity() 8 | export class ExampleBoardDepthComment extends BaseComment { 9 | @IsObject() 10 | @ManyToOne( 11 | (_) => ExampleBoardComment, 12 | (comment) => comment.id, 13 | { nullable: false }, 14 | ) 15 | ref!: ExampleBoardComment; 16 | 17 | @IsObject() 18 | @ManyToOne( 19 | (_) => User, 20 | (user) => user.id, 21 | { nullable: false }, 22 | ) 23 | user!: User; 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'NewFeature' 6 | assignees: 'Q00' 7 | --- 8 | 9 | ## Purpose 10 | 11 | - Write the purpose this task. 12 | 13 | ## Why 14 | 15 | - Write the resonable backgrounds about this task. 16 | 17 | ## TODO List 18 | 19 | - Write a next TODO list you think. if you think that task is done, let it empty. 20 | 21 | ## Describe alternatives you've considered 22 | 23 | - A clear and concise description of any alternative solutions or features you've considered. 24 | 25 | ## Additional context 26 | 27 | - Add any other context or screenshots about the feature request here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement request 3 | about: Suggest an enhancement idea for this project 4 | title: '' 5 | labels: 'Enhancement' 6 | assignees: 'Q00' 7 | --- 8 | 9 | ## Purpose 10 | 11 | - Write the purpose this task. 12 | 13 | ## Why 14 | 15 | - Write the resonable backgrounds about this task. 16 | 17 | ## TODO List 18 | 19 | - Write a next TODO list you think. if you think that task is done, let it empty. 20 | 21 | ## Describe alternatives you've considered 22 | 23 | - A clear and concise description of any alternative solutions or features you've considered. 24 | 25 | ## Additional context 26 | 27 | - Add any other context or screenshots about the feature request here. 28 | -------------------------------------------------------------------------------- /src/test/service/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { connectDatabase } from '../../database'; 2 | import { QueryRunner } from 'typeorm'; 3 | // if you want to use typedi, put @Service decorator to class 4 | // import { Container } from 'typedi'; 5 | let queryRunner: QueryRunner | null = null; 6 | 7 | beforeAll(async () => { 8 | const conn = await connectDatabase(); 9 | queryRunner = conn.createQueryRunner(); 10 | await queryRunner.startTransaction(); 11 | }); 12 | 13 | describe('example service', () => { 14 | it('New Example', async () => { 15 | // Container.get("SomethingClass") 16 | expect(true).toEqual(true); 17 | }); 18 | }); 19 | 20 | afterAll(async () => { 21 | if (queryRunner) { 22 | await queryRunner.release(); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /src/database/config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; 3 | import { NamingStrategy } from './NamingStrategy'; 4 | import '../utils/env'; 5 | const typeOrmConfig: PostgresConnectionOptions = { 6 | type: 'postgres', 7 | host: process.env.DB_HOST, 8 | namingStrategy: new NamingStrategy(), 9 | port: Number(process.env.DB_PORT), 10 | username: process.env.DB_USER, 11 | password: process.env.DB_PW, 12 | database: process.env.DATABASE, 13 | synchronize: false, 14 | logging: false, 15 | entities: [`${path.join(__dirname, '..', 'model')}/**.[tj]s`], 16 | migrations: [`${path.join(__dirname, '..', 'model')}/migration/**.[tj]s`], 17 | }; 18 | 19 | export { typeOrmConfig }; 20 | -------------------------------------------------------------------------------- /src/service/APILogService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from './BaseService'; 2 | import { APILog, User } from '../model'; 3 | import { Service } from 'typedi'; 4 | import { Method } from '../model/Enum'; 5 | 6 | export interface IapiLog { 7 | user: User; 8 | url: string; 9 | method: Method; 10 | log?: string; 11 | responseTime: number; 12 | } 13 | 14 | @Service() 15 | export class ApiLogService extends BaseService { 16 | constructor() { 17 | super(APILog); 18 | } 19 | 20 | async save(apiLog: IapiLog): Promise { 21 | return this.genericRepository.save({ 22 | user: apiLog.user, 23 | log: apiLog?.log, 24 | responseTime: apiLog.responseTime, 25 | method: apiLog.method, 26 | url: apiLog.url, 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/model/APIlogs.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, ManyToOne } from 'typeorm'; 2 | import { BaseModel } from './BaseModel'; 3 | import { User } from './Users'; 4 | import { IsString, IsObject, IsInt, IsEnum, IsUrl } from 'class-validator'; 5 | import { Method } from './Enum'; 6 | 7 | @Entity() 8 | export class APILog extends BaseModel { 9 | @IsString() 10 | @Column({ type: 'text', nullable: true }) 11 | log?: string; 12 | 13 | @IsEnum(Method) 14 | @Column({ type: 'enum', enum: Method }) 15 | method!: Method; 16 | 17 | @IsUrl() 18 | @Column({ type: 'text' }) 19 | url!: string; 20 | 21 | @IsObject() 22 | @ManyToOne( 23 | (_) => User, 24 | (user) => user.id, 25 | ) 26 | user!: User; 27 | 28 | @IsInt() 29 | @Column() 30 | responseTime!: number; 31 | } 32 | -------------------------------------------------------------------------------- /src/model/UserAccounts.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, OneToOne, Unique, JoinColumn } from 'typeorm'; 2 | import { IsEnum, IsString, IsObject } from 'class-validator'; 3 | import { User } from './Users'; 4 | import { BaseModel } from './BaseModel'; 5 | import { Provider } from './Enum'; 6 | 7 | @Entity() 8 | @Unique(['clientId', 'user']) 9 | export class UserAccount extends BaseModel { 10 | @IsEnum(Provider) 11 | @Column({ type: 'enum', enum: Provider }) 12 | provider!: Provider; 13 | 14 | @IsString() 15 | @Column({ length: 50 }) 16 | clientId!: string; 17 | 18 | @IsObject() 19 | @OneToOne( 20 | // eslint-disable-next-line no-unused-vars 21 | (_) => User, 22 | (user) => user.userAccount, 23 | { nullable: true }, 24 | ) 25 | @JoinColumn() 26 | user?: User; 27 | } 28 | -------------------------------------------------------------------------------- /src/view/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ReDoc 5 | 6 | 7 | 8 | 12 | 13 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'Bug' 6 | assignees: 'Q00' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /src/model/BaseModel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateDateColumn, 3 | Generated, 4 | PrimaryColumn, 5 | UpdateDateColumn, 6 | ValueTransformer, 7 | Column, 8 | } from 'typeorm'; 9 | import { IsInt, IsDate } from 'class-validator'; 10 | 11 | const bigIntTransformer: ValueTransformer = { 12 | to: (entitiyValue: bigint) => entitiyValue, 13 | from: (databaseValue: string) => Number(databaseValue), 14 | }; 15 | 16 | export abstract class BaseModel { 17 | @IsInt() 18 | @Generated('increment') 19 | @PrimaryColumn({ type: 'bigint', transformer: [bigIntTransformer] }) 20 | id!: number; 21 | 22 | @IsDate() 23 | @CreateDateColumn() 24 | createdAt!: Date; 25 | 26 | @IsDate() 27 | @UpdateDateColumn() 28 | updatedAt!: Date; 29 | 30 | @IsDate() 31 | @Column({ nullable: true, type: 'date', default: null }) 32 | deletedAt?: Date | null; 33 | } 34 | -------------------------------------------------------------------------------- /src/dto/UserDTO.ts: -------------------------------------------------------------------------------- 1 | import { IuserDTO } from '../service/UserService'; 2 | import { IsBoolean, IsString, IsDate, IsEnum } from 'class-validator'; 3 | import { IuserAccountDTO } from '../service/UserAccountService'; 4 | import { Provider } from '../model/Enum'; 5 | 6 | export class IuserDTOClass implements IuserDTO { 7 | @IsString() 8 | nickname!: string; 9 | @IsString() 10 | name!: string; 11 | @IsDate() 12 | birthday!: Date; 13 | @IsString() 14 | profile!: string; 15 | @IsString() 16 | phone!: string; 17 | @IsString() 18 | email!: string; 19 | } 20 | 21 | export class IuserAccountDTOClass implements IuserAccountDTO { 22 | @IsEnum(Provider) 23 | provider!: Provider; 24 | @IsString() 25 | clientId!: string; 26 | } 27 | 28 | export class LoginResponse { 29 | @IsBoolean() 30 | result!: boolean; 31 | @IsString() 32 | jwt!: string; 33 | } 34 | -------------------------------------------------------------------------------- /src/model/ExampleBoardComments.ts: -------------------------------------------------------------------------------- 1 | import { Entity, ManyToOne, OneToMany } from 'typeorm'; 2 | import { BaseComment } from './BaseComment'; 3 | import { ExampleBoard } from './ExampleBoards'; 4 | import { User } from './Users'; 5 | import { ExampleBoardDepthComment } from './ExampleBoardDepthComments'; 6 | import { IsObject } from 'class-validator'; 7 | 8 | @Entity() 9 | export class ExampleBoardComment extends BaseComment { 10 | @IsObject() 11 | @ManyToOne( 12 | (_) => ExampleBoard, 13 | (board) => board.id, 14 | { nullable: false }, 15 | ) 16 | exampleBoard!: ExampleBoard; 17 | 18 | @OneToMany( 19 | (_) => ExampleBoardDepthComment, 20 | (comment) => comment.ref, 21 | ) 22 | depthComments!: ExampleBoardDepthComment[]; 23 | 24 | @ManyToOne( 25 | (_) => User, 26 | (user) => user.id, 27 | { nullable: false }, 28 | ) 29 | @IsObject() 30 | user!: User; 31 | } 32 | -------------------------------------------------------------------------------- /src/service/ExampleBoardCommentService.ts: -------------------------------------------------------------------------------- 1 | import { Service, Container } from 'typedi'; 2 | import { BaseCommentService, IcommentDTO } from './BaseCommentService'; 3 | import { ExampleBoard, ExampleBoardComment } from '../model'; 4 | import { ExampleBoardService } from './ExampleBoardService'; 5 | 6 | @Service() 7 | export class ExampleBoardCommentService extends BaseCommentService< 8 | ExampleBoardComment 9 | > { 10 | constructor() { 11 | super(ExampleBoardComment); 12 | } 13 | 14 | async save(comment: IcommentDTO): Promise { 15 | const normalBoardService = Container.get(ExampleBoardService); 16 | const normalBoard = (await normalBoardService.getById( 17 | comment.boardId, 18 | )) as ExampleBoard; 19 | return this.genericRepository.save({ 20 | comment: comment.comment, 21 | normalBoard, 22 | user: comment.user, 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/provider/BaseProvider.ts: -------------------------------------------------------------------------------- 1 | import { apiClient } from '../utils/apiClient'; 2 | import { AxiosInstance } from 'axios'; 3 | import { Authentication } from '../utils/Authenticate'; 4 | 5 | export abstract class BaseProvider { 6 | protected accessToken: string; 7 | protected instance: AxiosInstance | null; 8 | constructor() { 9 | this.accessToken = ''; 10 | this.instance = null; 11 | } 12 | 13 | setToken(accessToken: string) { 14 | this.accessToken = accessToken; 15 | } 16 | 17 | setInstance(url: string, headers: object) { 18 | this.instance = apiClient(url, headers); 19 | this.instance.interceptors.response.use( 20 | (response) => response, 21 | (err) => Promise.reject(err), 22 | ); 23 | } 24 | 25 | getInstance() { 26 | return this.instance; 27 | } 28 | 29 | async generateToken(userId: number) { 30 | return `Bearer ${Authentication.generateToken(userId)}`; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/middleware/CheckJWT.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExpressMiddlewareInterface, 3 | Middleware, 4 | Req, 5 | Res, 6 | } from 'routing-controllers'; 7 | import express from 'express'; 8 | import { Authentication } from '../utils/Authenticate'; 9 | 10 | @Middleware({ type: 'before' }) 11 | export class CheckJwt implements ExpressMiddlewareInterface { 12 | // interface implementation is optional 13 | constructor() {} 14 | 15 | use( 16 | @Req() request: express.Request, 17 | @Res() response: express.Response, 18 | // tslint:disable-next-line: no-any 19 | next: (err?: any) => any, 20 | ) { 21 | const jwt: string = request.headers.authorization as string; 22 | if (jwt !== undefined) { 23 | const bearerToken = jwt.replace(/Bearer\s/, ''); 24 | const token = Authentication.refreshToken(bearerToken); 25 | response.setHeader('authorization', `Bearer ${token}`); 26 | } 27 | next(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/service/ExampleBoardService.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { BaseBoardService } from './BaseBoardService'; 3 | import { IboardDTO } from './BaseBoardService'; 4 | import { ExampleBoard, User } from '../model'; 5 | 6 | export interface IexampleBoardDTO extends IboardDTO { 7 | user: User; 8 | } 9 | 10 | @Service() 11 | export class ExampleBoardService extends BaseBoardService { 12 | constructor() { 13 | super(ExampleBoard); 14 | } 15 | 16 | async save( 17 | board: Pick, 18 | ): Promise { 19 | return this.genericRepository.save({ 20 | title: board.title, 21 | content: board.content, 22 | user: board.user!, 23 | }); 24 | } 25 | 26 | async getByUserId(userId: number): Promise { 27 | return super.getByWhere({ user: userId }, [ 28 | /*"normalBoardComments"*/ 'user', 29 | ]) as Promise; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | database: 4 | image: bitnami/postgresql:11-debian-9 5 | restart: on-failure 6 | ports: 7 | - '0.0.0.0:5432:5432' 8 | environment: 9 | - POSTGRESQL_PASSWORD=testpasswd 10 | - POSTGRESQL_USERNAME=testuser 11 | - POSTGRESQL_DATABASE=testdb 12 | - POSTGRESQL_REPLICATION_MODE=master 13 | - POSTGRESQL_REPLICATION_USER=repuser 14 | - POSTGRESQL_REPLICATION_PASSWORD=reppassword 15 | networks: 16 | pple2net: 17 | ipv4_address: 172.16.24.2 18 | server: 19 | build: 20 | context: . 21 | dockerfile: ./dev.dockerfile 22 | ports: 23 | - '3000:3000' 24 | volumes: 25 | - '.:/boilerplate/web' 26 | depends_on: 27 | - database 28 | command: 'yarn dev' 29 | networks: 30 | pple2net: 31 | ipv4_address: 172.16.24.3 32 | networks: 33 | pple2net: 34 | driver: bridge 35 | ipam: 36 | config: 37 | - subnet: 172.16.24.0/24 38 | -------------------------------------------------------------------------------- /src/database/NamingStrategy.ts: -------------------------------------------------------------------------------- 1 | import { plural } from 'pluralize'; 2 | import { DefaultNamingStrategy } from 'typeorm'; 3 | import { snakeCase } from 'typeorm/util/StringUtils'; 4 | 5 | export class NamingStrategy extends DefaultNamingStrategy { 6 | tableName(targetName: string, userSpecifiedName: string | undefined): string { 7 | return plural(snakeCase(userSpecifiedName || targetName)); 8 | } 9 | 10 | relationName(propertyName: string): string { 11 | return snakeCase(propertyName); 12 | } 13 | 14 | columnName(propertyName: string, customName: string) { 15 | return snakeCase(customName || propertyName); 16 | } 17 | 18 | joinColumnName(relationName: string, referencedColumnName: string) { 19 | return snakeCase(`${relationName}_${referencedColumnName}`); 20 | } 21 | 22 | joinTableColumnName( 23 | tableName: string, 24 | propertyName: string, 25 | columnName: string, 26 | ) { 27 | return snakeCase(`${tableName}_${columnName || propertyName}`); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/service/ExampleBoardDepthCommentService.ts: -------------------------------------------------------------------------------- 1 | import { Service, Container } from 'typedi'; 2 | import { BaseCommentService, IdepthCommentDTO } from './BaseCommentService'; 3 | import { ExampleBoardDepthComment } from '../model'; 4 | import { ExampleBoardCommentService } from './ExampleBoardCommentService'; 5 | 6 | @Service() 7 | export class ExampleBoardDepthCommentService extends BaseCommentService< 8 | ExampleBoardDepthComment 9 | > { 10 | constructor() { 11 | super(ExampleBoardDepthComment); 12 | } 13 | 14 | async createOrUpdate( 15 | depthComment: IdepthCommentDTO, 16 | ): Promise { 17 | const normalBoardCommentService = Container.get(ExampleBoardCommentService); 18 | const parentComment = await normalBoardCommentService.getById( 19 | depthComment.commentId, 20 | ); 21 | return this.genericRepository.save({ 22 | comment: depthComment.comment, 23 | ref: parentComment, 24 | user: depthComment.user, 25 | }) as Promise; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Q00 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/middleware/WriteLog.ts: -------------------------------------------------------------------------------- 1 | import { ExpressMiddlewareInterface, Middleware } from 'routing-controllers'; 2 | 3 | import { NextFunction, Request, Response } from 'express'; 4 | import { ApiLogService } from '../service'; 5 | import Container from 'typedi'; 6 | import { Method } from '../model/Enum'; 7 | 8 | @Middleware({ type: 'before' }) 9 | export class StartTimerMiddleware implements ExpressMiddlewareInterface { 10 | use(request: Request, _: Response, next: NextFunction): void { 11 | request.query.startTime = String(new Date().getTime()); 12 | next(); 13 | } 14 | } 15 | 16 | @Middleware({ type: 'after' }) 17 | export class EndTimerMiddleware implements ExpressMiddlewareInterface { 18 | async use(request: Request, _: Response, next: NextFunction): Promise { 19 | const time = new Date().getTime(); 20 | const responseTime = time - Number(request.query.startTime); 21 | const user = request.query.user; 22 | const url = request.url; 23 | const method = request.method as Method; 24 | 25 | const apiLogService = Container.get(ApiLogService); 26 | await apiLogService.save({ user, url, method, responseTime }); 27 | next(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/database/cli.ts: -------------------------------------------------------------------------------- 1 | import { Signale } from 'signale'; 2 | import { Connection, createConnection, getConnection } from 'typeorm'; 3 | import yargs from 'yargs'; 4 | import { typeOrmConfig } from './config'; 5 | 6 | const signale = new Signale(); 7 | 8 | async function getDatabase(): Promise { 9 | try { 10 | return getConnection(); 11 | } catch (e) { 12 | return createConnection(typeOrmConfig); 13 | } 14 | } 15 | 16 | async function schemaSync(db: Connection) { 17 | await db.synchronize(); 18 | } 19 | 20 | async function runMigration(db: Connection) { 21 | await db.runMigrations(); 22 | } 23 | 24 | async function main() { 25 | const { argv } = yargs; 26 | if (!argv._.length) { 27 | signale.error('not enough length'); 28 | return; 29 | } 30 | const order = argv._[0]; 31 | signale.info(`order is ${order}`); 32 | const db = await getDatabase(); 33 | switch (order) { 34 | case 'sync': 35 | await schemaSync(db); 36 | break; 37 | case 'migration': 38 | await runMigration(db); 39 | break; 40 | default: 41 | signale.error('does not match in orders'); 42 | break; 43 | } 44 | signale.info('bye'); 45 | await db.close(); 46 | process.exit(); 47 | } 48 | 49 | main(); 50 | -------------------------------------------------------------------------------- /src/utils/swagger.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { getMetadataArgsStorage } from 'routing-controllers'; 3 | import { getFromContainer, MetadataStorage } from 'class-validator'; 4 | import { routingControllersToSpec } from 'routing-controllers-openapi'; 5 | import { validationMetadatasToSchemas } from 'class-validator-jsonschema'; 6 | import { routingControllerOptions } from './routingConfig'; 7 | 8 | // Parse class-validator classes into JSON Schema: 9 | // tslint:disable-next-line: no-any 10 | const metadatas = (getFromContainer(MetadataStorage) as any) 11 | .validationMetadatas; 12 | const schemas = validationMetadatasToSchemas(metadatas, { 13 | refPointerPrefix: '#/components/schemas', 14 | }); 15 | 16 | // Parse routing-controllers classes into OPENAPI spec: 17 | const storage = getMetadataArgsStorage(); 18 | const spec = routingControllersToSpec(storage, routingControllerOptions, { 19 | components: { 20 | schemas, 21 | securitySchemes: { 22 | bearerAuth: { 23 | scheme: 'bearer', 24 | type: 'http', 25 | }, 26 | }, 27 | }, 28 | info: { 29 | description: 'put your server description', 30 | title: 'API SERVER BOILERPLATE', 31 | version: '2.0.0', 32 | }, 33 | }); 34 | 35 | export { spec }; 36 | -------------------------------------------------------------------------------- /src/service/BaseCommentService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from './BaseService'; 2 | import { BaseComment, User } from '../model'; 3 | import { ObjectType } from './BaseService'; 4 | export interface IcommentDTO { 5 | comment: string; 6 | boardId: number; 7 | user: User; 8 | } 9 | 10 | export interface IdepthCommentDTO { 11 | comment: string; 12 | commentId: number; 13 | user: User; 14 | } 15 | 16 | export abstract class BaseCommentService< 17 | T extends BaseComment 18 | > extends BaseService { 19 | constructor(repo: ObjectType) { 20 | super(repo); 21 | } 22 | async updateReportCount(id: number): Promise { 23 | const comment = await (this.getById(id) as Promise>); 24 | const newComment: Partial = { 25 | reportCount: Number(comment?.reportCount) + 1, 26 | }; 27 | return this.genericRepository.save({ ...comment, ...newComment } as object); 28 | } 29 | 30 | async update(id: number, comment: string): Promise { 31 | const oldComment = await (this.getById(id) as Promise); 32 | const newComment: Partial = { 33 | comment, 34 | }; 35 | 36 | return this.genericRepository.save({ 37 | ...oldComment, 38 | ...newComment, 39 | } as object); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest All", 11 | "program": "${workspaceFolder}/node_modules/.bin/jest", 12 | "args": ["--runInBand"], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "disableOptimisticBPs": true, 16 | "windows": { 17 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 18 | }, 19 | "env": { 20 | "NODE_ENV": "development" 21 | } 22 | }, 23 | { 24 | "type": "node", 25 | "request": "launch", 26 | "name": "Jest Current File", 27 | "program": "${workspaceFolder}/node_modules/.bin/jest", 28 | "args": [ 29 | "${fileDirname}/${fileBasenameNoExtension}", 30 | "--config", 31 | "jest.config.js" 32 | ], 33 | "console": "integratedTerminal", 34 | "internalConsoleOptions": "neverOpen", 35 | "disableOptimisticBPs": true, 36 | "windows": { 37 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 38 | }, 39 | "env": { 40 | "NODE_ENV": "development" 41 | } 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/service/UserAccountService.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { UserAccount } from '../model'; 3 | import { BaseService } from './BaseService'; 4 | import { Provider } from '../model/Enum'; 5 | import { InternalServerError } from 'routing-controllers'; 6 | export interface IuserAccountDTO { 7 | provider: Provider; 8 | clientId: string; 9 | } 10 | 11 | @Service() 12 | export class UserAccountService extends BaseService { 13 | constructor() { 14 | super(UserAccount); 15 | } 16 | 17 | async getOrNewAccount(tempUser: IuserAccountDTO): Promise { 18 | const user = await this.genericRepository.findOne({ 19 | where: { provider: tempUser.provider, clientId: tempUser.clientId }, 20 | relations: ['user'], 21 | }); 22 | 23 | if (user) { 24 | return user; 25 | } 26 | return this.genericRepository.save({ 27 | provider: tempUser.provider, 28 | clientId: tempUser.clientId, 29 | }); 30 | } 31 | 32 | getByClientId(clientId: string): Promise { 33 | return this.genericRepository.findOne({ 34 | relations: ['user'], 35 | where: { clientId }, 36 | }) as Promise; 37 | } 38 | 39 | async update( 40 | userAccountId: number, 41 | user: Partial, 42 | ): Promise { 43 | try { 44 | await this.genericRepository.update(userAccountId, { user }); 45 | } catch (err) { 46 | throw new InternalServerError(err); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import express, { Request, NextFunction, Response } from 'express'; 3 | import { Container } from 'typedi'; 4 | import './utils/env'; 5 | import { useContainer, useExpressServer } from 'routing-controllers'; 6 | import { routingControllerOptions } from './utils/routingConfig'; 7 | import swaggerUi from 'swagger-ui-express'; 8 | 9 | useContainer(Container); 10 | const app = express(); 11 | console.log(`Current NODE_ENV is ${process.env.NODE_ENV}`); 12 | 13 | app.use(express.static(__dirname + '/view')); 14 | 15 | useExpressServer(app, routingControllerOptions); 16 | export function runServer(host: string, port: number) { 17 | return new Promise((resolve, reject) => { 18 | // tslint:disable-next-line: no-any 19 | app.listen(port, host, (err: any) => { 20 | if (err) { 21 | reject(err); 22 | } 23 | resolve(); 24 | }); 25 | }); 26 | } 27 | 28 | import { spec } from './utils/swagger'; 29 | 30 | app.use(swaggerUi.serve); 31 | app.get('/swagger', swaggerUi.setup(spec)); 32 | 33 | app.get('/', (_: Request, res: Response) => { 34 | res.sendFile('./view/index.html'); 35 | }); 36 | app.get('/swagger.json', (_: Request, res: Response) => { 37 | res.json(spec); 38 | }); 39 | 40 | app.use((err: string, _req: Request, res: Response, _next: NextFunction) => { 41 | // The error id is attached to `res.sentry` to be returned 42 | // and optionally displayed to the user for support. 43 | res.end(`{ result: false, error: ${err} }`); 44 | }); 45 | 46 | export { app }; 47 | -------------------------------------------------------------------------------- /src/controller/KaKaoAuthController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JsonController, 3 | Get, 4 | QueryParam, 5 | Post, 6 | Body, 7 | } from 'routing-controllers'; 8 | import { KaKaoProvider } from '../provider/KakaoProvider'; 9 | import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; 10 | import { BaseAuthController } from './BaseAuthController'; 11 | import { IuserDTOClass, LoginResponse } from '../dto/UserDTO'; 12 | 13 | // just make other provider class and naming class then you can use another OAuth 14 | @JsonController('/kakao') 15 | export class KaKaoAuthController extends BaseAuthController { 16 | constructor(kakaoProvider: KaKaoProvider) { 17 | super(kakaoProvider); 18 | } 19 | 20 | @Get('/login') 21 | @OpenAPI({ 22 | summary: 'login with access_token', 23 | description: 24 | "return { result: true, jwt: jwt } or { result: false, jwt: '' } ", 25 | }) 26 | @ResponseSchema(LoginResponse) 27 | async kakaoLogin(@QueryParam('access_token') accessToken: string) { 28 | const clientId = await this.provider.getClient_id(accessToken); 29 | return this.login(clientId); 30 | } 31 | 32 | @Post('/register') 33 | @OpenAPI({ 34 | summary: 'register with access_token', 35 | description: 'kakao register', 36 | }) 37 | @ResponseSchema(LoginResponse, { 38 | description: 'register', 39 | isArray: false, 40 | statusCode: '201', 41 | }) 42 | async kakaoRegister( 43 | @Body() 44 | body: IuserDTOClass, 45 | @QueryParam('access_token') accessToken: string, 46 | ) { 47 | const clientId = await this.provider.getClient_id(accessToken); 48 | return this.register(body, clientId); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/model/Users.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Unique, OneToOne, OneToMany } from 'typeorm'; 2 | import { 3 | IsInt, 4 | IsString, 5 | IsDate, 6 | IsUrl, 7 | IsPhoneNumber, 8 | IsEmail, 9 | } from 'class-validator'; 10 | import { BaseModel } from './BaseModel'; 11 | import { UserAccount } from './UserAccounts'; 12 | import { ExampleBoard, ExampleBoardComment, ExampleBoardDepthComment } from '.'; 13 | 14 | // you can add column in user model if you want 15 | 16 | @Entity() 17 | @Unique(['nickname', 'phone', 'email']) 18 | export class User extends BaseModel { 19 | @Column({ length: 45 }) 20 | @IsInt() 21 | nickname!: string; // 닉네임 22 | 23 | @Column({ length: 10 }) 24 | @IsString() 25 | name!: string; 26 | 27 | @Column({ type: 'date' }) 28 | @IsDate() 29 | birthday!: Date; 30 | 31 | @Column({ length: 200 }) 32 | @IsUrl() 33 | profile!: string; 34 | 35 | @Column({ length: 25 }) 36 | @IsPhoneNumber('KR') 37 | phone!: string; 38 | 39 | @Column({ length: 35 }) 40 | @IsEmail() 41 | email!: string; 42 | 43 | @OneToOne( 44 | // eslint-disable-next-line no-unused-vars 45 | (_) => UserAccount, 46 | (userAccount) => userAccount.user, 47 | ) 48 | userAccount!: UserAccount; 49 | 50 | @OneToMany( 51 | (_) => ExampleBoard, 52 | (board) => board.user, 53 | ) 54 | normalBoards!: ExampleBoard[]; 55 | 56 | @OneToMany( 57 | (_) => ExampleBoardComment, 58 | (comment) => comment.user, 59 | ) 60 | normalBoardComments!: ExampleBoardComment[]; 61 | 62 | @OneToMany( 63 | (_) => ExampleBoardDepthComment, 64 | (comment) => comment.user, 65 | ) 66 | normalBoardDepthComments!: ExampleBoardDepthComment[]; 67 | } 68 | -------------------------------------------------------------------------------- /src/service/BaseBoardService.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { BaseService, ObjectType, listForm } from './BaseService'; 3 | import { BaseBoard } from '../model/BaseBoard'; 4 | import { Like, IsNull } from 'typeorm'; 5 | 6 | export interface IboardDTO { 7 | title: string; 8 | content: string; 9 | reportCount: number; 10 | } 11 | 12 | const listForm = Promise; 13 | 14 | @Service() 15 | export abstract class BaseBoardService extends BaseService< 16 | T 17 | > { 18 | constructor(repo: ObjectType) { 19 | super(repo); 20 | } 21 | 22 | async getBoardList(page: number, query?: string): listForm { 23 | if (Number.isNaN(page) || page === undefined) { 24 | page = 1; 25 | } 26 | const size = 10; 27 | const begin = (page - 1) * size; 28 | 29 | let boardList; 30 | if (query) { 31 | boardList = await this.getByWhere( 32 | [ 33 | { 34 | title: Like(`%${query}%`), 35 | deletedAt: IsNull(), 36 | }, 37 | { 38 | content: Like(`%${query}%`), 39 | deletedAt: IsNull(), 40 | }, 41 | ], 42 | ['user'], 43 | begin, 44 | size, 45 | ); 46 | } else { 47 | boardList = await this.list(['user'], begin, size); 48 | } 49 | 50 | return { array: boardList[0], total: boardList[1] }; 51 | } 52 | async updateReportCount(id: number): Promise { 53 | const board = await (this.getById(id) as Promise); 54 | const newBoard: Pick = { 55 | reportCount: board.reportCount + 1, 56 | }; 57 | return this.genericRepository.save({ ...board, ...newBoard } as object); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/controller/BaseCommentController.ts: -------------------------------------------------------------------------------- 1 | import { BaseController } from './BaseController'; 2 | import { BaseCommentService } from '../service/BaseCommentService'; 3 | import { BaseComment, User } from '../model'; 4 | import { NotFoundError, NotAcceptableError } from 'routing-controllers'; 5 | import Container from 'typedi'; 6 | import { ApiLogService } from '../service'; 7 | 8 | export abstract class BaseCommentController< 9 | U extends BaseComment, 10 | T extends BaseCommentService 11 | > extends BaseController { 12 | protected service: T; 13 | constructor(service: T) { 14 | super(); 15 | this.service = service; 16 | } 17 | 18 | protected async update(id: number, comment: string): Promise { 19 | const _comment = this.service.getById(id); 20 | if (!_comment) throw new NotFoundError('this commment is undefined'); 21 | return this.service.update(id, comment); 22 | } 23 | 24 | protected async updateReport( 25 | id: number, 26 | url: string, 27 | user: User, 28 | ): Promise { 29 | const apiLogService = Container.get(ApiLogService); 30 | const log = await apiLogService.getByWhere({ user, log: url }); 31 | if (log.length !== 0) { 32 | throw new NotAcceptableError('Already report comment'); 33 | } 34 | const _comment = this.service.getById(id); 35 | 36 | if (!_comment) throw new NotFoundError('this commment is undefined'); 37 | return this.service.updateReportCount(id); 38 | } 39 | 40 | protected async delete(id: number): Promise { 41 | const _comment = this.service.getById(id); 42 | if (!_comment) throw new NotFoundError('this commment is undefined'); 43 | return this.service.softDelete(id); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/controller/BaseAuthController.ts: -------------------------------------------------------------------------------- 1 | import { BaseController } from './BaseController'; 2 | import { UserAccountService, UserService } from '../service'; 3 | import { Provider } from '../model/Enum'; 4 | import { BaseProvider } from '../provider/BaseProvider'; 5 | import Container from 'typedi'; 6 | import { IuserDTOClass } from '../dto/UserDTO'; 7 | 8 | export class BaseAuthController extends BaseController { 9 | // this can be used in child class (ExampleAuthController) 10 | protected userAccountService: UserAccountService; 11 | protected userService: UserService; 12 | constructor(protected provider: T) { 13 | super(); 14 | this.provider = provider; 15 | this.userAccountService = Container.get(UserAccountService); 16 | this.userService = Container.get(UserService); 17 | } 18 | 19 | async login(clientId: string) { 20 | const userAccount = await this.userAccountService.getOrNewAccount({ 21 | provider: Provider['KAKAO'], 22 | clientId, 23 | }); 24 | if (userAccount.user == null) return { result: false, jwt: '' }; 25 | else { 26 | const jwt = await this.provider.generateToken(userAccount.user.id); 27 | return { result: true, jwt }; 28 | } 29 | } 30 | 31 | async register(userDTO: IuserDTOClass, clientId: string) { 32 | const user = await this.userService.createOrUpdate( 33 | { 34 | nickname: userDTO.nickname, 35 | name: userDTO.name, 36 | birthday: userDTO.birthday, 37 | profile: userDTO.profile, 38 | phone: userDTO.phone, 39 | email: userDTO.email, 40 | }, 41 | clientId, 42 | ); 43 | const jwt = await this.provider.generateToken(user.id); 44 | return { result: true, jwt, user }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.vscode/task_definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "ipcMode": "task", 3 | "containerDefinitions": [ 4 | { 5 | "dnsSearchDomains": [], 6 | "logConfiguration": { 7 | "logDriver": "awslogs", 8 | "secretOptions": [], 9 | "options": { 10 | "awslogs-group": "/ecs/your-group", 11 | "awslogs-region": "ap-northeast-2", 12 | "awslogs-stream-prefix": "ecs" 13 | } 14 | }, 15 | "entryPoint": [], 16 | "portMappings": [ 17 | { 18 | "hostPort": 80, 19 | "protocol": "tcp", 20 | "containerPort": 3000 21 | } 22 | ], 23 | "command": [], 24 | "linuxParameters": [], 25 | "environment": [ 26 | { 27 | "name": "NODE_ENV", 28 | "value": "production" 29 | } 30 | ], 31 | "resourceRequirements": [], 32 | "ulimits": [], 33 | "dnsServers": [], 34 | "mountPoints": [], 35 | "secrets": [], 36 | "dockerSecurityOptions": [], 37 | "memoryReservation": 512, 38 | "volumesFrom": [], 39 | "stopTimeout": 2, 40 | "image": "your ecr repository image url", 41 | "startTimeout": 2, 42 | "dependsOn": [], 43 | "workingDirectory": "/usr/src/app", 44 | "interactive": true, 45 | "healthCheck": { 46 | "command": ["CMD-SHELL", "curl -f http://localhost/ || exit 1"], 47 | "interval": 300, 48 | "timeout": 30, 49 | "retries": 3, 50 | "startPeriod": 0 51 | }, 52 | "essential": true, 53 | "links": [], 54 | "pseudoTerminal": true, 55 | "name": "api-server-boilerplate" 56 | } 57 | ], 58 | "placementConstraints": [], 59 | "family": "your family name", 60 | "pidMode": "task", 61 | "requiresCompatibilities": ["EC2"], 62 | "networkMode": "bridge", 63 | "inferenceAccelerators": [], 64 | "volumes": [] 65 | } 66 | -------------------------------------------------------------------------------- /src/service/UserService.ts: -------------------------------------------------------------------------------- 1 | import { Service, Container } from 'typedi'; 2 | import { User } from '../model'; 3 | import { BaseService } from './BaseService'; 4 | import { UserAccountService } from './UserAccountService'; 5 | 6 | export interface IuserDTO { 7 | nickname: string; 8 | name: string; 9 | birthday: Date; 10 | profile: string; 11 | phone: string; 12 | email: string; 13 | deletedAt?: Date; 14 | } 15 | 16 | @Service() 17 | export class UserService extends BaseService { 18 | constructor(private userAccountService: UserAccountService) { 19 | super(User); 20 | this.userAccountService = Container.get(UserAccountService); 21 | } 22 | 23 | async getById(userId: number): Promise { 24 | const relations = ['exampleBoards', 'userAccount']; 25 | return super.getById(userId, relations); 26 | } 27 | 28 | async createOrUpdate( 29 | user: Partial, 30 | clientId: string, 31 | ): Promise { 32 | const payload: Partial = {}; 33 | if (user.nickname) { 34 | payload.nickname = user.nickname; 35 | } 36 | if (user.name) { 37 | payload.name = user.name; 38 | } 39 | if (user.birthday) { 40 | payload.birthday = user.birthday; 41 | } 42 | if (user.profile) { 43 | payload.profile = user.profile; 44 | } 45 | if (user.phone) { 46 | payload.phone = user.phone; 47 | } 48 | if (user.email) { 49 | payload.email = user.email; 50 | } 51 | const tempAccount = await this.userAccountService.getByClientId(clientId); 52 | if (tempAccount.user) { 53 | return this.genericRepository.save({ 54 | ...tempAccount.user, 55 | ...payload, 56 | }); 57 | } else { 58 | const newUser = await this.genericRepository.save(payload); 59 | await this.userAccountService.update(tempAccount.id, newUser); 60 | return newUser; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/controller/UserController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JsonController, 3 | Get, 4 | CurrentUser, 5 | Body, 6 | Delete, 7 | Put, 8 | UseInterceptor, 9 | HttpCode, 10 | } from 'routing-controllers'; 11 | import { BaseController } from './BaseController'; 12 | import { User } from '../model'; 13 | import { UserService } from '../service/UserService'; 14 | import { ResponseJosnInterceptor } from '../interceptor/ResponseJsonInterceptor'; 15 | import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; 16 | import { IuserDTOClass } from '../dto/UserDTO'; 17 | 18 | @JsonController('/user') 19 | @UseInterceptor(ResponseJosnInterceptor) 20 | export class UserController extends BaseController { 21 | constructor(private userService: UserService) { 22 | super(); 23 | } 24 | @Get() 25 | @HttpCode(200) 26 | @OpenAPI({ 27 | summary: 'get user', 28 | description: 'get an user with jwt token', 29 | security: [{ bearerAuth: [] }], // Applied to each method 30 | }) 31 | @ResponseSchema(User) 32 | async getUser(@CurrentUser({ required: true }) user: User) { 33 | return user; 34 | } 35 | 36 | @Put() 37 | @HttpCode(201) 38 | @OpenAPI({ 39 | summary: 'edit user', 40 | description: 'update user', 41 | security: [{ bearerAuth: [] }], // Applied to each metho 42 | }) 43 | @ResponseSchema(User) 44 | async editUser( 45 | @CurrentUser({ required: true }) user: User, 46 | @Body() body: Partial, 47 | ) { 48 | const editUser = await this.userService.createOrUpdate( 49 | body, 50 | user.userAccount.clientId, 51 | ); 52 | return editUser; 53 | } 54 | 55 | @Delete() 56 | @HttpCode(204) 57 | @OpenAPI({ 58 | summary: 'delete user', 59 | description: 'sofrt delete user', 60 | security: [{ bearerAuth: [] }], // Applied to each method 61 | }) 62 | @ResponseSchema(User) 63 | async deleteUser(@CurrentUser({ required: true }) user: User) { 64 | // soft delete 65 | const editUser = await this.userService.createOrUpdate( 66 | { deletedAt: new Date() }, 67 | user.userAccount.clientId, 68 | ); 69 | return editUser; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/utils/Authenticate.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import jsonwebtoken from 'jsonwebtoken'; 3 | import { Action } from 'routing-controllers'; 4 | import Container from 'typedi'; 5 | import { UserService } from '../service'; 6 | // import { UserService } from "../services"; 7 | export interface Itoken { 8 | userId: number; 9 | iat: number; 10 | exp: number; 11 | } 12 | 13 | export class Authentication { 14 | static isToken(token: string) { 15 | return /Bearer\s[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/.test( 16 | token, 17 | ); 18 | } 19 | 20 | static generateToken(userId: number): string { 21 | return jsonwebtoken.sign({ userId }, process.env.CRYPTO_SECRETKEY || '', { 22 | algorithm: 'HS512', 23 | expiresIn: '1d', 24 | }); 25 | } 26 | 27 | static verifyToken(token: string): boolean { 28 | const data: Itoken = jsonwebtoken.verify( 29 | token, 30 | process.env.CRYPTO_SECRETKEY || '', 31 | { algorithms: ['HS512'] }, 32 | ) as Itoken; 33 | 34 | if (data.iat * 1000 - new Date().getTime() > 0) return false; 35 | if (data.exp * 1000 - new Date().getTime() <= 0) return false; 36 | return true; 37 | } 38 | 39 | static refreshToken(token: string): string { 40 | const data: Itoken = jsonwebtoken.verify( 41 | token, 42 | process.env.CRYPTO_SECRETKEY || '', 43 | { algorithms: ['HS512'] }, 44 | ) as Itoken; 45 | if (data.exp - new Date().getTime() / 1000 < 60 * 60) { 46 | return Authentication.generateToken(data.userId); 47 | } 48 | return token; 49 | } 50 | 51 | static getUserIdByToken(token: string): Pick { 52 | return jsonwebtoken.verify(token, process.env.CRYPTO_SECRETKEY || '', { 53 | algorithms: ['HS512'], 54 | }) as Pick; 55 | } 56 | 57 | static async currentUserChecker(action: Action) { 58 | const bearerToken = action.request.headers.authorization; 59 | if (!Authentication.isToken(bearerToken)) { 60 | return false; 61 | } 62 | const token = bearerToken.replace(/Bearer\s/, ''); 63 | if (!Authentication.verifyToken(token)) { 64 | return false; 65 | } 66 | const userService = Container.get(UserService); 67 | const user = await userService.getById( 68 | Authentication.getUserIdByToken(token).userId, 69 | ); 70 | 71 | action.request.query.user = user; 72 | return user; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api_server_boilerplate", 3 | "version": "1.0.0", 4 | "description": "api server boilerplate", 5 | "main": "index.js", 6 | "repository": "git@github.com:Q00/api_server_boilerplate.git", 7 | "author": "Q00 ", 8 | "license": "MIT", 9 | "scripts": { 10 | "db:dev": "cp .env.development .env && NODE_ENV=development ts-node ./src/database/cli", 11 | "db:sync": "node ./dist/database/cli sync", 12 | "build": "tsc", 13 | "test": "jest", 14 | "dev": "cp .env.development .env && NODE_ENV=development nodemon", 15 | "start": "cp .env.production .env&& NODE_ENV=production node ./dist", 16 | "check": "gts check", 17 | "clean": "gts clean", 18 | "compile": "tsc -p .", 19 | "fix": "gts fix", 20 | "prepare": "yarn run compile", 21 | "pretest": "yarn run compile", 22 | "posttest": "yarn run check" 23 | }, 24 | "devDependencies": { 25 | "@types/cors": "^2.8.6", 26 | "@types/dotenv": "^8.2.0", 27 | "@types/express": "^4.17.2", 28 | "@types/jest": "^25.1.1", 29 | "@types/jsonwebtoken": "^8.3.7", 30 | "@types/node": "^13.7.0", 31 | "@types/pluralize": "^0.0.29", 32 | "@types/signale": "^1.2.1", 33 | "@types/supertest": "^2.0.8", 34 | "@types/swagger-ui-express": "^4.1.1", 35 | "@types/yargs": "^15.0.3", 36 | "@typescript-eslint/eslint-plugin": "^2.19.1-alpha.1", 37 | "@typescript-eslint/parser": "^2.19.0", 38 | "babel-eslint": "^10.0.3", 39 | "eslint": "^6.8.0", 40 | "eslint-config-airbnb-base": "^14.0.0", 41 | "eslint-config-prettier": "^6.10.0", 42 | "eslint-plugin-import": "^2.20.0", 43 | "eslint-plugin-prettier": "^3.1.2", 44 | "gts": "^1.1.2", 45 | "jest": "^25.1.0", 46 | "nodemon": "^2.0.2", 47 | "prettier": "^1.19.1", 48 | "supertest": "^4.0.2", 49 | "ts-jest": "^25.1.0", 50 | "typescript": "^3.7.5" 51 | }, 52 | "dependencies": { 53 | "axios": "^0.19.2", 54 | "class-transformer": "^0.2.3", 55 | "class-validator": "^0.11.0", 56 | "class-validator-jsonschema": "^1.3.1", 57 | "cors": "^2.8.5", 58 | "dotenv": "^8.2.0", 59 | "express": "^4.17.1", 60 | "jsonwebtoken": "^8.5.1", 61 | "pg": "^7.18.1", 62 | "pluralize": "^8.0.0", 63 | "redoc": "^2.0.0-rc.20", 64 | "reflect-metadata": "^0.1.13", 65 | "routing-controllers": "^0.8.0", 66 | "routing-controllers-openapi": "^1.8.0", 67 | "signale": "^1.4.0", 68 | "swagger-ui-express": "^4.1.3", 69 | "typedi": "^0.8.0", 70 | "typeorm": "^0.2.22", 71 | "yargs": "^15.1.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/service/BaseService.ts: -------------------------------------------------------------------------------- 1 | import { getConnection, Repository, IsNull, DeleteResult } from 'typeorm'; 2 | import { BaseModel } from '../model/BaseModel'; 3 | export type ObjectType = { new (): T } | Function; 4 | export type listForm = Promise<[T[], number]> | Promise; 5 | const listForm = Promise; 6 | 7 | // you can extends this BaseService to use common method 8 | 9 | export abstract class BaseService { 10 | protected genericRepository: Repository; 11 | private repo: ObjectType; 12 | constructor(repo: ObjectType) { 13 | this.genericRepository = getConnection().getRepository(repo); 14 | this.repo = repo; 15 | } 16 | 17 | async list(relations?: string[], skip?: number, take?: number): listForm { 18 | if ((take || take === 0) && (skip || skip === 0)) { 19 | const list = await this.genericRepository.findAndCount({ 20 | order: { createdAt: 'DESC' }, 21 | where: { deletedAt: IsNull() }, 22 | relations, 23 | take, 24 | skip, 25 | }); 26 | return list; 27 | } else { 28 | const blist = await (this.genericRepository.find({ 29 | relations, 30 | }) as Promise); 31 | 32 | const array = [blist, blist.length]; 33 | return array; 34 | } 35 | } 36 | 37 | async getById(id: number, relations?: string[]): Promise { 38 | return this.genericRepository.findOne({ 39 | where: { id }, 40 | relations, 41 | }) as Promise; 42 | } 43 | 44 | async getByWhere( 45 | where: [] | {}, 46 | relations?: string[], 47 | skip?: number, 48 | take?: number, 49 | ): listForm { 50 | if ((take || take === 0) && (skip || skip === 0)) { 51 | const list = await this.genericRepository.findAndCount({ 52 | where, 53 | order: { createdAt: 'DESC' }, 54 | relations, 55 | take, 56 | skip, 57 | }); 58 | 59 | return list; 60 | } else { 61 | const get = await this.genericRepository.find({ 62 | where, 63 | relations, 64 | }); 65 | return get; 66 | } 67 | } 68 | 69 | async hardDelete(id: number): Promise { 70 | return getConnection() 71 | .createQueryBuilder() 72 | .delete() 73 | .from(this.repo) 74 | .where('id = :id', { id }) 75 | .execute(); 76 | } 77 | 78 | async softDelete(id: number): Promise { 79 | const oldOne = await (this.getById(id) as Promise); 80 | const newOne: Partial = {}; 81 | newOne.deletedAt = new Date(); 82 | return this.genericRepository.save({ ...oldOne, ...newOne } as object); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/controller/ExampleBoardController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JsonController, 3 | HttpCode, 4 | Get, 5 | QueryParam, 6 | InternalServerError, 7 | Param, 8 | NotFoundError, 9 | Post, 10 | CurrentUser, 11 | Body, 12 | Delete, 13 | UnauthorizedError, 14 | } from 'routing-controllers'; 15 | import { BaseController } from './BaseController'; 16 | import { ExampleBoardService } from '../service'; 17 | import { ResponseSchema, OpenAPI } from 'routing-controllers-openapi'; 18 | import { ExampleBoard, User } from '../model'; 19 | import { IboardDTOClass } from '../dto/BoardDTO'; 20 | 21 | @JsonController('/board') 22 | export class BoardController extends BaseController { 23 | constructor(private exampleBoardService: ExampleBoardService) { 24 | super(); 25 | } 26 | @HttpCode(200) 27 | @Get('/example') 28 | @ResponseSchema(ExampleBoard, { 29 | description: 'A list of exampleBoard objects', 30 | isArray: true, 31 | statusCode: '200', 32 | }) 33 | async exampleBoardList( 34 | @QueryParam('page') take: number, 35 | @QueryParam('query') query?: string, 36 | ) { 37 | try { 38 | return await this.exampleBoardService.getBoardList(take, query); 39 | } catch (err) { 40 | throw new InternalServerError(err); 41 | } 42 | } 43 | 44 | @HttpCode(200) 45 | @ResponseSchema(ExampleBoard, { 46 | description: 'get exampleBoard by id', 47 | isArray: false, 48 | statusCode: '200', 49 | }) 50 | @Get('/example/:id') 51 | async getExampleBoard(@Param('id') id: number) { 52 | const board = await this.exampleBoardService.getById(id, [ 53 | 'user', 54 | 'comments', 55 | 'comments.user', 56 | 'comments.depthComments', 57 | 'comments.depthComments.user', 58 | ]); 59 | if (board === undefined) { 60 | throw new NotFoundError(`can not get example board id ${id}`); 61 | } 62 | return board; 63 | } 64 | 65 | @Post('/example') 66 | @HttpCode(201) 67 | // if you want to use jwt token execution in swagger, put this line below 68 | @OpenAPI({ 69 | security: [{ bearerAuth: [] }], // Applied to each method 70 | }) 71 | @ResponseSchema(ExampleBoard, { 72 | description: 'write exampleBoard Body: title:string, content: string', 73 | isArray: false, 74 | statusCode: '201', 75 | }) 76 | async writeExampleBoard( 77 | @CurrentUser({ required: true }) user: User, 78 | // this makes Request schema in swagger 79 | @Body() body: IboardDTOClass, 80 | ) { 81 | const board = await this.exampleBoardService.save({ 82 | title: body.title, 83 | content: body.content, 84 | user, 85 | }); 86 | return board; 87 | } 88 | @HttpCode(204) 89 | @Delete('/example/:id') 90 | @OpenAPI({ 91 | security: [{ bearerAuth: [] }], // Applied to each method 92 | summary: 'soft delete example board', 93 | description: 94 | 'return { result: true content:{}} or { result: false, content: {} } ', 95 | }) 96 | async deleteExampleBoard(@Param('id') id: number, @CurrentUser() user: User) { 97 | const board = await this.exampleBoardService.getById(id); 98 | if (board.user.id !== user.id) { 99 | throw new UnauthorizedError('Unauthorized delete board'); 100 | } 101 | 102 | const deleteBoard = await this.exampleBoardService.softDelete(board.id); 103 | if (deleteBoard.deletedAt == null) { 104 | throw new InternalServerError('db transaction error'); 105 | } 106 | return {}; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /.github/workflows/aws.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build and push a new container image to Amazon ECR, 2 | # and then will deploy a new task definition to Amazon ECS, on every push 3 | # to the master branch. 4 | # 5 | # To use this workflow, you will need to complete the following set-up steps: 6 | # 7 | # 1. Create an ECR repository to store your images. 8 | # For example: `aws ecr create-repository --repository-name my-ecr-repo --region us-east-2`. 9 | # Replace the value of `ECR_REPOSITORY` in the workflow below with your repository's name. 10 | # Replace the value of `aws-region` in the workflow below with your repository's region. 11 | # 12 | # 2. Create an ECS task definition, an ECS cluster, and an ECS service. 13 | # For example, follow the Getting Started guide on the ECS console: 14 | # https://us-east-2.console.aws.amazon.com/ecs/home?region=us-east-2#/firstRun 15 | # Replace the values for `service` and `cluster` in the workflow below with your service and cluster names. 16 | # 17 | # 3. Store your ECS task definition as a JSON file in your repository. 18 | # The format should follow the output of `aws ecs register-task-definition --generate-cli-skeleton`. 19 | # Replace the value of `task-definition` in the workflow below with your JSON file's name. 20 | # Replace the value of `container-name` in the workflow below with the name of the container 21 | # in the `containerDefinitions` section of the task definition. 22 | # 23 | # 4. Store an IAM user access key in GitHub Actions secrets named `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. 24 | # See the documentation for each action used below for the recommended IAM policies for this IAM user, 25 | # and best practices on handling the access key credentials. 26 | 27 | on: 28 | push: 29 | branches: 30 | - master 31 | - release* 32 | pull_request: 33 | branches: 34 | - master 35 | - release 36 | 37 | name: Deploy to Amazon ECS 38 | 39 | jobs: 40 | deploy: 41 | name: Deploy 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v2 47 | 48 | - name: Configure AWS credentials 49 | uses: aws-actions/configure-aws-credentials@v1 50 | with: 51 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 52 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 53 | aws-region: us-east-2 54 | 55 | - name: Login to Amazon ECR 56 | id: login-ecr 57 | uses: aws-actions/amazon-ecr-login@v1 58 | 59 | - name: Build, tag, and push image to Amazon ECR 60 | id: build-image 61 | env: 62 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 63 | ECR_REPOSITORY: api-server-boilerplate 64 | IMAGE_TAG: ${{ github.sha }} 65 | run: | 66 | # Build a docker container and 67 | # push it to ECR so that it can 68 | # be deployed to ECS. 69 | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . 70 | docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG 71 | echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" 72 | 73 | - name: Fill in the new image ID in the Amazon ECS task definition 74 | id: task-def 75 | uses: aws-actions/amazon-ecs-render-task-definition@v1 76 | with: 77 | task-definition: task-definition.json 78 | container-name: api-server-boilerplate 79 | image: ${{ steps.build-image.outputs.image }} 80 | 81 | - name: Deploy Amazon ECS task definition 82 | uses: aws-actions/amazon-ecs-deploy-task-definition@v1 83 | with: 84 | task-definition: ${{ steps.task-def.outputs.task-definition }} 85 | service: api-server-boilerplate 86 | cluster: your-cluster 87 | wait-for-service-stability: true 88 | -------------------------------------------------------------------------------- /src/controller/ExampleBoardCommentController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JsonController, 3 | Post, 4 | CurrentUser, 5 | Body, 6 | Put, 7 | UnauthorizedError, 8 | Get, 9 | Req, 10 | Param, 11 | Delete, 12 | } from 'routing-controllers'; 13 | import { BaseCommentController } from './BaseCommentController'; 14 | import { ExampleBoardComment, User } from '../model'; 15 | import { ExampleBoardCommentService } from '../service'; 16 | import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; 17 | import express from 'express'; 18 | import { IcommentDTOClass } from '../dto/commentDTO'; 19 | 20 | @JsonController('/example_board_comment') 21 | export class ExampleBoardCommentController extends BaseCommentController< 22 | ExampleBoardComment, 23 | ExampleBoardCommentService 24 | > { 25 | constructor(private exampleBoardCommentService: ExampleBoardCommentService) { 26 | super(exampleBoardCommentService); 27 | } 28 | 29 | @Post() 30 | @OpenAPI({ 31 | summary: 'save exampleBoardComment', 32 | description: ' comment: string; boardId: number; user: User;', 33 | security: [{ bearerAuth: [] }], // Applied to each method 34 | }) 35 | @ResponseSchema(ExampleBoardComment, { 36 | isArray: false, 37 | statusCode: '201', 38 | }) 39 | async save( 40 | @CurrentUser({ required: true }) user: User, 41 | @Body() body: Pick, 42 | ) { 43 | return this.exampleBoardCommentService.save({ 44 | comment: body.comment, 45 | boardId: body.boardId, 46 | user, 47 | }); 48 | } 49 | 50 | @Put('/:comment_id') 51 | @OpenAPI({ 52 | summary: 'edit exampleBoardComment', 53 | description: ' comment: string; boardId: number; user: User;', 54 | security: [{ bearerAuth: [] }], // Applied to each method 55 | }) 56 | @ResponseSchema(ExampleBoardComment, { 57 | isArray: false, 58 | statusCode: '201', 59 | }) 60 | async updateComment( 61 | @CurrentUser({ required: true }) user: User, 62 | @Body() body: Pick, 63 | @Param('comment_id') id: number, 64 | ) { 65 | const oldComment = await this.exampleBoardCommentService.getById(id, [ 66 | 'user', 67 | ]); 68 | if (oldComment.user.id !== user.id) { 69 | throw new UnauthorizedError('Unauthorized update comment'); 70 | } 71 | return this.update(id, body.comment); 72 | } 73 | 74 | @Get('/report/:comment_id') 75 | @OpenAPI({ 76 | summary: 'report exampleBoardComment', 77 | description: '', 78 | security: [{ bearerAuth: [] }], // Applied to each method 79 | }) 80 | @ResponseSchema(ExampleBoardComment, { 81 | isArray: false, 82 | statusCode: '201', 83 | }) 84 | async reportComment( 85 | @CurrentUser({ required: true }) user: User, 86 | @Param('comment_id') commentId: number, 87 | @Req() 88 | request: express.Request, 89 | ) { 90 | const url = `${request.method}|${request.url}`; 91 | return this.updateReport(commentId, url, user); 92 | } 93 | 94 | @Delete('/:comment_id') 95 | @OpenAPI({ 96 | summary: 'report exampleBoardComment', 97 | description: '', 98 | security: [{ bearerAuth: [] }], // Applied to each method 99 | }) 100 | @ResponseSchema(ExampleBoardComment, { 101 | isArray: false, 102 | statusCode: '201', 103 | }) 104 | async deleteComment( 105 | @CurrentUser({ required: true }) user: User, 106 | @Param('comment_id') commentId: number, 107 | ) { 108 | const oldComment = await this.exampleBoardCommentService.getById( 109 | commentId, 110 | ['user', 'depthComments'], 111 | ); 112 | if (oldComment.user.id !== user.id) { 113 | throw new UnauthorizedError('Unathorized delete comment'); 114 | } else if (oldComment.depthComments.length !== 0) { 115 | return this.update(commentId, '[This is deleted comment.]'); 116 | } 117 | return this.delete(commentId); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/controller/ExampleBoardDepthCommentController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JsonController, 3 | Post, 4 | CurrentUser, 5 | Body, 6 | Put, 7 | UnauthorizedError, 8 | Get, 9 | Req, 10 | Param, 11 | Delete, 12 | } from 'routing-controllers'; 13 | import { BaseCommentController } from './BaseCommentController'; 14 | import { User, ExampleBoardDepthComment } from '../model'; 15 | import { ExampleBoardDepthCommentService } from '../service'; 16 | import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; 17 | import express from 'express'; 18 | import { IcommentDTOClass } from '../dto/commentDTO'; 19 | 20 | @JsonController('/example_board_depth_comment') 21 | export class ExampleBoardDepthCommentController extends BaseCommentController< 22 | ExampleBoardDepthComment, 23 | ExampleBoardDepthCommentService 24 | > { 25 | constructor( 26 | private exampleBoardDepthCommentService: ExampleBoardDepthCommentService, 27 | ) { 28 | super(exampleBoardDepthCommentService); 29 | } 30 | 31 | @Post('/:comment_id') 32 | @OpenAPI({ 33 | summary: 'save exampleBoardDepthComment', 34 | description: ' comment: string; commentId: number; user: User;', 35 | security: [{ bearerAuth: [] }], // Applied to each method 36 | }) 37 | @ResponseSchema(ExampleBoardDepthComment, { 38 | isArray: false, 39 | statusCode: '201', 40 | }) 41 | async save( 42 | @CurrentUser({ required: true }) user: User, 43 | @Body() body: Pick, 44 | @Param('comment_id') commentId: number, 45 | ) { 46 | return this.exampleBoardDepthCommentService.createOrUpdate({ 47 | comment: body.comment, 48 | commentId, 49 | user, 50 | }); 51 | } 52 | 53 | @Put('/:depth_comment_id') 54 | @OpenAPI({ 55 | summary: 'edit exampleBoardComment', 56 | description: ' comment: string; boardId: number; user: User;', 57 | security: [{ bearerAuth: [] }], // Applied to each method 58 | }) 59 | @ResponseSchema(ExampleBoardDepthComment, { 60 | isArray: false, 61 | statusCode: '201', 62 | }) 63 | async updateComment( 64 | @CurrentUser({ required: true }) user: User, 65 | @Body() body: Pick, 66 | @Param('depth_comment_id') id: number, 67 | ) { 68 | const oldComment = await this.exampleBoardDepthCommentService.getById(id, [ 69 | 'user', 70 | ]); 71 | if (oldComment.user.id !== user.id) { 72 | throw new UnauthorizedError('Unauthorized update depth comment'); 73 | } 74 | return this.update(id, body.comment); 75 | } 76 | 77 | @Get('/report/:depth_comment_id') 78 | @OpenAPI({ 79 | summary: 'report exampleBoardComment', 80 | description: '', 81 | security: [{ bearerAuth: [] }], // Applied to each method 82 | }) 83 | @ResponseSchema(ExampleBoardDepthComment, { 84 | isArray: false, 85 | statusCode: '201', 86 | }) 87 | async reportComment( 88 | @CurrentUser({ required: true }) user: User, 89 | @Param('depth_comment_id') commentId: number, 90 | @Req() 91 | request: express.Request, 92 | ) { 93 | const url = `${request.method}|${request.url}`; 94 | return this.updateReport(commentId, url, user); 95 | } 96 | 97 | @Delete('/:depth_comment_id') 98 | @OpenAPI({ 99 | summary: 'report exampleBoardComment', 100 | description: '', 101 | security: [{ bearerAuth: [] }], // Applied to each method 102 | }) 103 | @ResponseSchema(ExampleBoardDepthComment, { 104 | isArray: false, 105 | statusCode: '201', 106 | }) 107 | async deleteComment( 108 | @CurrentUser({ required: true }) user: User, 109 | @Param('depth_comment_id') commentId: number, 110 | ) { 111 | const oldComment = await this.exampleBoardDepthCommentService.getById( 112 | commentId, 113 | ['user', 'depthComments'], 114 | ); 115 | if (oldComment.user.id !== user.id) { 116 | throw new UnauthorizedError('권한이 없습니다.'); 117 | } 118 | return this.delete(commentId); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | > This project use gts style and semantic commit message. please follow rules below. 11 | 12 | - git semantic commit message 13 | 14 | | when? | commit message example | 15 | | ---------------------------- | --------------------------------------- | 16 | | feature | feat: ⚡️ add feature | 17 | | fix | fix: 🔥 bug fix | 18 | | refactoring | refactor: 🛠 refactoring something logic | 19 | | code without production code | chore: 📦 add some packages and scripts | 20 | | readme or document | docs: 📚 update readme | 21 | | code about deploy | deploy: ✈️ make dockefile etc | 22 | | style commit ( gts ..) | style: 💅 change gts style | 23 | 24 | - [gts](https://github.com/google/gts) 25 | 26 | ```sh 27 | #this project use gts style so I recommend you to use eslint and prettier with gts extension 28 | yarn check 29 | yarn clean 30 | yarn fix 31 | ``` 32 | 33 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 34 | build. 35 | 2. Update the README.md with details of changes to the interface, this includes new environment 36 | variables, exposed ports, useful file locations and container parameters. 37 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 38 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 39 | If you change something, you should update [CHANGELOG](./CHANGELOG.md) 40 | 4. You may merge the Pull Request in once you have the sign-off of other developers. 41 | 42 | ## Code of Conduct 43 | 44 | ### Our Pledge 45 | 46 | In the interest of fostering an open and welcoming environment, we as 47 | contributors and maintainers pledge to making participation in our project and 48 | our community a harassment-free experience for everyone, regardless of age, body 49 | size, disability, ethnicity, gender identity and expression, level of experience, 50 | nationality, personal appearance, race, religion, or sexual identity and 51 | orientation. 52 | 53 | ### Our Standards 54 | 55 | Examples of behavior that contributes to creating a positive environment 56 | include: 57 | 58 | - Using welcoming and inclusive language 59 | - Being respectful of differing viewpoints and experiences 60 | - Gracefully accepting constructive criticism 61 | - Focusing on what is best for the community 62 | - Showing empathy towards other community members 63 | 64 | Examples of unacceptable behavior by participants include: 65 | 66 | - The use of sexualized language or imagery and unwelcome sexual attention or 67 | advances 68 | - Trolling, insulting/derogatory comments, and personal or political attacks 69 | - Public or private harassment 70 | - Publishing others' private information, such as a physical or electronic 71 | address, without explicit permission 72 | - Other conduct which could reasonably be considered inappropriate in a 73 | professional setting 74 | 75 | ### Our Responsibilities 76 | 77 | Project maintainers are responsible for clarifying the standards of acceptable 78 | behavior and are expected to take appropriate and fair corrective action in 79 | response to any instances of unacceptable behavior. 80 | 81 | Project maintainers have the right and responsibility to remove, edit, or 82 | reject comments, commits, code, wiki edits, issues, and other contributions 83 | that are not aligned to this Code of Conduct, or to ban temporarily or 84 | permanently any contributor for other behaviors that they deem inappropriate, 85 | threatening, offensive, or harmful. 86 | 87 | ### Scope 88 | 89 | This Code of Conduct applies both within project spaces and in public spaces 90 | when an individual is representing the project or its community. Examples of 91 | representing a project or community include using an official project e-mail 92 | address, posting via an official social media account, or acting as an appointed 93 | representative at an online or offline event. Representation of a project may be 94 | further defined and clarified by project maintainers. 95 | 96 | ### Enforcement 97 | 98 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 99 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 100 | complaints will be reviewed and investigated and will result in a response that 101 | is deemed necessary and appropriate to the circumstances. The project team is 102 | obligated to maintain confidentiality with regard to the reporter of an incident. 103 | Further details of specific enforcement policies may be posted separately. 104 | 105 | Project maintainers who do not follow or enforce the Code of Conduct in good 106 | faith may face temporary or permanent repercussions as determined by other 107 | members of the project's leadership. 108 | 109 | ### Attribution 110 | 111 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 112 | available at [http://contributor-covenant.org/version/1/4][version] 113 | 114 | [homepage]: http://contributor-covenant.org 115 | [version]: http://contributor-covenant.org/version/1/4/ 116 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | testTimeout: 100000, 6 | // All imported modules in your tests should be mocked automatically 7 | // automock: false, 8 | 9 | // Stop running tests after `n` failures 10 | // bail: 0, 11 | 12 | // Respect "browser" field in package.json when resolving modules 13 | // browser: false, 14 | 15 | // The directory where Jest should store its cached dependency information 16 | // cacheDirectory: "/private/var/folders/y0/hznv9jbn7glgx8h51v94qxwm0000gn/T/jest_dx", 17 | 18 | // Automatically clear mock calls and instances between every test 19 | // clearMocks: false, 20 | 21 | // Indicates whether the coverage information should be collected while executing the test 22 | // collectCoverage: false, 23 | 24 | // An array of glob patterns indicating a set of files for which coverage information should be collected 25 | // collectCoverageFrom: null, 26 | 27 | // The directory where Jest should output its coverage files 28 | // coverageDirectory: null, 29 | 30 | // An array of regexp pattern strings used to skip coverage collection 31 | coveragePathIgnorePatterns: ['/node_modules/'], 32 | 33 | // A list of reporter names that Jest uses when writing coverage reports 34 | // coverageReporters: [ 35 | // "json", 36 | // "text", 37 | // "lcov", 38 | // "clover" 39 | // ], 40 | 41 | // An object that configures minimum threshold enforcement for coverage results 42 | // coverageThreshold: null, 43 | 44 | // A path to a custom dependency extractor 45 | // dependencyExtractor: null, 46 | 47 | // Make calling deprecated APIs throw helpful error messages 48 | // errorOnDeprecated: false, 49 | 50 | // Force coverage collection from ignored files using an array of glob patterns 51 | // forceCoverageMatch: [], 52 | 53 | // A path to a module which exports an async function that is triggered once before all test suites 54 | // globalSetup: null, 55 | 56 | // A path to a module which exports an async function that is triggered once after all test suites 57 | // globalTeardown: null, 58 | 59 | // A set of global variables that need to be available in all test environments 60 | globals: { 61 | 'ts-jest': { 62 | diagnostics: { 63 | pathRegex: /\.(spec)\.ts$/, 64 | warnOnly: true, 65 | }, 66 | }, 67 | }, 68 | 69 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 70 | // maxWorkers: "50%", 71 | 72 | // An array of directory names to be searched recursively up from the requiring module's location 73 | // moduleDirectories: [ 74 | // "node_modules" 75 | // ], 76 | 77 | // An array of file extensions your modules use 78 | moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'], 79 | 80 | // A map from regular expressions to module names that allow to stub out resources with a single module 81 | // moduleNameMapper: {}, 82 | 83 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 84 | // modulePathIgnorePatterns: [], 85 | 86 | // Activates notifications for test results 87 | // notify: false, 88 | 89 | // An enum that specifies notification mode. Requires { notify: true } 90 | // notifyMode: "failure-change", 91 | 92 | // A preset that is used as a base for Jest's configuration 93 | preset: 'ts-jest', 94 | 95 | // Run tests from one or more projects 96 | // projects: null, 97 | 98 | // Use this configuration option to add custom reporters to Jest 99 | // reporters: undefined, 100 | 101 | // Automatically reset mock state between every test 102 | // resetMocks: false, 103 | 104 | // Reset the module registry before running each individual test 105 | // resetModules: false, 106 | 107 | // A path to a custom resolver 108 | // resolver: null, 109 | 110 | // Automatically restore mock state between every test 111 | // restoreMocks: false, 112 | 113 | // The root directory that Jest should scan for tests and modules within 114 | // rootDir: null, 115 | 116 | // A list of paths to directories that Jest should use to search for files in 117 | roots: ['/src'], 118 | 119 | // Allows you to use a custom runner instead of Jest's default test runner 120 | // runner: "jest-runner", 121 | 122 | // The paths to modules that run some code to configure or set up the testing environment before each test 123 | // setupFiles: [], 124 | 125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 126 | // setupFilesAfterEnv: [], 127 | 128 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 129 | // snapshotSerializers: [], 130 | 131 | // The test environment that will be used for testing 132 | testEnvironment: 'node', 133 | 134 | // Options that will be passed to the testEnvironment 135 | // testEnvironmentOptions: {}, 136 | 137 | // Adds a location field to test results 138 | // testLocationInResults: false, 139 | 140 | // The glob patterns Jest uses to detect test files 141 | // testMatch: [ 142 | // "**/__tests__/**/*.[jt]s?(x)", 143 | // "**/?(*.)+(spec|test).[tj]s?(x)" 144 | // ], 145 | 146 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 147 | testPathIgnorePatterns: ['/node_modules/'], 148 | 149 | // The regexp pattern or array of patterns that Jest uses to detect test files 150 | testRegex: [/\.(spec)\.ts$/], 151 | 152 | // This option allows the use of a custom results processor 153 | // testResultsProcessor: null, 154 | 155 | // This option allows use of a custom test runner 156 | // testRunner: "jasmine2", 157 | 158 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 159 | // testURL: "http://localhost", 160 | 161 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 162 | // timers: "real", 163 | 164 | // A map from regular expressions to paths to transformers 165 | // transform: null, 166 | 167 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 168 | // transformIgnorePatterns: [ 169 | // "/node_modules/" 170 | // ], 171 | 172 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 173 | // unmockedModulePathPatterns: undefined, 174 | 175 | // Indicates whether each individual test should be reported during the run 176 | // verbose: null, 177 | 178 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 179 | // watchPathIgnorePatterns: [], 180 | 181 | // Whether to use watchman for file crawling 182 | // watchman: true, 183 | }; 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to api_server_boilerplate 👋

2 |

3 | Version 4 | 5 | Documentation 6 | 7 | 8 | License: MIT 9 | 10 | Cody Style: Google 11 |

12 | 13 | > easy to use typescript express boilerplate. You can use board api, user api, error tracking etc.. 14 | 15 | --- 16 | 17 | ### 🏠 [Homepage](https://github.com/Q00/api_server_boilerplate/blob/development/README.md) 18 | 19 | ## Install 20 | 21 | ```sh 22 | yarn install 23 | # after put your env flie 24 | yarn db:dev sync # development environment 25 | # or 26 | yarn db:sync # production environment 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```sh 32 | yarn dev # development environment 33 | yarn start # production environment 34 | ``` 35 | 36 | ## Run tests 37 | 38 | ```sh 39 | yarn prepare 40 | yarn build 41 | yarn test 42 | 43 | ``` 44 | 45 | Or you can use debug with vscode 46 | 47 | ## code 48 | 49 | ### model 50 | 51 | There are base models. You can extend this base models. 52 | 53 | #### Base model 54 | 55 | ```typescript 56 | export abstract class BaseModel { 57 | @IsInt() 58 | @Generated('increment') 59 | @PrimaryColumn({ type: 'bigint', transformer: [bigIntTransformer] }) 60 | id!: number; 61 | 62 | @IsDate() 63 | @CreateDateColumn() 64 | createdAt!: Date; 65 | 66 | @IsDate() 67 | @UpdateDateColumn() 68 | updatedAt!: Date; 69 | 70 | @IsDate() 71 | @Column({ nullable: true, type: 'date', default: null }) 72 | deletedAt?: Date | null; 73 | } 74 | ``` 75 | 76 | Also There are base board, base comment model. 77 | 78 | #### Base board model 79 | 80 | ```typescript 81 | // you can extends this class making child board and add user 82 | 83 | export abstract class BaseBoard extends BaseModel { 84 | @Column({ length: 50 }) 85 | @IsString() 86 | title!: string; 87 | 88 | @IsString() 89 | @Column({ type: 'text' }) 90 | content!: string; 91 | 92 | @IsInt() 93 | @Column({ default: 0 }) 94 | reportCount!: number; 95 | } 96 | ``` 97 | 98 | #### Base comment model 99 | 100 | ```typescript 101 | // you can extends this class making child comment and add user 102 | 103 | export abstract class BaseComment extends BaseModel { 104 | @Column({ length: 50 }) 105 | @IsString() 106 | @MaxLength(50) 107 | comment!: string; 108 | 109 | @Column({ default: 0 }) 110 | @IsInt() 111 | reportCount!: number; 112 | } 113 | ``` 114 | 115 | ### service 116 | 117 | Threr are base services. You can extend this base services to other child service. 118 | 119 | #### Base service 120 | 121 | ```typescript 122 | // you can extends this BaseService to use common method 123 | 124 | export abstract class BaseService { 125 | protected genericRepository: Repository; 126 | private repo: ObjectType; 127 | constructor(repo: ObjectType) { 128 | this.genericRepository = getConnection().getRepository(repo); 129 | this.repo = repo; 130 | } 131 | } 132 | ``` 133 | 134 | And then you just call super call with your using repository 135 | 136 | ```typescript 137 | constructor() { 138 | super(RepoName); 139 | } 140 | ``` 141 | 142 | #### Base board service 143 | 144 | ```typescript 145 | @Service() 146 | export abstract class BaseBoardService extends BaseService< 147 | T 148 | > { 149 | constructor(repo: ObjectType) { 150 | super(repo); 151 | } 152 | } 153 | ``` 154 | 155 | This service is base board service. In this service, there are common method about board. You can extend this service to other child board service. 156 | 157 | #### Base comment service 158 | 159 | ```typescript 160 | export abstract class BaseCommentService< 161 | T extends BaseComment 162 | > extends BaseService { 163 | constructor(repo: ObjectType) { 164 | super(repo); 165 | } 166 | } 167 | ``` 168 | 169 | This service is base comment service. This service is very similar to base board service. 170 | 171 | ### Provider 172 | 173 | This module makes OAUTH logic. You can use base provider to extends other OAUTH. 174 | 175 | ```typescript 176 | export abstract class BaseProvider { 177 | protected accessToken: string; 178 | protected instance: AxiosInstance | null; 179 | constructor() { 180 | this.accessToken = ''; 181 | this.instance = null; 182 | } 183 | setToken(accessToken: string) { 184 | this.accessToken = accessToken; 185 | } 186 | 187 | setInstance(url: string, headers: object) { 188 | this.instance = apiClient(url, headers); 189 | this.instance.interceptors.response.use( 190 | (response) => response, 191 | (err) => Promise.reject(err), 192 | ); 193 | } 194 | 195 | getInstance() { 196 | return this.instance; 197 | } 198 | 199 | async generateToken(userId: number) { 200 | return `Bearer ${Authentication.generateToken(userId)}`; 201 | } 202 | } 203 | ``` 204 | 205 | Auth Conroller use this provider to make JWT token. 206 | 207 | ### Controller 208 | 209 | There are BaseAuthController, BaseCommentController and Other Controller. This project use [routing-controllers](https://github.com/typestack/routing-controllers) and [typedi](https://github.com/typestack/typedi). Thier README help you understand this architecture. 210 | 211 | #### Base Auth Controller 212 | 213 | ```typescript 214 | export class BaseAuthController extends BaseController { 215 | // this can be used in child class (ExampleAuthController) 216 | protected userAccountService: UserAccountService; 217 | protected userService: UserService; 218 | constructor(protected provider: T) { 219 | super(); 220 | this.provider = provider; 221 | this.userAccountService = Container.get(UserAccountService); 222 | this.userService = Container.get(UserService); 223 | } 224 | } 225 | ``` 226 | 227 | #### Base Comment Controller 228 | 229 | ```typescript 230 | export abstract class BaseCommentController< 231 | U extends BaseComment, 232 | T extends BaseCommentService 233 | > extends BaseController { 234 | protected service: T; 235 | constructor(service: T) { 236 | super(); 237 | this.service = service; 238 | } 239 | } 240 | ``` 241 | 242 | If you want to extends this controller, you should call super with service like below. 243 | 244 | ```typescript 245 | @JsonController('/example_board_comment') 246 | export class ExampleBoardCommentController extends BaseCommentController< 247 | ExampleBoardComment, 248 | ExampleBoardCommentService 249 | > { 250 | //this private service automaticaly injected by typedi 251 | constructor(private exampleBoardCommentService: ExampleBoardCommentService) { 252 | super(exampleBoardCommentService); 253 | } 254 | } 255 | ``` 256 | 257 | ### DTO 258 | 259 | To make request schema, this project use [class-validator](https://github.com/typestack/class-validator). This request schema will be shown in swagger ui or Redoc. 260 | 261 | ### Interceptor 262 | 263 | This module use [routing-controllers](https://github.com/typestack/routing-controllers) interceptor 264 | 265 | ### Middleware 266 | 267 | This module use [routing-controllers](https://github.com/typestack/routing-controllers) Middleware 268 | 269 | ### Database 270 | 271 | This project use [typeorm](https://typeorm.io/) and connect with [Postgres](https://www.postgresql.org/). 272 | 273 | #### Naming Strategy 274 | 275 | using snake case. 276 | 277 | ```typescript 278 | export class NamingStrategy extends DefaultNamingStrategy { 279 | tableName(targetName: string, userSpecifiedName: string | undefined): string { 280 | return plural(snakeCase(userSpecifiedName || targetName)); 281 | } 282 | 283 | relationName(propertyName: string): string { 284 | return snakeCase(propertyName); 285 | } 286 | 287 | columnName(propertyName: string, customName: string) { 288 | return snakeCase(customName || propertyName); 289 | } 290 | 291 | joinColumnName(relationName: string, referencedColumnName: string) { 292 | return snakeCase(`${relationName}_${referencedColumnName}`); 293 | } 294 | 295 | joinTableColumnName( 296 | tableName: string, 297 | propertyName: string, 298 | columnName: string, 299 | ) { 300 | return snakeCase(`${tableName}_${columnName || propertyName}`); 301 | } 302 | } 303 | ``` 304 | 305 | #### config 306 | 307 | ```typescript 308 | const typeOrmConfig: PostgresConnectionOptions = { 309 | type: 'postgres', 310 | host: process.env.DB_HOST, 311 | namingStrategy: new NamingStrategy(), 312 | port: Number(process.env.DB_PORT), 313 | username: process.env.DB_USER, 314 | password: process.env.DB_PW, 315 | database: process.env.DATABASE, 316 | synchronize: false, 317 | logging: false, 318 | entities: [`${path.join(__dirname, '..', 'model')}/**.[tj]s`], 319 | migrations: [`${path.join(__dirname, '..', 'model')}/migration/**.[tj]s`], 320 | }; 321 | ``` 322 | 323 | ## Env variable 324 | 325 | ``` 326 | DB_HOST= 327 | DB_USER= 328 | DB_PW= 329 | PORT= # your server port 330 | DB_PORT= 331 | DATABASE= # database name 332 | TEST_TOKEN= # jwt token to use in testing 333 | SENTRY_DSN= # sentry dsn 334 | ``` 335 | 336 | ## Author 337 | 338 | 👤 **Q00 ** 339 | 340 | - Website: https://velog.io/@q00 341 | - Github: [@Q00](https://github.com/Q00) 342 | 343 | ## 🤝 Contributing 344 | 345 | Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/Q00/api_server_boilerplate/issues).
If you want to contribute this repo, check [contribute page](./CONTRIBUTING.md) 346 | 347 | ## 🔍 Relase note && Change log 348 | 349 | Release note and change log are exist in [CHANGELOG](./CHANGELOG.md) 350 | 351 | ## Show your support 352 | 353 | Give a ⭐️ if this project helped you! 354 | 355 | --- 356 | 357 | _This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ 358 | --------------------------------------------------------------------------------