프로그래밍(Web)/Javascript(TS,Node)

[바미] Typescript-restful-starter 코드 분석해보기 !

Bami 2020. 12. 22. 17:17
728x90
반응형

본 글은

github.com/camesine/Typescript-restful-starter

 

camesine/Typescript-restful-starter

Node.js + ExpressJS + Joi + Typeorm + Typescript + JWT + ES2015 + Clustering + Tslint + Mocha + Chai - camesine/Typescript-restful-starter

github.com

의 코드를 가지고 TypeScript를 처음 공부 하였을 때 정리 해놓은 것을 써놓은 글입니다.

혹여나 코드의 해석이 틀릴 수 있으므로, 유의 하시기 바랍니다.

app/controllers 코드 분석


ContController.ts

req, res 설정 부분

import * as express from "express";
/* req, res 설정 */
export abstract class Controller {

    public req: express.Request;
    public res: express.Response;

    constructor(req: express.Request, res: express.Response) {
        this.req = req;
        this.res = res;
    }
}

index.ts

import { JWTController } from "./Jwt.controller";
import { SampleController } from "./Sample.controller";

export { JWTController, SampleController };

Jwt.controller.ts

JWT 설정 부분

import { Request, Response } from "express";
import { JwtService } from "../services";
import { Controller } from "./Controller";
/* JWT 설정 */
export class JWTController extends Controller {

    private jwtService: JwtService;

    constructor(req: Request, res: Response) {
        super(req, res);
        this.jwtService = new JwtService();
    }

    public async index(): Promise<Response> {
        const { payload } = this.req.body;
        const token = await this.jwtService.signToken(payload);
        return this.res.send(token);
    }

}

Sample.controller.ts

SampleController 클래스 생성

export class SampleController extends Controller {

    private sampleService: SampleService;
    private sample: Sample;

    constructor(req: Request, res: Response) {
        super(req, res);
        this.sample = new Sample();
        this.sampleService = new SampleService();
}

SampleController안에 정의된 컨트롤러들

  // 생성된 데이터 리스트 출력
    public async all(): Promise<Response> {
        const sampleList = await this.sampleService.find();
        return this.res.send(sampleList);
    }

    // select -> routes/Sample.route.ts 참조.
    public async find(): Promise<Response> {
        const { id } = this.req.params as unknown as { id: number };
        const sample = await this.sampleService.findOneById(id);
        if (sample) {
            return this.res.status(200).send(sample);
        } else {
            return this.res.status(404).send({ text: "not found" });
        }
    }

    // input -> routes/Sample.route.ts 참조. 
    public async create(): Promise<Response> {
        const { text } = this.req.body as { text: string };
        // Sample.schemas.ts에서 따로 email의 입력 받는 틀을 잡아주면 아래의 코드를 사용할 수 있다.
        // const { text, email } = this.req.body as { text: string, email: string };
        this.sample.text = text;
        this.sample.email = "someone@somewhere.com";
        // this.sample.email = email; 
        try {
            const result = await this.sampleService.save(this.sample);
            return this.res.status(200).send(result);
        } catch (ex) {
            return this.res.status(404).send({ text: "ERROR" });
        }
    }

    // update -> routes/Sample.route.ts 참조.
    public async update(): Promise<Response> {
        const { id, text, email } = this.req.body as { id: number, text: string, email: string };
        this.sample.id = id;
        this.sample.text = text;
        this.sample.email = email;
        try {
            const sample = await this.sampleService.save(this.sample);
            if (sample) {
                return this.res.status(200).send();
            } else {
                return this.res.status(404).send({ text: "not found" });
            }
        } catch (ex) {
            return this.res.status(404).send({ text: "error" });
        }
    }

