From a01fbe9889e86fc214307eb47c4d4d51fd327b63 Mon Sep 17 00:00:00 2001 From: n9mi Date: Mon, 16 Sep 2024 11:11:29 +0700 Subject: [PATCH] feat: find all and search contacts --- __tests__/contact.spec.ts | 136 +++++++++++++++++++++++++++++++++++--- src/controller/contact.ts | 20 +++++- src/model/contact.ts | 8 +++ src/model/page.ts | 10 +++ src/router/contact.ts | 5 +- src/service/contact.ts | 84 ++++++++++++++++++++++- src/validation/contact.ts | 32 ++++++++- 7 files changed, 279 insertions(+), 16 deletions(-) create mode 100644 src/model/page.ts diff --git a/__tests__/contact.spec.ts b/__tests__/contact.spec.ts index 158d40c..5f15393 100644 --- a/__tests__/contact.spec.ts +++ b/__tests__/contact.spec.ts @@ -5,7 +5,121 @@ import { basePath, web } from "../src/application/web"; import logger from "../src/application/logger"; import { Contact } from "@prisma/client"; -describe("GET /contact:id", () => { +describe("GET /contacts", () => { + let token: string = ""; + let createdContact: Contact = {} as Contact; + + beforeAll(async () => { + token = await ContactTestUtil.getToken(); + createdContact = await ContactTestUtil.createContact(); + }); + + afterAll(async () => { + await ContactTestUtil.deleteContact(); + await ContactTestUtil.deleteUser(); + }); + + it("should be able to get all contact", async () => { + const res = await supertest(web) + .get(`${basePath}/contacts`) + .set('Authorization', `Bearer ${token}`); + + logger.info(res.body); + expect(res.status).toBe(200); + expect(res.body.data).toBeInstanceOf(Array); + expect(res.body.data.length).toBe(1); + expect(res.body.paging.current_page).toBe(1); + expect(res.body.paging.total_page).toBe(1); + expect(res.body.paging.page_size).toBe(10); + }); + + it("should be able to search contacts by name - found", async () => { + const res = await supertest(web) + .get(`${basePath}/contacts`) + .query({ + name: "ast" + }) + .set('Authorization', `Bearer ${token}`); + + logger.info(res.body); + expect(res.status).toBe(200); + expect(res.body.data).toBeInstanceOf(Array); + expect(res.body.data.length).toBe(1); + expect(res.body.paging.current_page).toBe(1); + expect(res.body.paging.total_page).toBe(1); + expect(res.body.paging.page_size).toBe(10); + }); + + it("should be able to search contacts by name - not found", async () => { + const res = await supertest(web) + .get(`${basePath}/contacts`) + .query({ + name: "random" + }) + .set('Authorization', `Bearer ${token}`); + + logger.info(res.body); + expect(res.status).toBe(200); + expect(res.body.data).toBeInstanceOf(Array); + expect(res.body.data.length).toBe(0); + expect(res.body.paging.current_page).toBe(1); + expect(res.body.paging.total_page).toBe(0); + expect(res.body.paging.page_size).toBe(10); + }); + + it("should be able to search contacts by email - found", async () => { + const res = await supertest(web) + .get(`${basePath}/contacts`) + .query({ + email: "t.co" + }) + .set('Authorization', `Bearer ${token}`); + + logger.info(res.body); + expect(res.status).toBe(200); + expect(res.body.data).toBeInstanceOf(Array); + expect(res.body.data.length).toBe(1); + expect(res.body.paging.current_page).toBe(1); + expect(res.body.paging.total_page).toBe(1); + expect(res.body.paging.page_size).toBe(10); + }); + + it("should be able to search contacts by phone - found", async () => { + const res = await supertest(web) + .get(`${basePath}/contacts`) + .query({ + phone: "081" + }) + .set('Authorization', `Bearer ${token}`); + + logger.info(res.body); + expect(res.status).toBe(200); + expect(res.body.data).toBeInstanceOf(Array); + expect(res.body.data.length).toBe(1); + expect(res.body.paging.current_page).toBe(1); + expect(res.body.paging.total_page).toBe(1); + expect(res.body.paging.page_size).toBe(10); + }); + + it("should be able to search contacts by phone - not found", async () => { + const res = await supertest(web) + .get(`${basePath}/contacts`) + .query({ + phone: "99" + }) + .set('Authorization', `Bearer ${token}`); + + logger.info(res.body); + expect(res.status).toBe(200); + expect(res.body.data).toBeInstanceOf(Array); + expect(res.body.data.length).toBe(0); + expect(res.body.paging.current_page).toBe(1); + expect(res.body.paging.total_page).toBe(0); + expect(res.body.paging.page_size).toBe(10); + }); +}); + +describe("GET /contacts:id", () => { let token: string = ""; let createdContact: Contact = {} as Contact; @@ -21,7 +135,7 @@ describe("GET /contact:id", () => { it ("should return 200 - success getting a contact", async () => { const res = await supertest(web) - .get(`${basePath}/contact/${createdContact.id}`) + .get(`${basePath}/contacts/${createdContact.id}`) .set('Authorization', `Bearer ${token}`); logger.info(res.body); @@ -35,7 +149,7 @@ describe("GET /contact:id", () => { it("should return 404 - invalid contact id", async () => { const res = await supertest(web) - .get(`${basePath}/contact/test123`) + .get(`${basePath}/contacts/test123`) .set('Authorization', `Bearer ${token}`); logger.info(res.body); @@ -45,7 +159,7 @@ describe("GET /contact:id", () => { it("should return 404 - contact id doesn't exists", async () => { const res = await supertest(web) - .get(`${basePath}/contact/10000`) + .get(`${basePath}/contacts/10000`) .set('Authorization', `Bearer ${token}`); logger.info(res.body); @@ -55,7 +169,7 @@ describe("GET /contact:id", () => { it("should return 401 - empty authorization", async () => { const res = await supertest(web) - .get(`${basePath}/contact/${createdContact.id}`); + .get(`${basePath}/contacts/${createdContact.id}`); logger.info(res.body); expect(res.status).toBe(401); @@ -63,7 +177,7 @@ describe("GET /contact:id", () => { }); }); -describe("POST /contact", () => { +describe("POST /contacts", () => { let token: string = ""; beforeAll(async () => { @@ -77,7 +191,7 @@ describe("POST /contact", () => { it("should return 200 - success creating contact", async () => { const res = await supertest(web) - .post(`${basePath}/contact`) + .post(`${basePath}/contacts`) .send(ContactTestUtil.contact) .set('Authorization', `Bearer ${token}`); @@ -92,7 +206,7 @@ describe("POST /contact", () => { it("should return 200 - success creating contact with emty last_name, email, and phone", async () => { const res = await supertest(web) - .post(`${basePath}/contact`) + .post(`${basePath}/contacts`) .send({ first_name: ContactTestUtil.contact.first_name }) @@ -109,7 +223,7 @@ describe("POST /contact", () => { it("should return 400 - bad request invalid email", async () => { const res = await supertest(web) - .post(`${basePath}/contact`) + .post(`${basePath}/contacts`) .send({ first_name: ContactTestUtil.contact.first_name, last_name: ContactTestUtil.contact.last_name, @@ -124,7 +238,7 @@ describe("POST /contact", () => { it("should return 400 - bad request phone number more than 20 characters", async () => { const res = await supertest(web) - .post(`${basePath}/contact`) + .post(`${basePath}/contacts`) .send({ first_name: ContactTestUtil.contact.first_name, last_name: ContactTestUtil.contact.last_name, @@ -139,7 +253,7 @@ describe("POST /contact", () => { it("should return 401 - empty authorization", async () => { const res = await supertest(web) - .post(`${basePath}/contact`) + .post(`${basePath}/contacts`) .send({ first_name: ContactTestUtil.contact.first_name }); diff --git a/src/controller/contact.ts b/src/controller/contact.ts index 8c23b44..2b6acb1 100644 --- a/src/controller/contact.ts +++ b/src/controller/contact.ts @@ -1,10 +1,28 @@ import { Request, Response, NextFunction } from "express"; import ContactService from "../service/contact"; -import { ContactRequest } from "../model/contact"; +import { ContactRequest, ContactSearchRequest } from "../model/contact"; import { ResponseError } from "../error/response"; export class ContactController { + static async getAll(req: Request, res: Response, next: NextFunction) { + try { + const getReq: ContactSearchRequest = { + name: req.query.name as string, + email: req.query.email as string, + phone: req.query.phone as string, + page: req.query.page ? Number(req.query.page) : 1, + page_size: req.query.page_size ? Number(req.query.page_size) : 10 + }; + const getRes = await ContactService.findAll(res.locals.user.username, getReq); + + res.status(200) + .json(getRes); + } catch (e) { + next(e); + } + } + static async getById(req: Request, res: Response, next: NextFunction) { try { if (isNaN(Number(req.params.id))) { diff --git a/src/model/contact.ts b/src/model/contact.ts index e7b81f3..e63bbc2 100644 --- a/src/model/contact.ts +++ b/src/model/contact.ts @@ -1,5 +1,13 @@ import { Contact } from "@prisma/client" +export interface ContactSearchRequest { + name?: string, + email?: string, + phone?: string, + page: number, + page_size: number +} + export interface ContactRequest { first_name: string, last_name: string, diff --git a/src/model/page.ts b/src/model/page.ts new file mode 100644 index 0000000..96fdad3 --- /dev/null +++ b/src/model/page.ts @@ -0,0 +1,10 @@ +export interface Paging { + page_size: number, + total_page: number, + current_page: number +} + +export interface Pageable { + data: Array + paging: Paging +} \ No newline at end of file diff --git a/src/router/contact.ts b/src/router/contact.ts index 6eebdbd..32326ea 100644 --- a/src/router/contact.ts +++ b/src/router/contact.ts @@ -5,8 +5,9 @@ import { accessValidation } from "../middleware/accessValidation"; export const getContactRouter = (basePath: string) => { const contactRouter = express.Router(); contactRouter.use(accessValidation); - contactRouter.get(`${basePath}/contact/:id`, ContactController.getById); - contactRouter.post(`${basePath}/contact`, ContactController.create); + contactRouter.get(`${basePath}/contacts`, ContactController.getAll); + contactRouter.get(`${basePath}/contacts/:id`, ContactController.getById); + contactRouter.post(`${basePath}/contacts`, ContactController.create); return contactRouter; } \ No newline at end of file diff --git a/src/service/contact.ts b/src/service/contact.ts index 1d7380e..68ecbf3 100644 --- a/src/service/contact.ts +++ b/src/service/contact.ts @@ -1,11 +1,93 @@ +import { Contact } from "@prisma/client"; import prisma from "../application/database"; import { ResponseError } from "../error/response"; -import { ContactRequest, ContactResponse, toContactResponse } from "../model/contact"; +import { ContactRequest, ContactResponse, ContactSearchRequest, toContactResponse } from "../model/contact"; +import { Pageable } from "../model/page"; import { ContactValidation } from "../validation/contact"; import { Validation } from "../validation/validation"; export default class ContactService { + static async findAll(username: string, req: ContactSearchRequest): Promise> { + const validatedReq = Validation.validate(ContactValidation.SEARCH, req); + const skip = (validatedReq.page - 1) * validatedReq.page_size; + + const filters = []; + if (validatedReq.name) { + filters.push({ + OR: [ + { + first_name: { + contains: validatedReq.name, + // mode: "insensitive", + } + }, + { + last_name: { + contains: validatedReq.name, + // mode: "insensitive" + } + } + ] + }) + } + if (validatedReq.email) { + filters.push({ + email: { + contains: validatedReq.email + } + }); + } + if (validatedReq.phone) { + filters.push({ + phone: { + contains: validatedReq.phone + } + }); + } + + let contacts: Contact[] = []; + let total: number = 0; + if (filters.length > 0) { + contacts = await prisma.contact.findMany({ + where: { + username: username, + AND: filters + }, + take: validatedReq.page_size, + skip: skip + }); + total = await prisma.contact.count({ + where: { + username: username, + AND: filters + }, + }); + } else { + contacts = await prisma.contact.findMany({ + where: { + username: username + }, + take: validatedReq.page_size, + skip: skip + }); + total = await prisma.contact.count({ + where: { + username: username + }, + }); + } + + return { + data: contacts.map(c => toContactResponse(c)), + paging: { + current_page: validatedReq.page, + total_page: Math.ceil(total / validatedReq.page_size), + page_size: validatedReq.page_size + } + }; + } + static async findById(username: string, id: number): Promise { const contact = await prisma.contact.findFirstOrThrow({ where: { diff --git a/src/validation/contact.ts b/src/validation/contact.ts index bb9f942..2f7fb40 100644 --- a/src/validation/contact.ts +++ b/src/validation/contact.ts @@ -33,5 +33,35 @@ export class ContactValidation { }).max(20, { message: "phone length should less than 20 character" }).optional(), - }) + }); + + static readonly SEARCH: ZodType = z.object({ + name: z.string({ + invalid_type_error: "first name should be string" + }).min(1, { + message: "first name length should more than 1 character" + }).optional(), + phone: z.string({ + invalid_type_error: "phone should be string" + }).min(1, { + message: "phone length should more than 1 character" + }).optional(), + email: z.string({ + invalid_type_error: "email should be string" + }).min(1, { + message: "email length should more than 1 character" + }).optional(), + page: z.number({ + invalid_type_error: "page should be a number" + }).min(1, { + message: "page should be at least equal to 1" + }).positive(), + page_size: z.number({ + invalid_type_error: "page_size should be a number" + }).min(1, { + message: "page should be at least equal to 1" + }).max(100, { + message: "page_size can't be more than 100" + }).positive(), + }); } \ No newline at end of file