diff --git a/__tests__/address.spec.ts b/__tests__/address.spec.ts index d101fe1..82c7aa9 100644 --- a/__tests__/address.spec.ts +++ b/__tests__/address.spec.ts @@ -5,6 +5,75 @@ import { basePath, web } from "../src/application/web"; import { Address, Contact } from "@prisma/client"; import logger from "../src/application/logger"; +describe("GET /contacts/:contactId/addresses", () => { + let token: string = ""; + let contact: Contact = {} as Contact; + let address: Address = {} as Address; + + beforeAll(async () => { + token = await AddressTestUtil.getToken(); + contact = await AddressTestUtil.createContact(); + address = await AddressTestUtil.createAddress(contact.id); + }); + + afterAll(async () => { + await AddressTestUtil.deleteAddress(); + await AddressTestUtil.deleteContact(); + await AddressTestUtil.deleteUser(); + }) + + it("should be able to get all address", async () => { + const res = await supertest(web) + .get(`${basePath}/contacts/${contact.id}/addresses`) + .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.page_size).toBe(10); + expect(res.body.paging.total_page).toBe(1); + expect(res.body.paging.current_page).toBe(1); + }); + + it("should be able to get all address - pagination", async () => { + const res = await supertest(web) + .get(`${basePath}/contacts/${contact.id}/addresses`) + .query({ + page: 2, + page_size: 5 + }) + .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.page_size).toBe(5); + expect(res.body.paging.total_page).toBe(1); + expect(res.body.paging.current_page).toBe(2); + }); + + it("should return 404 - not found contact", async () => { + const res = await supertest(web) + .get(`${basePath}/contacts/${1000}/addresses`) + .set('Authorization', `Bearer ${token}`); + + logger.info(res.body); + expect(res.status).toBe(404); + expect(res.body.errors).toBeDefined(); + }); + + it("should return 401 - empty authorization", async () => { + const res = await supertest(web) + .get(`${basePath}/contacts/${contact.id}/addresses`) + + logger.info(res.body); + expect(res.status).toBe(401); + expect(res.body.errors).toBeDefined(); + }); +}); + describe("POST /contacts/:contactId/addresses", () => { let token: string = ""; let contact: Contact = {} as Contact; diff --git a/docker-compose.yml b/docker-compose.yml index 8987e6d..a05fa83 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,23 +13,23 @@ services: - POSTGRES_PASSWORD=${DB_PASSWORD} networks: - contact_api_network - # contact-api: - # environment: - # - DATABASE_URL=${DATABASE_URL} - # - PORT=${PORT} - # - BASE_URL_PATH=${BASE_URL_PATH} - # - JWT_SECRET=${JWT_SECRET} - # - JWT_EXPIRED_IN_MINUTES=${JWT_EXPIRED_IN_MINUTES} - # build: . - # image: contact-api - # ports: - # - '3000:3000' - # depends_on: - # - db - # volumes: - # - .:/usr/src/node-app - # networks: - # - contact_api_network + contact-api: + environment: + - DATABASE_URL=${DATABASE_URL} + - PORT=${PORT} + - BASE_URL_PATH=${BASE_URL_PATH} + - JWT_SECRET=${JWT_SECRET} + - JWT_EXPIRED_IN_MINUTES=${JWT_EXPIRED_IN_MINUTES} + build: . + image: contact-api + ports: + - '3000:3000' + depends_on: + - db + volumes: + - .:/usr/src/node-app + networks: + - contact_api_network volumes: postgres-db: diff --git a/src/controller/address.ts b/src/controller/address.ts index 8e6b8de..635e431 100644 --- a/src/controller/address.ts +++ b/src/controller/address.ts @@ -5,6 +5,25 @@ import { ResponseError } from "../error/response"; export class AddressController { + static async getAll(req: Request, res: Response, next: NextFunction) { + try { + if (isNaN(Number(req.params.contactId)) || Number(req.params.contactId) < 1) { + throw new ResponseError(404, "contact doesn't exists"); + } + + const getReq = { + page: req.query.page ? Number(req.query.page) : 1, + page_size: req.query.page_size ? Number(req.query.page_size) : 10 + }; + const getRes = await AddressService.findAll(res.locals.user.usename, Number(req.params.contactId), getReq); + + res.status(200) + .json(getRes); + } catch (e) { + next(e); + } + } + static async create(req: Request, res: Response, next: NextFunction) { try { if (isNaN(Number(req.params.contactId)) || Number(req.params.contactId) < 1) { @@ -12,9 +31,9 @@ export class AddressController { } const createReq = req.body as AddressRequest; - createReq.contact_id = Number(req.params.contactId); + const contactId = Number(req.params.contactId); - const createRes = await AddressService.create(res.locals.user.usename, createReq); + const createRes = await AddressService.create(res.locals.user.usename, contactId, createReq); return res.status(200) .json({ diff --git a/src/model/address.ts b/src/model/address.ts index fa17e56..71aae0a 100644 --- a/src/model/address.ts +++ b/src/model/address.ts @@ -1,12 +1,16 @@ import { Address } from "@prisma/client"; +export interface AddressListRequest { + page: number, + page_size: number +} + export interface AddressRequest { street?: string, city?: string, province?: string, country: string, - postal_code: string, - contact_id: number + postal_code: string } export interface AddressResponse { diff --git a/src/router/address.ts b/src/router/address.ts index d20dade..679def1 100644 --- a/src/router/address.ts +++ b/src/router/address.ts @@ -5,6 +5,7 @@ import { AddressController } from "../controller/address"; export const getAddressRouter = (basePath: string) => { const addressRouter = express.Router(); addressRouter.use(accessValidation); + addressRouter.get(`${basePath}/contacts/:contactId/addresses`, AddressController.getAll); addressRouter.post(`${basePath}/contacts/:contactId/addresses`, AddressController.create); return addressRouter; diff --git a/src/service/address.ts b/src/service/address.ts index 752c4d9..2251ee9 100644 --- a/src/service/address.ts +++ b/src/service/address.ts @@ -1,19 +1,55 @@ import prisma from "../application/database"; import { ResponseError } from "../error/response"; -import { AddressRequest, toAddressResponse } from "../model/address"; +import { AddressListRequest, AddressRequest, AddressResponse, toAddressResponse } from "../model/address"; +import { Pageable } from "../model/page"; import { AddressValidation } from "../validation/address"; import { Validation } from "../validation/validation"; export default class AddressService { - // static async + static async findAll(username: string, contactId: number, req: AddressListRequest): Promise> { + const validatedReq = Validation.validate(AddressValidation.LIST, req); - static async create(username: string, req: AddressRequest) { + const isContactExists = await prisma.contact.count({ + where: { + id: contactId, + username: username + } + }) === 1; + if (!isContactExists) { + throw new ResponseError(404, "contact doesn't exists"); + } + + const skip = (validatedReq.page - 1) * validatedReq.page_size; + const addresses = await prisma.address.findMany({ + where: { + contact_id: contactId, + }, + take: validatedReq.page_size, + skip: skip + }); + const total = await prisma.address.count({ + where: { + contact_id: contactId + } + }); + + return { + data: addresses.map(a => toAddressResponse(a)), + paging: { + current_page: validatedReq.page, + total_page: Math.ceil(total / validatedReq.page_size), + page_size: validatedReq.page_size + } + } + } + + static async create(username: string, contactId: number, req: AddressRequest) { const validateReq = Validation.validate(AddressValidation.SAVE, req); const isContactExists = await prisma.contact.count({ where: { - id: validateReq.contact_id, + id: contactId, username: username } }) === 1; @@ -22,7 +58,10 @@ export default class AddressService { } const address = await prisma.address.create({ - data: validateReq + data: { + ...validateReq, + contact_id: contactId + } }); return toAddressResponse(address); diff --git a/src/validation/address.ts b/src/validation/address.ts index 15dfeea..8235238 100644 --- a/src/validation/address.ts +++ b/src/validation/address.ts @@ -2,6 +2,21 @@ import { z, ZodType } from "zod"; export class AddressValidation { + static readonly LIST: ZodType = z.object({ + 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(), + }); + static readonly SAVE: ZodType = z.object({ street: z.string({ invalid_type_error: "street should be string" @@ -39,13 +54,6 @@ export class AddressValidation { message: "postal code length should more than or equal 5 characters" }).max(20, { message: "postal code length should less than 20 characters" - }), - contact_id: z.number({ - invalid_type_error: "contact id should be a number" - }).min(1, { - message: "contact should more than 1" - }).positive({ - message: "contact should be a positive number" - }), + }) }); } \ No newline at end of file diff --git a/src/validation/contact.ts b/src/validation/contact.ts index 7c5801c..9b1df9b 100644 --- a/src/validation/contact.ts +++ b/src/validation/contact.ts @@ -19,7 +19,7 @@ export class ContactValidation { message: "email length should more than 1 character" }).optional(), page: z.number({ - invalid_type_error: "page should be a number" + invalid_type_error: "page should be a number" }).min(1, { message: "page should be at least equal to 1" }).positive(),