     // update -> routes/Sample.route.ts 참조.
    public async delete(): Promise<Response> {
        const { id } = this.req.body as { id: number };
        try {
            await this.sampleService.removeById(id);
            return this.res.status(204).send();
        } catch (ex) {
            return this.res.status(404).send({ text: "ERROR" });
        }
    }

app/middlewares 코드 분석


index.ts

index

import { Validator } from "./Validator";
export { Validator };

Validator.ts

검증 부분

/* Joi를 사용하여 Express 애플리케이션의 입력을 검증한다. */
import * as express from "express";
import { ObjectSchema, ValidationOptions } from "joi";

const OPTS: ValidationOptions = {
    abortEarly: false,
    language: {
        key: "{{key}} ",
    },
};

export function Validator(schema: ObjectSchema) {
    return (req: express.Request, res: express.Response, next: express.NextFunction) => {
        const params = req.method === "GET" ? req.params : req.body; // req.method가 GET방식일 경우.
        const { error } = schema.validate(params, OPTS);
        if (error) {
            const { message } = error;
            return res.status(400).json({ message });
        } else {
            return next();
        }
    };
}

app/models 코드 분석


어플리케이션이 “무엇”을 할 것인지를 정의하는 부분. 내부 비지니스 로직을 처리하기 위한 역할.

ex)처리되는 알고리즘, DB, 데이터 등등.

index.ts

index

import { Sample } from "./Sample.model";
export { Sample };

Sample.model.ts

import

import { IsEmail } from "class-validator";
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";

DB모델 정의

@Entity("sample")
export class Sample extends BaseEntity {

    @PrimaryGeneratedColumn() // 인덱스 자동 증가.
    public id: number;

    @Column("text")
    public text: string;

    @Column("text")
    @IsEmail()
    public email: string;

app/repository/ 코드 분석


서비스에서 인자로 전달받은 EntityManager를 통해 쿼리를 수행한다.

index.ts

index

import { SampleRepository } from "./Sample.repository";
export { SampleRepository };

Sample.repository.ts

실행되는 쿼리들

@EntityRepository(Sample)
export class SampleRepository extends Repository<Sample> {

    public bulkCreate(Samples: Sample[]): Promise<any> {
        return this.manager.createQueryBuilder().insert().into(Sample).values(Samples).execute();
    }

    public async removeById(id: number): Promise<Sample> {
        const itemToRemove: Sample = await this.findOne({id});
        return this.manager.remove(itemToRemove);
    }

    public findByText(text: string): Promise<Sample[]> {
        return this.manager.find(Sample, {where: {text}});
    }

    public findOneById(id: number): Promise<Sample> {
        return this.manager.findOne(Sample, {where: {id}});
    }

}

app/routes 코드 분석


index.ts

index

import { JwtRouter } from "./Jwt.route";
import { SampleRouter } from "./Sample.route";
export { JwtRouter, SampleRouter };

Jwt.route.ts

Jwt 라우터(클래스 생성)

export class JwtRouter extends Router {
    constructor() {
        super(JWTController);
        this.router
            .post("/", this.handler(JWTController.prototype.index));
    }
}

Router.ts

라우터(클래스 생성)

export abstract class Router {

    public router: express.Router;
    private controller: any;

    constructor(controller: any) {
        this.controller = controller;
        this.router = express.Router();
    }

    protected handler(action: () => void): any { // 요청을 처리하는 콜백함수.
        return (req: Request, res: Response) => action.call(new this.controller(req, res));
    }
}

Sample.route.ts

import 부분

import { SampleController } from "../controllers";
import { Validator } from "../middlewares";
import { createSample, deleteSample, updateSample } from "../schemas";
import { Router } from "./Router";

Method 방식에 따른 처리

  • .get("/",...) - get방식에서 '/'이후 아무것도 입력하지 않았을 때 'createSample'을 통해 만들어진 데이터들이 리스트로 출력된다.
  • .get("/:id", ...) - get방식에서 '/'옆에 id값을 입력 시 해당 id값의 데이터를 출력시켜준다.
    ex: http://localhost:8080/2
  • .post("/", ...) - post방식에서 Sample.schemas.ts의 형식에 맞게 json값을 입력 시 id, text, eamil 값이 DB에 생성된다.

