From d49b988a6cd5abc3a5867e9bdc273f4f51c8f077 Mon Sep 17 00:00:00 2001 From: arden-dave Date: Fri, 8 Sep 2023 17:59:26 +0800 Subject: [PATCH] wip: unit tests --- server/package.json | 1 + .../src/api/job/dtos/person-in-charge.dto.ts | 26 ++ server/src/api/job/job.controller.spec.ts | 182 +++++++++---- server/src/api/job/job.controller.ts | 109 +++++--- server/src/api/job/job.module.ts | 14 +- server/src/api/job/job.service.spec.ts | 243 ++++++++++++------ server/src/api/job/job.service.ts | 239 ++++++++++------- .../api/schedule/dtos/calendar-events.dto.ts | 28 ++ .../src/api/schedule/dtos/get-schedule.dto.ts | 21 ++ .../src/api/schedule/schedule.controller.ts | 111 +++++--- server/src/api/schedule/schedule.module.ts | 12 +- .../src/api/schedule/schedule.service.spec.ts | 116 +++++++-- server/src/api/schedule/schedule.service.ts | 121 ++++++++- server/src/api/user/dtos/user.dto.ts | 34 +++ server/src/main.ts | 27 +- server/src/models/seed.ts | 30 ++- server/src/models/seeders/customer.seed.ts | 20 ++ server/src/models/seeders/job.seed.ts | 20 ++ server/src/models/seeders/schedule.seed.ts | 46 ++++ server/yarn.lock | 5 + 20 files changed, 1034 insertions(+), 371 deletions(-) create mode 100644 server/src/api/job/dtos/person-in-charge.dto.ts create mode 100644 server/src/api/schedule/dtos/calendar-events.dto.ts create mode 100644 server/src/api/schedule/dtos/get-schedule.dto.ts create mode 100644 server/src/api/user/dtos/user.dto.ts create mode 100644 server/src/models/seeders/customer.seed.ts create mode 100644 server/src/models/seeders/job.seed.ts create mode 100644 server/src/models/seeders/schedule.seed.ts diff --git a/server/package.json b/server/package.json index 8ff0f0c..d8eadf1 100644 --- a/server/package.json +++ b/server/package.json @@ -28,6 +28,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "jest-junit": "^16.0.0", + "moment": "^2.29.4", "nestjs-seeder": "^0.3.2", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1" diff --git a/server/src/api/job/dtos/person-in-charge.dto.ts b/server/src/api/job/dtos/person-in-charge.dto.ts new file mode 100644 index 0000000..a9d7958 --- /dev/null +++ b/server/src/api/job/dtos/person-in-charge.dto.ts @@ -0,0 +1,26 @@ +import { OmitType } from "@nestjs/swagger"; +import { IsString } from "class-validator"; +import { UserDto } from "../../user/dtos/user.dto"; +import { Exclude, Expose } from "class-transformer"; + +export class PersonInChargeDto extends OmitType(UserDto, [ + "createdAt", + "updatedAt", +]) { + @Exclude() + createdAt: Date; + + @Exclude() + updatedAt: Date; + + @Expose() + @IsString() + get fullname(): string | undefined { + return `${this.firstName} ${this.lastName}`; + } + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/server/src/api/job/job.controller.spec.ts b/server/src/api/job/job.controller.spec.ts index f45dda2..7db28f0 100644 --- a/server/src/api/job/job.controller.spec.ts +++ b/server/src/api/job/job.controller.spec.ts @@ -1,24 +1,45 @@ -import { $Enums } from '@prisma/client'; +import { $Enums } from "@prisma/client"; -import { PrismaService } from '../../database/connection.service'; +import { PrismaService } from "../../database/connection.service"; -import { JobService } from './job.service'; -import { JobController } from './job.controller'; -import { CustomerService } from '../customer/customer.service'; +import { JobService } from "./job.service"; +import { JobController } from "./job.controller"; +import { CustomerService } from "../customer/customer.service"; +import { Test } from "@nestjs/testing"; +import { NotFoundException } from "@nestjs/common"; +import { PersonInChargeDto } from "./dtos/person-in-charge.dto"; -describe('JobController', () => { +describe("JobController", () => { let jobService: JobService; let jobController: JobController; - let customerService: CustomerService - let prisma: PrismaService - - beforeEach(() => { - jobService = new JobService(prisma, customerService) - jobController = new JobController(jobService) + let prisma: PrismaService; + + const mockPrisma = { + job: { + groupBy: jest.fn(), + }, + user: { + findMany: jest.fn(), + }, + }; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + controllers: [JobController], + providers: [ + JobService, + CustomerService, + { provide: PrismaService, useValue: mockPrisma }, + ], + }).compile(); + + jobController = moduleRef.get(JobController); + jobService = moduleRef.get(JobService); + prisma = moduleRef.get(PrismaService); }); - describe('create', () => { - it('should return a new job', async () => { + describe("create", () => { + it("should return a new job", async () => { const jobInput = { customer_registration: { firstName: "John", @@ -34,8 +55,8 @@ describe('JobController', () => { remarks: "string", paymentMethod: $Enums.PaymentMethod.CASH, }, - work_schedules: [] - } + work_schedules: [], + }; const newJob = { id: 1, @@ -46,50 +67,57 @@ describe('JobController', () => { customerId: 1, paymentMethod: $Enums.PaymentMethod.CASH, userId: 1, - } + }; + + const mockCreateJobWithCustomerAndSchedules = jest + .fn() + .mockResolvedValue(newJob); - const mockCreateJobWithCustomerAndSchedules = jest.fn().mockResolvedValue(newJob) - - jest.spyOn(jobService, 'createJobWithCustomerAndSchedules').mockImplementation(mockCreateJobWithCustomerAndSchedules) - - const createdJob = await jobController.create(jobInput) + jest + .spyOn(jobService, "createJobWithCustomerAndSchedules") + .mockImplementation(mockCreateJobWithCustomerAndSchedules); + + const createdJob = await jobController.create(jobInput); expect(createdJob).toEqual(newJob); - }) - }) + }); + }); - describe('findAll', () => { - it('should return an array of jobs', async () => { - const jobs = [{ - id: 1, - title: "Sample title 1", - type: "A", - tags: [$Enums.Tag.TAG_A], - remarks: "", - customerId: 1, - paymentMethod: "CARD", - userId: 1, - }, { - id: 2, - title: "Sample title 2", - type: "A", - tags: [$Enums.Tag.TAG_A], - remarks: "", - customerId: 1, - paymentMethod: "CARD", - userId: 1, - }]; + describe("findAll", () => { + it("should return an array of jobs", async () => { + const jobs = [ + { + id: 1, + title: "Sample title 1", + type: "A", + tags: [$Enums.Tag.TAG_A], + remarks: "", + customerId: 1, + paymentMethod: "CARD", + userId: 1, + }, + { + id: 2, + title: "Sample title 2", + type: "A", + tags: [$Enums.Tag.TAG_A], + remarks: "", + customerId: 1, + paymentMethod: "CARD", + userId: 1, + }, + ]; - jest.spyOn(jobService, 'findAll').mockResolvedValue(jobs) + jest.spyOn(jobService, "findAll").mockResolvedValue(jobs); const foundJobs = await jobController.findAll(); expect(foundJobs).toEqual(jobs); - }) - }) + }); + }); - describe('findOne', () => { - it('should return a single job', async () => { + describe("findOne", () => { + it("should return a single job", async () => { const job = { id: 1, title: "Sample title 1", @@ -99,13 +127,59 @@ describe('JobController', () => { customerId: 1, paymentMethod: "CARD", userId: 1, - } + }; - jest.spyOn(jobService, 'findOne').mockResolvedValue(job); + jest.spyOn(jobService, "findOne").mockResolvedValue(job); const foundJob = await jobController.findOne(1); expect(foundJob).toEqual(job); - }) - }) + }); + }); + + describe("findAllTypes", () => { + const mockValues = [ + { type: "developer" }, + { type: "writer" }, + { type: "designer" }, + ]; + + it("should return string array of types", async () => { + prisma.job.groupBy = jest.fn().mockResolvedValue(mockValues); + + const expectedReturn = ["developer", "writer", "designer"]; + const returnedTypes = await jobController.findAllTypes(); + expect(returnedTypes).toEqual(expectedReturn); + }); + + it("should throw error", async () => { + jobService.findAllTypes = jest.fn().mockResolvedValue([]); + expect(async () => jobController.findAllTypes()).rejects.toThrow( + NotFoundException, + ); + }); + }); + + describe("findAllPersonInCharge", () => { + const mockReturnValues = [ + { id: 1, name: "john" }, + { id: 2, name: "doe" }, + { id: 3, name: "jane" }, + ]; + + it("should return array of PersonInChargeDto", async () => { + prisma.user.findMany = jest.fn().mockResolvedValue(mockReturnValues); + + const result = await jobController.findAllPersonInCharge(); + + expect(result).toBeInstanceOf(Array); + }); + + it("should throw error", async () => { + jobService.findAllPersonInCharge = jest.fn().mockResolvedValue([]); + expect(async () => jobController.findAllPersonInCharge()).rejects.toThrow( + NotFoundException, + ); + }); + }); }); diff --git a/server/src/api/job/job.controller.ts b/server/src/api/job/job.controller.ts index 04d1a9c..e65adbd 100644 --- a/server/src/api/job/job.controller.ts +++ b/server/src/api/job/job.controller.ts @@ -1,48 +1,87 @@ -import { Job } from '@prisma/client'; -import { ApiTags } from '@nestjs/swagger'; -import { BadRequestException, Body, Controller, Get, NotFoundException, Param, ParseIntPipe, Post, UsePipes, ValidationPipe } from '@nestjs/common'; +import { Job } from "@prisma/client"; +import { ApiTags } from "@nestjs/swagger"; +import { + BadRequestException, + Body, + ClassSerializerInterceptor, + Controller, + Get, + NotFoundException, + Param, + ParseIntPipe, + Post, + UseInterceptors, + UsePipes, + ValidationPipe, +} from "@nestjs/common"; -import { JobService } from './job.service'; +import { JobService } from "./job.service"; -import { CreateJobWithCustomerAndSchedulesDto } from './dtos/create-job-with-customer-and-schedule.dto'; +import { CreateJobWithCustomerAndSchedulesDto } from "./dtos/create-job-with-customer-and-schedule.dto"; +import { PersonInChargeDto } from "./dtos/person-in-charge.dto"; -@ApiTags('job') -@Controller('job') +@ApiTags("job") +@Controller("job") export class JobController { - constructor( - private readonly jobService: JobService - ) { } - - @Post() - @UsePipes(new ValidationPipe()) - async create(@Body() options: CreateJobWithCustomerAndSchedulesDto): Promise { - const job = await this.jobService.createJobWithCustomerAndSchedules(options); - - if (!job ) { - throw new BadRequestException("Something went wrong. Job not created.") - } - - return job; + constructor(private readonly jobService: JobService) {} + + @Get("types") + async findAllTypes(): Promise { + const types = await this.jobService.findAllTypes(); + + if (types.length === 0) { + throw new NotFoundException("No job types found."); } - @Get() - async findAll(): Promise { - const jobs = await this.jobService.findAll() + return types; + } + + @Get("personInCharge") + @UseInterceptors(ClassSerializerInterceptor) + async findAllPersonInCharge(): Promise { + const person = await this.jobService.findAllPersonInCharge(); - if (jobs == 0) { - throw new NotFoundException("No job found.") - } - return jobs + if (person.length === 0) { + throw new NotFoundException("No person in charge found."); } - @Get('/:id') - async findOne(@Param('id', ParseIntPipe) id: number): Promise { - const job = await this.jobService.findOne({ id }) + return person.map((person) => new PersonInChargeDto({ ...person })); + } - if (!job) { - throw new NotFoundException("No job found.") - } + @Post() + @UsePipes(new ValidationPipe()) + async create( + @Body() options: CreateJobWithCustomerAndSchedulesDto, + ): Promise { + const job = await this.jobService.createJobWithCustomerAndSchedules( + options, + ); - return job + if (!job) { + throw new BadRequestException("Something went wrong. Job not created."); } + + return job; + } + + @Get() + async findAll(): Promise { + const jobs = await this.jobService.findAll(); + + if (jobs == 0) { + throw new NotFoundException("No job found."); + } + return jobs; + } + + @Get("/:id") + async findOne(@Param("id", ParseIntPipe) id: number): Promise { + const job = await this.jobService.findOne({ id }); + + if (!job) { + throw new NotFoundException("No job found."); + } + + return job; + } } diff --git a/server/src/api/job/job.module.ts b/server/src/api/job/job.module.ts index f030f75..ae97dd9 100644 --- a/server/src/api/job/job.module.ts +++ b/server/src/api/job/job.module.ts @@ -1,15 +1,15 @@ -import { Module } from '@nestjs/common'; +import { Module } from "@nestjs/common"; -import { PrismaService } from '../../database/connection.service'; +import { PrismaService } from "../../database/connection.service"; -import { CustomerService } from '../customer/customer.service'; -import { ScheduleService } from '../schedule/schedule.service'; -import { JobService } from './job.service'; -import { JobController } from './job.controller'; +import { CustomerService } from "../customer/customer.service"; +import { ScheduleService } from "../schedule/schedule.service"; +import { JobService } from "./job.service"; +import { JobController } from "./job.controller"; @Module({ controllers: [JobController], providers: [JobService, PrismaService, CustomerService, ScheduleService], - exports: [], + exports: [JobService], }) export class JobModule {} diff --git a/server/src/api/job/job.service.spec.ts b/server/src/api/job/job.service.spec.ts index 97e2ac7..c9e8a77 100644 --- a/server/src/api/job/job.service.spec.ts +++ b/server/src/api/job/job.service.spec.ts @@ -1,22 +1,40 @@ -import { $Enums } from '@prisma/client'; -import { BadRequestException } from '@nestjs/common'; +import { Test } from "@nestjs/testing"; +import { $Enums } from "@prisma/client"; +import { BadRequestException } from "@nestjs/common"; -import { PrismaService } from '../../database/connection.service'; +import { PrismaService } from "../../database/connection.service"; -import { JobService } from './job.service'; -import { CustomerService } from '../customer/customer.service'; +import { JobService } from "./job.service"; +import { CustomerService } from "../customer/customer.service"; -describe('JobController', () => { +describe("JobController", () => { let jobService: JobService; - let customerService: CustomerService - let prisma: PrismaService + let prisma: PrismaService; - beforeEach(() => { - jobService = new JobService(prisma, customerService) + const mockPrisma = { + job: { + groupBy: jest.fn(), + }, + user: { + findMany: jest.fn(), + }, + }; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + JobService, + CustomerService, + { provide: PrismaService, useValue: mockPrisma }, + ], + }).compile(); + + jobService = moduleRef.get(JobService); + prisma = moduleRef.get(PrismaService); }); - describe('createJobWithCustomerAndSchedules', () => { - it('should return a new job', async () => { + describe("createJobWithCustomerAndSchedules", () => { + it("should return a new job", async () => { const date = new Date(); const jobInput = { customer_registration: { @@ -33,8 +51,8 @@ describe('JobController', () => { remarks: "string", paymentMethod: $Enums.PaymentMethod.CASH, }, - work_schedules: [] - } + work_schedules: [], + }; const newJob = { id: 1, @@ -46,21 +64,27 @@ describe('JobController', () => { paymentMethod: $Enums.PaymentMethod.CASH, userId: 1, createdAt: date, - updatedAt: date - } + updatedAt: date, + }; - const mockCreateJobWithCustomerAndSchedules = jest.fn().mockResolvedValue(newJob) + const mockCreateJobWithCustomerAndSchedules = jest + .fn() + .mockResolvedValue(newJob); - jest.spyOn(jobService, 'createJobWithCustomerAndSchedules').mockImplementation(mockCreateJobWithCustomerAndSchedules) + jest + .spyOn(jobService, "createJobWithCustomerAndSchedules") + .mockImplementation(mockCreateJobWithCustomerAndSchedules); - const createdJob = await jobService.createJobWithCustomerAndSchedules(jobInput) + const createdJob = await jobService.createJobWithCustomerAndSchedules( + jobInput, + ); expect(createdJob).toEqual(newJob); - }) - }) + }); + }); - describe('findOne', () => { - it('should return a single job', async () => { + describe("findOne", () => { + it("should return a single job", async () => { const date = new Date(); const job = { id: 1, @@ -72,54 +96,57 @@ describe('JobController', () => { paymentMethod: $Enums.PaymentMethod.CASH, userId: 1, createdAt: date, - updatedAt: date - } + updatedAt: date, + }; - jest.spyOn(jobService, 'findOne').mockResolvedValue(job); + jest.spyOn(jobService, "findOne").mockResolvedValue(job); const foundJob = await jobService.findOne(1); expect(foundJob).toEqual(job); - }) - }) + }); + }); - describe('findAll', () => { - it('should return an array of jobs', async () => { + describe("findAll", () => { + it("should return an array of jobs", async () => { const date = new Date(); - const jobs = [{ - id: 1, - title: "Sample title 1", - type: "A", - tags: [$Enums.Tag.TAG_A], - remarks: "", - customerId: 1, - paymentMethod: $Enums.PaymentMethod.CASH, - userId: 1, - createdAt: date, - updatedAt: date - }, { - id: 2, - title: "Sample title 2", - type: "A", - tags: [$Enums.Tag.TAG_A], - remarks: "", - customerId: 1, - paymentMethod: $Enums.PaymentMethod.CARD, - userId: 1, - createdAt: date, - updatedAt: date - }]; + const jobs = [ + { + id: 1, + title: "Sample title 1", + type: "A", + tags: [$Enums.Tag.TAG_A], + remarks: "", + customerId: 1, + paymentMethod: $Enums.PaymentMethod.CASH, + userId: 1, + createdAt: date, + updatedAt: date, + }, + { + id: 2, + title: "Sample title 2", + type: "A", + tags: [$Enums.Tag.TAG_A], + remarks: "", + customerId: 1, + paymentMethod: $Enums.PaymentMethod.CARD, + userId: 1, + createdAt: date, + updatedAt: date, + }, + ]; - jest.spyOn(jobService, 'findAll').mockResolvedValue(jobs) + jest.spyOn(jobService, "findAll").mockResolvedValue(jobs); const foundJobs = await jobService.findAll(); expect(foundJobs).toEqual(jobs); - }) - }) + }); + }); - describe('createJob', () => { - it('should return a new job', async () => { + describe("createJob", () => { + it("should return a new job", async () => { const date = new Date(); const jobInput = { title: "Sample title 1", @@ -129,7 +156,7 @@ describe('JobController', () => { paymentMethod: $Enums.PaymentMethod.CASH, customerId: 1, userId: 1, - } + }; const newJob = { id: 1, @@ -141,41 +168,91 @@ describe('JobController', () => { customerId: 1, userId: 1, createdAt: date, - updatedAt: date - } + updatedAt: date, + }; - jest.spyOn(jobService, 'createJob').mockResolvedValue(newJob) + jest.spyOn(jobService, "createJob").mockResolvedValue(newJob); - const createdJob = await jobService.createJob(jobInput) + const createdJob = await jobService.createJob(jobInput); - expect(createdJob).toEqual(newJob) - }) - }) + expect(createdJob).toEqual(newJob); + }); + }); - describe('checkIfJobAlreadyExists', () => { - it('should return return a job', async () => { + describe("checkIfJobAlreadyExists", () => { + it("should return return a job", async () => { const customerId = 1; - const validTitle = 'valid title' + const validTitle = "valid title"; - jest.spyOn(jobService, 'checkIfJobAlreadyExists').mockResolvedValue(true) + jest.spyOn(jobService, "checkIfJobAlreadyExists").mockResolvedValue(true); - const checkIfJobExists = await jobService.checkIfJobAlreadyExists(validTitle, customerId); + const checkIfJobExists = await jobService.checkIfJobAlreadyExists( + validTitle, + customerId, + ); - expect(checkIfJobExists).toEqual(true) - }) - }) + expect(checkIfJobExists).toEqual(true); + }); + }); - describe('createSchedules', () => { - it('should throw BadRequestException if schedules is an empty array', () => { - const schedules = [] + describe("createSchedules", () => { + it("should throw BadRequestException if schedules is an empty array", () => { + const schedules = []; - jest.spyOn(jobService, 'createSchedules').mockRejectedValue(new BadRequestException('Something went wrong. Schedule not created')) + jest + .spyOn(jobService, "createSchedules") + .mockRejectedValue( + new BadRequestException("Something went wrong. Schedule not created"), + ); const createSchedulesPromise = async () => { - await jobService.createSchedules(schedules, 1) - } + await jobService.createSchedules(schedules, 1); + }; + + expect(createSchedulesPromise).rejects.toThrow(BadRequestException); + }); + }); - expect(createSchedulesPromise).rejects.toThrow(BadRequestException) - }) - }) + describe("findAllTypes", () => { + const mockValues = [ + { type: "developer" }, + { type: "writer" }, + { type: "designer" }, + ]; + + it("should return string array of types", async () => { + prisma.job.groupBy = jest.fn().mockResolvedValue(mockValues); + + const expectedReturn = ["developer", "writer", "designer"]; + const returnedTypes = await jobService.findAllTypes(); + expect(returnedTypes).toEqual(expectedReturn); + }); + }); + + describe("findAllPersonInCharge", () => { + const mockJobValues = [ + { userId: 1 }, + { userId: 2 }, + { userId: 3 }, + { userId: 4 }, + { userId: 5 }, + ]; + const mockUserIdValues = [1, 2, 3, 4, 5]; + + const mockFindManyValues = { + where: { + id: { + in: mockUserIdValues, + }, + }, + }; + + it("should return ", async () => { + prisma.job.groupBy = jest.fn().mockResolvedValue(mockJobValues); + + await jobService.findAllPersonInCharge(); + + expect(prisma.user.findMany).toHaveBeenCalledWith(mockFindManyValues); + }); + }); }); diff --git a/server/src/api/job/job.service.ts b/server/src/api/job/job.service.ts index 5717d83..c2b7ac6 100644 --- a/server/src/api/job/job.service.ts +++ b/server/src/api/job/job.service.ts @@ -1,115 +1,160 @@ -import { Customer, Job } from '@prisma/client'; -import { UsePipes, ValidationPipe, BadRequestException, Injectable } from '@nestjs/common'; - -import { PrismaService } from '../../database/connection.service'; - -import { AbstractService } from '../../shared/abstract-service'; -import { CustomerService } from '../customer/customer.service'; -import { CreateJobDto } from './dtos/create-job-dto'; -import { CreateCustomerDto } from '../customer/dtos/create-customer.dto'; -import { CreateJobWithCustomerAndSchedulesDto } from './dtos/create-job-with-customer-and-schedule.dto'; -import { CreateScheduleWithoutJobIdDto } from '../schedule/dtos/create-schedule-without-job-id.dto'; +import { Customer, Job, User } from "@prisma/client"; +import { + UsePipes, + ValidationPipe, + BadRequestException, + Injectable, +} from "@nestjs/common"; + +import { PrismaService } from "../../database/connection.service"; + +import { AbstractService } from "../../shared/abstract-service"; +import { CustomerService } from "../customer/customer.service"; +import { CreateJobDto } from "./dtos/create-job-dto"; +import { CreateCustomerDto } from "../customer/dtos/create-customer.dto"; +import { CreateJobWithCustomerAndSchedulesDto } from "./dtos/create-job-with-customer-and-schedule.dto"; +import { CreateScheduleWithoutJobIdDto } from "../schedule/dtos/create-schedule-without-job-id.dto"; @Injectable() @UsePipes(new ValidationPipe()) export class JobService extends AbstractService { - constructor( - prisma: PrismaService, - private readonly customerService: CustomerService, - ) { - super(prisma, "Job") + constructor( + prisma: PrismaService, + private readonly customerService: CustomerService, + ) { + super(prisma, "Job"); + } + + async createJobWithCustomerAndSchedules( + createJobWithCustomerAndSchedulesOptions: CreateJobWithCustomerAndSchedulesDto, + ): Promise { + const { customer_registration, job_information, work_schedules } = + createJobWithCustomerAndSchedulesOptions; + + const transaction = await this.prisma.$transaction(async () => { + // Step 1: Create the customer + const customerInput = { ...customer_registration }; + const customer = await this.createCustomer(customerInput); + + // Step 2: Create the job associated with the customer + // TODO: Update userId to be authUserId + const jobInput = { + ...job_information, + customerId: customer.id, + userId: 1, + }; + const job = await this.createJob(jobInput); + + // Step 3: Create schedules for the job + await this.createSchedules(work_schedules, job.id); + + return job; + }); + + return transaction; + } + + async createJob(options: CreateJobDto): Promise { + const { title, customerId } = options; + await this.checkIfJobAlreadyExists(title, customerId); + + const newJob = await this.prisma.job.create({ + data: options, + }); + + if (!newJob) { + throw new BadRequestException("Something went wrong. job not created."); } - async createJobWithCustomerAndSchedules(createJobWithCustomerAndSchedulesOptions: CreateJobWithCustomerAndSchedulesDto): Promise { - const { - customer_registration, - job_information, - work_schedules - } = createJobWithCustomerAndSchedulesOptions; - - const transaction = await this.prisma.$transaction(async () => { - - // Step 1: Create the customer - const customerInput = { ...customer_registration } - const customer = await this.createCustomer(customerInput) - - // Step 2: Create the job associated with the customer - // TODO: Update userId to be authUserId - const jobInput = { ...job_information, customerId: customer.id, userId: 1 } - const job = await this.createJob(jobInput) - - // Step 3: Create schedules for the job - await this.createSchedules(work_schedules, job.id) - - return job - }) - - return transaction; + return newJob; + } + + async checkIfJobAlreadyExists( + title: string, + customerId: number, + ): Promise { + const jobAlreadyExists = await this.prisma.job.findFirst({ + where: { + title, + customerId, + }, + }); + + if (jobAlreadyExists) { + throw new BadRequestException( + "Job with the same title and customer already exists.", + ); } - async createJob(options: CreateJobDto): Promise { - const { title, customerId } = options - await this.checkIfJobAlreadyExists(title, customerId) + return !!jobAlreadyExists; + } - const newJob = await this.prisma.job.create({ - data: options - }) + async createCustomer(options: CreateCustomerDto): Promise { + const { email } = options; + const existingCustomer = await this.checkIfCustomerAlreadyExists(email); - if (!newJob) { - throw new BadRequestException('Something went wrong. job not created.') - } - - return newJob + if (existingCustomer) { + return existingCustomer; } - async checkIfJobAlreadyExists(title: string, customerId: number): Promise { - const jobAlreadyExists = await this.prisma.job.findFirst({ - where: { - title, - customerId, - }, - }); - - if (jobAlreadyExists) { - throw new BadRequestException('Job with the same title and customer already exists.'); - } + const newCustomer = await this.customerService.create(options); - return !!jobAlreadyExists + if (!newCustomer) { + throw new BadRequestException( + "Something went wrong. Customer not created.", + ); } - async createCustomer(options: CreateCustomerDto): Promise { - const { email } = options - const existingCustomer = await this.checkIfCustomerAlreadyExists(email) - - if (existingCustomer) { - return existingCustomer - } - - const newCustomer = await this.customerService.create(options) - - if (!newCustomer) { - throw new BadRequestException('Something went wrong. Customer not created.') - } - - return newCustomer + return newCustomer; + } + + async checkIfCustomerAlreadyExists(email: string) { + return await this.customerService.findOne({ email }); + } + + async createSchedules( + schedulesData: CreateScheduleWithoutJobIdDto[], + jobId: number, + ): Promise { + if (schedulesData.length == 0) { + throw new BadRequestException( + "Something went wrong. Schedule not created.", + ); } - async checkIfCustomerAlreadyExists(email: string) { - return await this.customerService.findOne({ email }) - } - - async createSchedules(schedulesData: CreateScheduleWithoutJobIdDto[], jobId: number): Promise { - if (schedulesData.length == 0) { - throw new BadRequestException('Something went wrong. Schedule not created.') - } - - const schedulesDataWithJobId = schedulesData.map((schedule) => ({ - ...schedule, - jobId, - })); - - await this.prisma.schedule.createMany({ - data: schedulesDataWithJobId - }) - } + const schedulesDataWithJobId = schedulesData.map((schedule) => ({ + ...schedule, + jobId, + })); + + await this.prisma.schedule.createMany({ + data: schedulesDataWithJobId, + }); + } + + async findAllTypes(): Promise { + const types = await this.prisma.job.groupBy({ + by: ["type"], + }); + + return types.map((x) => x.type); + } + + async findAllPersonInCharge(): Promise { + const userIds = await this.prisma.job.groupBy({ + by: ["userId"], + }); + + const personInChargeIds = userIds.map((x) => x.userId); + + const users = await this.prisma.user.findMany({ + where: { + id: { + in: personInChargeIds, + }, + }, + }); + + return users; + } } diff --git a/server/src/api/schedule/dtos/calendar-events.dto.ts b/server/src/api/schedule/dtos/calendar-events.dto.ts new file mode 100644 index 0000000..123d07d --- /dev/null +++ b/server/src/api/schedule/dtos/calendar-events.dto.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsDateString, IsNumber, IsString } from "class-validator"; + +export class CalendarEventDto { + @ApiProperty() + @IsString() + title: string; + + @ApiProperty() + @IsDateString() + start: string; + + @ApiProperty() + @IsDateString() + end: string; + + @ApiProperty() + @IsNumber() + scheduleId: number; + + @ApiProperty() + @IsNumber() + jobId: number; + + constructor(event: CalendarEventDto) { + Object.assign(this, event); + } +} diff --git a/server/src/api/schedule/dtos/get-schedule.dto.ts b/server/src/api/schedule/dtos/get-schedule.dto.ts new file mode 100644 index 0000000..69a5553 --- /dev/null +++ b/server/src/api/schedule/dtos/get-schedule.dto.ts @@ -0,0 +1,21 @@ +import { Transform } from "class-transformer"; +import { IsDateString, IsNumber, IsOptional, IsString } from "class-validator"; + +export class GetScheduleQueryDto { + @IsDateString() + @IsOptional() + startDate?: string; + + @IsDateString() + @IsOptional() + endDate?: string; + + @IsString() + @IsOptional() + jobType?: string; + + @Transform(({ value }) => parseInt(value), { toClassOnly: true }) + @IsNumber() + @IsOptional() + personInChargeId?: number; +} diff --git a/server/src/api/schedule/schedule.controller.ts b/server/src/api/schedule/schedule.controller.ts index bbdde3c..d3e1363 100644 --- a/server/src/api/schedule/schedule.controller.ts +++ b/server/src/api/schedule/schedule.controller.ts @@ -1,54 +1,93 @@ -import { ApiTags } from '@nestjs/swagger'; -import { Schedule } from '@prisma/client'; -import { BadRequestException, Body, Controller, Get, NotFoundException, Post, UsePipes, ValidationPipe } from '@nestjs/common'; +import { ApiQuery, ApiTags } from "@nestjs/swagger"; +import { Schedule } from "@prisma/client"; +import { + BadRequestException, + Body, + Controller, + Get, + NotFoundException, + Post, + Query, + UsePipes, + ValidationPipe, +} from "@nestjs/common"; -import { JobService } from '../job/job.service'; -import { ScheduleService } from './schedule.service'; +import { JobService } from "../job/job.service"; +import { ScheduleService } from "./schedule.service"; -import { CreateScheduleDto } from './dtos/create-schedule.dto'; +import { CreateScheduleDto } from "./dtos/create-schedule.dto"; +import { GetScheduleQueryDto } from "./dtos/get-schedule.dto"; -@ApiTags('schedule') -@Controller('schedule') +@ApiTags("schedule") +@Controller("schedule") export class ScheduleController { - constructor( - private readonly scheduleService: ScheduleService, - private readonly jobService: JobService - ) { } + constructor( + private readonly scheduleService: ScheduleService, + private readonly jobService: JobService, + ) {} - @Post() - @UsePipes(new ValidationPipe()) - async create(@Body() params: CreateScheduleDto): Promise { - const { jobId } = params + @Post() + @UsePipes(new ValidationPipe()) + async create(@Body() params: CreateScheduleDto): Promise { + const { jobId } = params; - await this.checkIfJobExists(jobId) + await this.checkIfJobExists(jobId); - const schedule = await this.scheduleService.create(params) + const schedule = await this.scheduleService.create(params); - if (!schedule) { - throw new BadRequestException("Something went wrong. Schedule not created.") - } - - return schedule + if (!schedule) { + throw new BadRequestException( + "Something went wrong. Schedule not created.", + ); } - async checkIfJobExists(id: number): Promise { - const job = await this.jobService.findOne({ id: Number(id) }) + return schedule; + } - if (!job) { - throw new NotFoundException("No job found.") - } + async checkIfJobExists(id: number): Promise { + const job = await this.jobService.findOne({ id: Number(id) }); - return !!job + if (!job) { + throw new NotFoundException("No job found."); } - @Get() - async findAll(): Promise { - const schedules = await this.scheduleService.findAll(); + return !!job; + } - if (schedules.length == 0) { - throw new NotFoundException("No schedule found.") - } + @Get() + async findAll(): Promise { + const schedules = await this.scheduleService.findAll(); - return schedules + if (schedules.length == 0) { + throw new NotFoundException("No schedule found."); } + + return schedules; + } + + @Get("/calendar") + @ApiQuery({ + name: "startDate", + required: false, + }) + @ApiQuery({ + name: "endDate", + required: false, + }) + @ApiQuery({ + name: "jobType", + required: false, + }) + @ApiQuery({ + name: "personInChargeId", + required: false, + }) + async findCalendarSchedule( + @Query() + queries: GetScheduleQueryDto, + ): Promise { + const schedules = await this.scheduleService.getCalendarEvents(queries); + + return schedules; + } } diff --git a/server/src/api/schedule/schedule.module.ts b/server/src/api/schedule/schedule.module.ts index 6516cb1..d938921 100644 --- a/server/src/api/schedule/schedule.module.ts +++ b/server/src/api/schedule/schedule.module.ts @@ -1,4 +1,12 @@ -import { Module } from '@nestjs/common'; +import { Module } from "@nestjs/common"; +import { ScheduleController } from "./schedule.controller"; +import { PrismaService } from "src/database/connection.service"; +import { ScheduleService } from "./schedule.service"; +import { JobModule } from "../job/job.module"; -@Module({}) +@Module({ + imports: [JobModule], + controllers: [ScheduleController], + providers: [PrismaService, ScheduleService], +}) export class ScheduleModule {} diff --git a/server/src/api/schedule/schedule.service.spec.ts b/server/src/api/schedule/schedule.service.spec.ts index da3e70e..1a5ed66 100644 --- a/server/src/api/schedule/schedule.service.spec.ts +++ b/server/src/api/schedule/schedule.service.spec.ts @@ -1,17 +1,36 @@ +import { Test } from "@nestjs/testing"; +import { PrismaService } from "../../database/connection.service"; +import { ScheduleService } from "./schedule.service"; +import { GetScheduleQueryDto } from "./dtos/get-schedule.dto"; +import { Schedule } from "@prisma/client"; -import { PrismaService } from '../../database/connection.service'; -import { ScheduleService } from './schedule.service'; - -describe('ScheduleService', () => { +describe("ScheduleService", () => { let scheduleService: ScheduleService; let prisma: PrismaService; + const mockPrisma = { + job: { + groupBy: jest.fn(), + }, + schedule: { + findMany: jest.fn(), + }, + }; + beforeEach(async () => { - scheduleService = new ScheduleService(prisma) + const moduleRef = await Test.createTestingModule({ + providers: [ + ScheduleService, + { provide: PrismaService, useValue: mockPrisma }, + ], + }).compile(); + + scheduleService = moduleRef.get(ScheduleService); + prisma = moduleRef.get(PrismaService); }); - describe('create', () => { - it('should return a schedule', async () => { + describe("create", () => { + it("should return a schedule", async () => { const date = new Date(); const scheduleInput = { @@ -20,7 +39,7 @@ describe('ScheduleService', () => { startTime: "2023-01-02T01:00:00.000Z", endTime: "2023-01-02T01:00:00.000Z", jobId: 1, - } + }; const schedule = { id: 1, @@ -29,18 +48,18 @@ describe('ScheduleService', () => { startTime: date, endTime: date, jobId: 1, - } + }; - jest.spyOn(scheduleService, 'create').mockResolvedValue(schedule) + jest.spyOn(scheduleService, "create").mockResolvedValue(schedule); - const createdSchedule = await scheduleService.create(scheduleInput) + const createdSchedule = await scheduleService.create(scheduleInput); expect(createdSchedule).toEqual(schedule); - }) - }) + }); + }); - describe('findOne', () => { - it('should return a single schedule', async () => { + describe("findOne", () => { + it("should return a single schedule", async () => { const date = new Date(); const schedule = { @@ -50,18 +69,18 @@ describe('ScheduleService', () => { startTime: date, endTime: date, jobId: 1, - } + }; - jest.spyOn(scheduleService, 'findOne').mockResolvedValue(schedule) + jest.spyOn(scheduleService, "findOne").mockResolvedValue(schedule); - const findAllPromise = await scheduleService.findOne({ id: 1 }) + const findAllPromise = await scheduleService.findOne({ id: 1 }); expect(findAllPromise).toEqual(schedule); - }) - }) + }); + }); - describe('findAll', () => { - it('should return an array of schedules', async () => { + describe("findAll", () => { + it("should return an array of schedules", async () => { const date = new Date(); const schedules = [ @@ -80,14 +99,55 @@ describe('ScheduleService', () => { startTime: date, endTime: date, jobId: 1, - } - ] + }, + ]; - jest.spyOn(scheduleService, 'findAll').mockResolvedValue(schedules) + jest.spyOn(scheduleService, "findAll").mockResolvedValue(schedules); - const findAllPromise = await scheduleService.findAll() + const findAllPromise = await scheduleService.findAll(); expect(findAllPromise).toEqual(schedules); - }) - }) + }); + }); + + describe("getSchedules", () => { + it("asdasd", async () => { + const date = new Date(); + const query: GetScheduleQueryDto = { + startDate: new Date("2023-01-01").toISOString(), + endDate: new Date("2023-01-31").toISOString(), + jobType: "TypeA", + personInChargeId: 1, + }; + + const sampleSchedules: Schedule[] = [ + { + id: 1, + startDate: date, + endDate: date, + startTime: date, + endTime: date, + jobId: 1, + createdAt: date, + updatedAt: date, + }, + { + id: 2, + startDate: date, + endDate: date, + startTime: date, + endTime: date, + jobId: 1, + createdAt: date, + updatedAt: date, + }, + ]; + + prisma.schedule.findMany = jest.fn().mockResolvedValue(sampleSchedules); + + const result = await scheduleService.getSchedules(query); + + expect(result).toEqual(sampleSchedules); + }); + }); }); diff --git a/server/src/api/schedule/schedule.service.ts b/server/src/api/schedule/schedule.service.ts index c72af1b..a1f1a08 100644 --- a/server/src/api/schedule/schedule.service.ts +++ b/server/src/api/schedule/schedule.service.ts @@ -1,12 +1,121 @@ -import { Injectable } from '@nestjs/common'; +import * as moment from "moment"; +import { Injectable } from "@nestjs/common"; -import { PrismaService } from '../../database/connection.service'; +import { PrismaService } from "../../database/connection.service"; -import { AbstractService } from '../../shared/abstract-service'; +import { AbstractService } from "../../shared/abstract-service"; +import { Schedule } from "@prisma/client"; +import { GetScheduleQueryDto } from "./dtos/get-schedule.dto"; +import { CalendarEventDto } from "./dtos/calendar-events.dto"; @Injectable() export class ScheduleService extends AbstractService { - constructor(prisma: PrismaService) { - super(prisma, "Schedule") - } + constructor(prisma: PrismaService) { + super(prisma, "Schedule"); + } + + async getSchedules({ + startDate, + endDate, + jobType, + personInChargeId, + }: GetScheduleQueryDto): Promise { + const schedules = await this.prisma.schedule.findMany({ + where: { + OR: !(startDate === undefined && endDate === undefined) + ? [ + { + AND: [ + { startDate: { lte: endDate } }, // Check if schedule starts before or on 'end' + { endDate: { gte: startDate } }, // Check if schedule ends after or on 'start' + ], + }, + { + AND: [ + { startDate: { gte: startDate } }, // Check if schedule starts after or on 'start' + { startDate: { lte: endDate } }, // Check if schedule starts before or on 'end' + ], + }, + ] + : undefined, + job: { + userId: { + equals: personInChargeId, + }, + type: { + equals: jobType, + mode: "insensitive", + }, + }, + }, + }); + return schedules; + } + + async getCalendarEvents(queries: GetScheduleQueryDto): Promise { + const schedules = await this.getSchedules(queries); + const jobIds = schedules.map((x) => x.jobId); + const jobs = await this.prisma.job.findMany({ + where: { + id: { + in: jobIds, + }, + }, + }); + + const calendarEvent: CalendarEventDto[] = []; + + schedules.forEach((schedule) => { + // const startDate = moment(queries.startDate).startOf("day"); + // const endDate = moment(queries.endDate).endOf("day"); + const startDate = moment(queries.startDate); + const endDate = moment(queries.endDate); + const duration = endDate.diff(startDate, "days") + 1; + console.log(startDate.toISOString(), endDate.toISOString(), duration); + + for (let i = 0; i <= duration; i++) { + const targetedDate = moment(startDate).add(i, "day"); + + // Extract date components + const year = targetedDate.year(); + const month = targetedDate.month(); + const day = targetedDate.date(); + + // Extract start time components + const startHours = moment(schedule.startTime).hours(); + const startMinutes = moment(schedule.startTime).minutes(); + const startSeconds = moment(schedule.startTime).seconds(); + + // Extract end time components + const endHours = moment(schedule.endTime).hours(); + const endMinutes = moment(schedule.endTime).minutes(); + const endSeconds = moment(schedule.endTime).seconds(); + + const newCalendarEvent = new CalendarEventDto({ + jobId: schedule.jobId, + scheduleId: schedule.id, + start: moment() + .year(year) + .month(month) + .date(day) + .hours(startHours) + .minutes(startMinutes) + .seconds(startSeconds) + .toISOString(), + end: moment() + .year(year) + .month(month) + .date(day) + .hours(endHours) + .minutes(endMinutes) + .seconds(endSeconds) + .toISOString(), + title: jobs.find((x) => x.id === schedule.jobId)?.title, + }); + + calendarEvent.push(newCalendarEvent); + } + }); + return schedules; + } } diff --git a/server/src/api/user/dtos/user.dto.ts b/server/src/api/user/dtos/user.dto.ts new file mode 100644 index 0000000..b91369b --- /dev/null +++ b/server/src/api/user/dtos/user.dto.ts @@ -0,0 +1,34 @@ +import { Role } from "@prisma/client"; +import { + IsDateString, + IsEmail, + IsIn, + IsNumber, + IsOptional, + IsString, +} from "class-validator"; + +export class UserDto { + @IsNumber() + id: number; + + @IsString() + firstName: string; + + @IsString() + lastName: string; + + @IsEmail() + email: string; + + @IsOptional() + @IsString() + @IsIn(Object.values(Role)) + role: string; + + @IsDateString() + createdAt: Date; + + @IsDateString() + updatedAt: Date; +} diff --git a/server/src/main.ts b/server/src/main.ts index 0626313..eefe0fe 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,23 +1,28 @@ -import { NestFactory } from '@nestjs/core'; -import { ValidationPipe } from '@nestjs/common'; +import { NestFactory } from "@nestjs/core"; +import { ValidationPipe } from "@nestjs/common"; -import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger' +import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger"; -import { AppModule } from './app.module'; +import { AppModule } from "./app.module"; async function bootstrap() { const app = await NestFactory.create(AppModule); - app.useGlobalPipes(new ValidationPipe()); - app.setGlobalPrefix('api') - + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + }), + ); + app.setGlobalPrefix("api"); + const config = new DocumentBuilder() - .setTitle('sim-JMS') - .setDescription('This is the API documentation for sim-jms') - .setVersion('1.0') + .setTitle("sim-JMS") + .setDescription("This is the API documentation for sim-jms") + .setVersion("1.0") .build(); const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('api', app, document); + SwaggerModule.setup("api", app, document); await app.listen(4000); } diff --git a/server/src/models/seed.ts b/server/src/models/seed.ts index 40b42fa..efc7850 100644 --- a/server/src/models/seed.ts +++ b/server/src/models/seed.ts @@ -1,21 +1,27 @@ import { PrismaClient } from "@prisma/client"; import seedUsers from "./seeders/user.seed"; +import seedJobs from "./seeders/job.seed"; +import seedCustomers from "./seeders/customer.seed"; +import seedSchedules from "./seeders/schedule.seed"; async function seed() { - const prisma = new PrismaClient(); + const prisma = new PrismaClient(); - try { - await prisma.$transaction(async () => { - await seedUsers(); - // add other seeder files here - }); - } catch (error) { - console.log('Error seeding database'); - throw error; - } finally { - await prisma.$disconnect(); - } + try { + await prisma.$transaction(async () => { + await seedUsers(); + await seedCustomers(); + await seedJobs(); + await seedSchedules(); + // add other seeder files here + }); + } catch (error) { + console.log("Error seeding database"); + throw error; + } finally { + await prisma.$disconnect(); + } } seed(); diff --git a/server/src/models/seeders/customer.seed.ts b/server/src/models/seeders/customer.seed.ts new file mode 100644 index 0000000..33470fe --- /dev/null +++ b/server/src/models/seeders/customer.seed.ts @@ -0,0 +1,20 @@ +import { faker } from "@faker-js/faker"; +import { PrismaClient, Prisma } from "@prisma/client"; + +export default async function seedCustomers() { + const prisma = new PrismaClient(); + + const customerData: Prisma.CustomerCreateManyInput[] = Array.from({ + length: 5, + }).map(() => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + email: faker.internet.email(), + contact: faker.phone.number(), + address: `${faker.location.streetAddress()}, ${faker.location.city()}, ${faker.location.country()}`, + })); + + await prisma.customer.createMany({ + data: customerData, + }); +} diff --git a/server/src/models/seeders/job.seed.ts b/server/src/models/seeders/job.seed.ts new file mode 100644 index 0000000..41784fb --- /dev/null +++ b/server/src/models/seeders/job.seed.ts @@ -0,0 +1,20 @@ +import { faker } from "@faker-js/faker"; +import { PrismaClient, Prisma } from "@prisma/client"; + +export default async function seedJobs() { + const prisma = new PrismaClient(); + + const jobData: Prisma.JobCreateManyInput[] = Array.from({ length: 5 }).map( + () => ({ + title: faker.commerce.productName(), + type: faker.commerce.product(), + remarks: faker.lorem.sentence({ min: 5, max: 10 }), + customerId: faker.helpers.rangeToNumber({ min: 1, max: 5 }), + userId: faker.helpers.rangeToNumber({ min: 1, max: 5 }), + }), + ); + + await prisma.job.createMany({ + data: jobData, + }); +} diff --git a/server/src/models/seeders/schedule.seed.ts b/server/src/models/seeders/schedule.seed.ts new file mode 100644 index 0000000..7d28041 --- /dev/null +++ b/server/src/models/seeders/schedule.seed.ts @@ -0,0 +1,46 @@ +import { faker } from "@faker-js/faker"; +import { PrismaClient, Prisma } from "@prisma/client"; + +export default async function seedSchedules() { + const prisma = new PrismaClient(); + + const scheduleData: Prisma.ScheduleCreateManyInput[] = Array.from({ + length: 5, + }).map(() => { + const randomStartHours = Math.floor(Math.random() * 19) + 1; + const randomOffsetHours = Math.floor(Math.random() * 3) + 1; + + const startTime = new Date( + `2023-09-15T${padNumToString(randomStartHours, 2, "0")}:00:00.000Z`, + ); + const endTime = new Date( + `2023-09-15T${padNumToString( + randomStartHours + randomOffsetHours, + 2, + "0", + )}:00:00.000Z`, + ); + + return { + jobId: faker.helpers.rangeToNumber({ min: 1, max: 5 }), + startDate: faker.date.between({ + from: "2023-09-01T00:00:00.000Z", + to: "2023-09-15T00:00:00.000Z", + }), + endDate: faker.date.between({ + from: "2023-09-15T00:00:00.000Z", + to: "2023-09-30T00:00:00.000Z", + }), + startTime, + endTime, + }; + }); + + await prisma.schedule.createMany({ + data: scheduleData, + }); +} + +const padNumToString = (num: number, digits: number, padString: string) => { + return num.toString().padStart(digits, padString); +}; diff --git a/server/yarn.lock b/server/yarn.lock index b86db84..df8284d 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -3610,6 +3610,11 @@ mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +moment@^2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"