728x90
반응형
본 글은
github.com/camesine/Typescript-restful-starter
의 코드를 가지고 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
반응형