    (현재는 임의적인 text값만 주면 생성된다.)
  • .put("/", ...) - put 방식에서 id값을 json방식으로 입력 시 해당 id값이 삭제된다. (routes/Sample.route.ts에서 )
export class SampleRouter extends Router {
    constructor() {
        super(SampleController);
        this.router
            .get("/", this.handler(SampleController.prototype.all))
            .get("/:id", this.handler(SampleController.prototype.find))
            .post("/", [ Validator(createSample) ], this.handler(SampleController.prototype.create))
            .put("/", [ Validator(updateSample) ],  this.handler(SampleController.prototype.update))
            .delete("/", [ Validator(deleteSample) ], this.handler(SampleController.prototype.delete));
    }
}

app/schemas/ 코드 분석


입력받을 data들의 구조 정의하는 부분

index.ts

index

import { createSample, deleteSample, updateSample } from "./Sample.schemas";
export { createSample, deleteSample, updateSample };

Sample.schemas.ts

import

import { number, object, string } from "joi";

구조 정의

export const createSample = object().keys({
    text: string().required(),
    // email: string().required(),
});

export const updateSample = object().keys({
    id: number().required(),
    text: string().required(),
});

export const deleteSample = object().keys({
    id: number().required(),
});

app/test/ 코드 분석


/*
    create 시 json -> 
    {
        "text" : "임의 텍스트(영문으로)"
    }
*/

Sample.test.ts

import

import * as chai from "chai";
import * as dotenv from "dotenv";
import * as express from "express";
import { resolve } from "path";
import * as supertest from "supertest";
import { Sample } from "../app/models";
import { JwtService } from "../app/services/Jwt.service";
import { SampleService } from "../app/services/Sample.service";
import { Server } from "../config/Server";

환경변수 설정

dotenv.config({ path: resolve() + "/.env" });

전역변수 설정

let token: string;
let IdRecord: number;
let IdRecordTwo: number;
const server: Server = new Server();
let app: express.Application;
const sampleService = new SampleService();

test 예제들

describe("Sample route", () => {
    before((done) => {
        const sample = new Sample();
        sample.text = "SAMPLE TEXT";
        sample.email = "SAMPLE EMAIL";
        server.start().then(() => {
            app = server.App();
            Promise.all([
                new JwtService().signToken({ name: "name", role: "rol" }),
                sampleService.save(sample),
            ]).then((res) => {
                token = res[0];
                IdRecord = res[1].id;
                done();
            });
        });
    });

    after(async () => {
        const sampleOne = await sampleService.findOneById(IdRecord);
        const sampleTwo = await sampleService.findOneById(IdRecordTwo);
        if (sampleOne) {
            await sampleService.remove(sampleOne);
        }
        if (sampleTwo) {
            await sampleService.remove(sampleTwo);
        }
    });
    /* 각 기능 예제들 */
    it("Random Url gives 404", (done) => {
        supertest(app).get("/random-url")
            .set("Authorization", `bearer ${token}`).set("Accept", "application/json")
            .end((err: Error, res: supertest.Response) => {
                chai.expect(res.status).to.be.a("number");
                chai.expect(res.status).to.eq(404);
                done();
            });
    });

    it("Can list all Samples", (done) => {
        supertest(app).get("/")
            .set("Authorization", `bearer ${token}`).set("Accept", "application/json")
            .end((err: Error, res: supertest.Response) => {
                chai.expect(res.status).to.be.a("number");
                chai.expect(res.status).to.eq(200);
                chai.expect(res.body).to.be.a("array");
                chai.expect(res.body[0].text).to.be.a("string");
                done();
            });
    });

    it("Can search for Sample by Id", (done) => {
        supertest(app).get(`/${IdRecord}`)
            .set("Authorization", `bearer ${token}`).set("Accept", "application/json")
            .end((err: Error, res: supertest.Response) => {
                chai.expect(res.status).to.eq(200);
                chai.expect(res.body).to.be.a("object");
                chai.expect(res.body).to.have.all.keys("id", "text", "email");
                chai.expect(res.body.text).to.be.a("string");
                done();
            });
    });

    it("Can create a new Sample", (done) => {
        supertest(app).post("/")
            .set("Authorization", `bearer ${token}`)
            .set("Accept", "application/json")
            .send({text: "Sample text 100"})
            .end((err: Error, res: supertest.Response) => {
                chai.expect(res.status).to.eq(200);
                chai.expect(res.body).to.have.all.keys("id", "text", "email");
                chai.expect(res.body.id).to.be.a("number");
                chai.expect(res.body.text).to.be.a("string");
                IdRecordTwo = res.body.id;
                done();
            });
    });

    it("Can update an existing Sample", (done) => {
        supertest(app).put("/")
            .set("Authorization", `bearer ${token}`)
            .set("Accept", "application/json")
            .send({id: IdRecord, text: "Sample text updateado"})
            .end((err: Error, res: supertest.Response) => {
                chai.expect(res.status).to.eq(200);
                done();
            });
    });

    it("Can remove a sample by Id", (done) => {
        supertest(app).delete("/").set("Authorization", `bearer ${token}`)
            .set("Accept", "application/json")
            .send({id: IdRecord})
            .end((err: Error, res: supertest.Response) => {
                chai.expect(res.status).to.eq(204);
                done();
            });
    });

    it("Reports an error when finding a non-existent Sample by Id", (done) => {
        supertest(app).get(`/9999`)
            .set("Authorization", `bearer ${token}`)
            .set("Accept", "application/json")
            .end((err: Error, res: supertest.Response) => {
                chai.expect(res.status).to.eq(404);
                chai.expect(res.body).to.have.all.keys("text");
                chai.expect(res.body.text).to.be.a("string");
                chai.expect(res.body.text).to.equal("not found");
                done();
            });
    });

    it("Reports an error when trying to create an invalid Sample", (done) => {
        supertest(app).post("/").set("Authorization", `bearer ${token}`)
            .set("Accept", "application/json")
            .send({sample: "XXXX"})
            .end((err: Error, res: supertest.Response) => {
                chai.expect(res.status).to.eq(400);
                done();
            });
    });

    it("Reports an error when trying to update a Sample with invalid data", (done) => {
        supertest(app).put("/").set("Authorization", `bearer ${token}`)
            .set("Accept", "application/json")
            .send({sample: "XXXX"})
            .end((err: Error, res: supertest.Response) => {
                chai.expect(res.status).to.eq(400);
                done();
            });
    });

    it("Reports an error when trying to delete a Sample with invalid data", (done) => {
        supertest(app).delete("/").set("Authorization", `bearer ${token}`)
            .set("Accept", "application/json")
            .send({sample: "XXXX"})
            .end((err: Error, res: supertest.Response) => {
                chai.expect(res.status).to.eq(400);
                done();
            });
    });

});

## app/services 코드 분석

# index.ts
### index
```typescript
import { JwtService } from "./Jwt.service";
import { SampleService } from "./Sample.service";

export { JwtService, SampleService };

Jwt.service.ts

import

import * as JWT from "jsonwebtoken";
import { config } from "../../config";

Jwtservice 클래스 생성

export class JwtService {

    public signToken(params: { name: string, role: string }, options?: any): string {
        return JWT.sign(params, config.SECRET, options || undefined);
    }

}

Sample.service.ts

import

import { getCustomRepository } from "typeorm";
import { Sample } from "../models";
import { SampleRepository } from "../repository";

SampleService 클래스 생성 -> 서비스 생성.

export class SampleService {

    public findByText(text: string): Promise<Sample[]> {
        return getCustomRepository(SampleRepository).findByText(text);
    }

    public bulkCreate(Samples: Sample[]): Promise<Sample[]> {
        return getCustomRepository(SampleRepository).bulkCreate(Samples);
    }

    public findOneById(id: number): Promise<Sample> {
        return getCustomRepository(SampleRepository).findOneById(id);
    }

    public find(): Promise<Sample[]> {
        return getCustomRepository(SampleRepository).find();
    }

    public remove(sample: Sample): Promise<Sample> {
        return getCustomRepository(SampleRepository).remove(sample);
    }

    public removeById(id: number): Promise<Sample> {
        return getCustomRepository(SampleRepository).removeById(id);
    }

    public save(sample: Sample): Promise<Sample> {
        return getCustomRepository(SampleRepository).save(sample);
    }
}

실행화면


data list 출력

Tester

Web

insert

Tester

Web

select

update

Tester

Web

delete

Tester

Web

728x90
반응형