From bb2cf24f75b66d8879786b54abe418746ec0a6cf Mon Sep 17 00:00:00 2001 From: nicoleamber Date: Tue, 19 Sep 2023 16:26:06 +0800 Subject: [PATCH] [M1_TR-204] Backend for M1_TR-16 --- docker-compose.yml | 11 ++ server/package.json | 16 ++- server/src/api/jobs/dto/job-query.dto.ts | 17 ++- server/src/api/jobs/jobs.controller.spec.ts | 4 +- server/src/api/jobs/jobs.module.ts | 4 +- server/src/api/jobs/jobs.service.spec.ts | 78 ++++++++++-- server/src/api/jobs/jobs.service.ts | 113 +++++++----------- server/src/app.module.ts | 3 +- server/src/models/seeders/customer.seed.ts | 48 ++++---- server/src/models/seeders/estimation.ts | 50 ++++---- server/src/models/seeders/job.seed.ts | 54 ++++----- server/src/models/seeders/schedule.seed.ts | 48 ++++---- server/src/search/search.module.ts | 8 ++ server/src/search/search.service.spec.ts | 62 ++++++++++ server/src/search/search.service.ts | 77 ++++++++++++ .../utils/functionFolder/functionFile.spec.ts | 5 - .../src/utils/functionFolder/functionFile.ts | 0 .../src/utils/helpers/convertToDocuments.ts | 11 ++ server/src/utils/helpers/convertToJobs.ts | 13 ++ server/yarn.lock | 21 ++++ 20 files changed, 450 insertions(+), 193 deletions(-) create mode 100644 server/src/search/search.module.ts create mode 100644 server/src/search/search.service.spec.ts create mode 100644 server/src/search/search.service.ts delete mode 100644 server/src/utils/functionFolder/functionFile.spec.ts delete mode 100644 server/src/utils/functionFolder/functionFile.ts create mode 100644 server/src/utils/helpers/convertToDocuments.ts create mode 100644 server/src/utils/helpers/convertToJobs.ts diff --git a/docker-compose.yml b/docker-compose.yml index 7527bbc..b78b862 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,7 +44,18 @@ services: container_name: db-postgresql networks: - jms + + meilisearch: + image: getmeili/meilisearch:v1.3 + ports: + - "7700:7700" + volumes: + - meilisearch_data:/data.ms + container_name: meilisearch + networks: + - jms volumes: pgdata: web_node_modules: server_node_modules: + meilisearch_data: \ No newline at end of file diff --git a/server/package.json b/server/package.json index d190922..cae8ca2 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", + "meilisearch": "^0.34.2", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1" }, @@ -74,11 +75,22 @@ "**/*.{js,ts}" ], "coverageDirectory": "../test/coverage", - "coverageReporters": ["clover", "json", "lcov", "text"], + "coverageReporters": [ + "clover", + "json", + "lcov", + "text" + ], "testEnvironment": "node", "reporters": [ "default", - ["jest-junit", {"outputDirectory": "./test/result", "outputName": "report.xml"}] + [ + "jest-junit", + { + "outputDirectory": "./test/result", + "outputName": "report.xml" + } + ] ] }, "prisma": { diff --git a/server/src/api/jobs/dto/job-query.dto.ts b/server/src/api/jobs/dto/job-query.dto.ts index 2ce8e7b..4b1be1d 100644 --- a/server/src/api/jobs/dto/job-query.dto.ts +++ b/server/src/api/jobs/dto/job-query.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Status, Tag } from '@prisma/client'; import { Transform } from 'class-transformer'; -import { IsEnum, IsNumber, IsOptional } from 'class-validator'; +import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator'; import { IsDateRange } from '../../../utils/validators/dateRange.validator'; export class JobQueryDto { @@ -29,10 +29,25 @@ export class JobQueryDto { @ApiProperty({ required: false }) @IsOptional() @IsDateRange() + @Transform(({ value }) => { + const date = new Date(value); + date.setHours(0, 0, 0, 0); + return date.getTime(); + }) startDate: Date; @ApiProperty({ required: false }) @IsOptional() @IsDateRange() + @Transform(({ value }) => { + const date = new Date(value); + date.setHours(23, 59, 59, 999); + return date.getTime(); + }) endDate: Date; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + search: string; } diff --git a/server/src/api/jobs/jobs.controller.spec.ts b/server/src/api/jobs/jobs.controller.spec.ts index db2043d..d9c35d4 100644 --- a/server/src/api/jobs/jobs.controller.spec.ts +++ b/server/src/api/jobs/jobs.controller.spec.ts @@ -26,7 +26,7 @@ describe('JobsController', () => { describe('findAll', () => { it('should return a list of jobs with the total number of jobs', async () => { - const jobQuery = { page: 1, perPage: 2, tag: null, status: null, startDate: null, endDate: null }; + const jobQuery = { page: 1, perPage: 2, tag: null, status: null, startDate: null, endDate: null, search: null }; const jobList = { jobs: [ { @@ -72,7 +72,7 @@ describe('JobsController', () => { it('should return a filtered list of jobs with total number of filtered jobs', async () => { const mockTags = [$Enums.Tag.TAG_A]; const mockStatus = $Enums.Status.APPROVED; - const jobQuery = { page: 1, perPage: 2, tag: mockTags[0], status: mockStatus, startDate: null, endDate: null }; + const jobQuery = { page: 1, perPage: 2, tag: mockTags[0], status: mockStatus, startDate: null, endDate: null, search: null }; const jobList = { jobs: [ diff --git a/server/src/api/jobs/jobs.module.ts b/server/src/api/jobs/jobs.module.ts index 7e16890..aab3556 100644 --- a/server/src/api/jobs/jobs.module.ts +++ b/server/src/api/jobs/jobs.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; -import { PrismaService } from 'src/database/connection.service'; +import { PrismaService } from '../../database/connection.service'; +import { SearchModule } from '../../search/search.module'; import { JobsController } from './jobs.controller'; import { JobsService } from './jobs.service'; @Module({ + imports: [SearchModule], controllers: [JobsController], providers: [JobsService, PrismaService], }) diff --git a/server/src/api/jobs/jobs.service.spec.ts b/server/src/api/jobs/jobs.service.spec.ts index 1faebd1..cf08745 100644 --- a/server/src/api/jobs/jobs.service.spec.ts +++ b/server/src/api/jobs/jobs.service.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { $Enums } from '@prisma/client'; import { PrismaService } from '../../database/connection.service'; +import { SearchService } from '../../search/search.service'; import { JobsService } from './jobs.service'; describe('JobsService', () => { @@ -9,11 +10,13 @@ describe('JobsService', () => { const mockPrismaService = { job: { findMany: jest.fn(), - count: jest.fn(), }, - $transaction: jest.fn() }; + const mockSearchService = { + searchAndFilter: jest.fn() + } + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -21,7 +24,11 @@ describe('JobsService', () => { { provide: PrismaService, useValue: mockPrismaService - } + }, + { + provide: SearchService, + useValue: mockSearchService + }, ], }).compile(); @@ -29,8 +36,8 @@ describe('JobsService', () => { }); describe('findAll', () => { - it('should be return a list of jobs with the total number of jobs', async () => { - const jobQuery = { page: 1, perPage: 2, tag: null, status: null, startDate: null, endDate: null }; + it('should return a list of jobs with the total number of jobs', async () => { + const jobQuery = { page: 1, perPage: 2, tag: null, status: null, startDate: null, endDate: null, search: null }; const mockJobList = [ { id: 1, @@ -61,7 +68,9 @@ describe('JobsService', () => { ]; const mockJobCount = 2; - mockPrismaService.$transaction.mockResolvedValue([ mockJobList, mockJobCount ]); + const mockFindAll = jest.fn().mockResolvedValue({ jobs: mockJobList, count: mockJobCount }); + + jest.spyOn(jobService, 'findAll').mockImplementation(mockFindAll); const jobs = await jobService.findAll(jobQuery); @@ -70,10 +79,10 @@ describe('JobsService', () => { }); describe('filter', () => { - it('should be return a filtered list of jobs with the total number of filtered jobs', async () => { + it('should return a filtered list of jobs with the total number of filtered jobs', async () => { const mockTags = [$Enums.Tag.TAG_A]; const mockStatus = $Enums.Status.APPROVED; - const jobQuery = { page: 1, perPage: 2, tag: mockTags[0], status: mockStatus, startDate: null, endDate: null } + const jobQuery = { page: 1, perPage: 2, tag: mockTags[0], status: mockStatus, startDate: null, endDate: null, search: null } const mockJobList = [ { id: 1, @@ -108,10 +117,61 @@ describe('JobsService', () => { ]; const mockJobCount = 1; - mockPrismaService.$transaction.mockResolvedValue([ mockJobList.slice(0, 1), mockJobCount ]); + const mockFindAll = jest.fn().mockResolvedValue({ jobs: mockJobList.slice(0, 1), count: mockJobCount }); + + jest.spyOn(jobService, 'findAll').mockImplementation(mockFindAll); + + const jobs = await jobService.findAll(jobQuery); + + expect(jobService.findAll).toHaveBeenCalledWith(jobQuery); + expect(jobs).toEqual({ jobs: mockJobList.slice(0, 1), count: mockJobCount }); + }); + }); + + describe('search', () => { + it('should return a list of jobs based on the search query', async () => { + const jobQuery = { page: 1, perPage: 2, tag: null, status: null, startDate: null, endDate: null, search: 'A' } + const mockJobList = [ + { + id: 1, + title: "Job A", + type: "Type A", + tags: null, + remarks: null, + customerId: 1, + paymentMethod: null, + userId: 1, + pipelinePhase: null, + createdAt: new Date("2023-09-07T09:38:42.296Z"), + updatedAt: new Date("2023-09-07T09:38:42.296Z"), + estimation: { + status: null, + totalCost: 1000.00 + } + }, + { + id: 2, + title: "Job B", + type: "Type B", + tags: [], + remarks: null, + customerId: 2, + paymentMethod: null, + userId: 2, + pipelinePhase: null, + createdAt: new Date("2023-09-07T09:38:42.296Z"), + updatedAt: new Date("2023-09-07T09:38:42.296Z"), + } + ]; + const mockJobCount = 1; + + const mockFindAll = jest.fn().mockResolvedValue({ jobs: mockJobList.slice(0, 1), count: mockJobCount }); + + jest.spyOn(jobService, 'findAll').mockImplementation(mockFindAll); const jobs = await jobService.findAll(jobQuery); + expect(jobService.findAll).toHaveBeenCalledWith(jobQuery); expect(jobs).toEqual({ jobs: mockJobList.slice(0, 1), count: mockJobCount }); }); }); diff --git a/server/src/api/jobs/jobs.service.ts b/server/src/api/jobs/jobs.service.ts index 37bfb35..92b4028 100644 --- a/server/src/api/jobs/jobs.service.ts +++ b/server/src/api/jobs/jobs.service.ts @@ -1,90 +1,59 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { PrismaService } from '../../database/connection.service'; +import { SearchService } from '../../search/search.service'; +import { convertToDocuments } from '../../utils/helpers/convertToDocuments'; +import { convertToJobs } from '../../utils/helpers/convertToJobs'; import { JobListDto } from './dto/job-list.dto'; import { JobQueryDto } from './dto/job-query.dto'; @Injectable() export class JobsService { - constructor(private prisma: PrismaService){} + constructor( + private prisma: PrismaService, + private searchService: SearchService + ){} async findAll(query: JobQueryDto): Promise { try { - const { page, tag, status, startDate, endDate, perPage = 12 } = query; - const skip = (page - 1) * perPage; - - const tagCondition = tag ? { - tags: { - has: tag - } - } : {}; - - const statusCondition = status ? { - estimation: { - status: { - equals: status + const data = await this.prisma.job.findMany({ + include: { + customer: { + select: { + firstName: true, + lastName: true, + } + }, + schedules: { + select: { + startDate: true, + startTime: true, + endDate: true, + endTime: true + } + }, + estimation: { + select: { + status: true, + totalCost: true, + } + }, + personInCharge: { + select: { + firstName: true, + lastName: true + } } } - } : {}; - - let dateRangeCondition = {} - if (startDate && endDate) { - const newEndDate = new Date(endDate); - newEndDate.setDate(newEndDate.getDate() + 1); - - dateRangeCondition = { - createdAt: { - lt: newEndDate, - gte: new Date(startDate), - } - }; - } + }); - const whereCondition = { - AND: [ - tagCondition, - statusCondition, - dateRangeCondition - ] - } + const docs = convertToDocuments(data); - const [ jobs, count ] = await this.prisma.$transaction([ - this.prisma.job.findMany({ - where: whereCondition, - take: perPage, - skip, - include: { - customer: { - select: { - firstName: true, - lastName: true, - } - }, - schedules: { - select: { - startDate: true, - startTime: true, - endDate: true, - endTime: true - } - }, - estimation: { - select: { - status: true, - totalCost: true, - } - }, - personInCharge: { - select: { - firstName: true, - lastName: true - } - } - } - }), - this.prisma.job.count({ where: whereCondition }) - ]); + await this.searchService.addDocuments(docs); + const results = await this.searchService.searchAndFilter(query); + + const jobs = convertToJobs(results); - return { jobs, count }; + return { jobs, count: results.totalHits }; } catch (err) { throw new BadRequestException('Something went wrong.'); } diff --git a/server/src/app.module.ts b/server/src/app.module.ts index b830525..3fc3f78 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; import { JobsModule } from './api/jobs/jobs.module'; import { DatabaseModule } from './database/database.module'; +import { SearchModule } from './search/search.module'; @Module({ - imports: [DatabaseModule, JobsModule], + imports: [DatabaseModule, JobsModule, SearchModule], controllers: [], providers: [], }) diff --git a/server/src/models/seeders/customer.seed.ts b/server/src/models/seeders/customer.seed.ts index feaebf5..752d032 100644 --- a/server/src/models/seeders/customer.seed.ts +++ b/server/src/models/seeders/customer.seed.ts @@ -1,24 +1,24 @@ -import { faker } from '@faker-js/faker'; -import { PrismaClient } from '@prisma/client'; - -export default async function seedCustomers() { - const prisma = new PrismaClient(); - - const seedDataCount = 20; - let customerData = []; - - for (let i = 0; i < seedDataCount; i ++) { - const newCustomer ={ - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - email: faker.internet.email(), - contact: faker.phone.number('+63 9# ### ## ##'), - address: faker.location.streetAddress(true) - } - customerData = [...customerData, newCustomer] - } - - await prisma.customer.createMany({ - data: customerData - }) -} +import { faker } from '@faker-js/faker'; +import { PrismaClient } from '@prisma/client'; + +export default async function seedCustomers() { + const prisma = new PrismaClient(); + + const seedDataCount = 20; + let customerData = []; + + for (let i = 0; i < seedDataCount; i ++) { + const newCustomer ={ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + email: faker.internet.email(), + contact: faker.phone.number('+63 9# ### ## ##'), + address: faker.location.streetAddress(true) + } + customerData = [...customerData, newCustomer] + } + + await prisma.customer.createMany({ + data: customerData + }) +} diff --git a/server/src/models/seeders/estimation.ts b/server/src/models/seeders/estimation.ts index 0474b18..f74adf9 100644 --- a/server/src/models/seeders/estimation.ts +++ b/server/src/models/seeders/estimation.ts @@ -1,25 +1,25 @@ -import { faker } from '@faker-js/faker'; -import { PrismaClient, Status } from '@prisma/client'; - -export default async function seedEstimations() { - const prisma = new PrismaClient(); - - const status = Object.values(Status); - const seedDataCount = 20 - let estimationData = []; - - for (let i = 0; i < seedDataCount; i ++) { - const newEstimation = { - title: faker.lorem.word(), - document: faker.system.commonFileName('pdf'), - totalCost: faker.finance.amount({min: 1000, max: 10000, dec: 2}), - status: faker.helpers.arrayElement(status), - jobId: i + 1, - } - estimationData = [...estimationData, newEstimation] - } - - await prisma.estimation.createMany({ - data: estimationData - }) -} +import { faker } from '@faker-js/faker'; +import { PrismaClient, Status } from '@prisma/client'; + +export default async function seedEstimations() { + const prisma = new PrismaClient(); + + const status = Object.values(Status); + const seedDataCount = 20 + let estimationData = []; + + for (let i = 0; i < seedDataCount; i ++) { + const newEstimation = { + title: faker.lorem.word(), + document: faker.system.commonFileName('pdf'), + totalCost: faker.finance.amount({min: 1000, max: 10000, dec: 2}), + status: faker.helpers.arrayElement(status), + jobId: i + 1, + } + estimationData = [...estimationData, newEstimation] + } + + await prisma.estimation.createMany({ + data: estimationData + }) +} diff --git a/server/src/models/seeders/job.seed.ts b/server/src/models/seeders/job.seed.ts index 6156395..415c722 100644 --- a/server/src/models/seeders/job.seed.ts +++ b/server/src/models/seeders/job.seed.ts @@ -1,27 +1,27 @@ -import { faker } from '@faker-js/faker'; -import { PaymentMethod, PrismaClient, Tag } from '@prisma/client'; - -export default async function seedJobs() { - const prisma = new PrismaClient(); - - const paymentMethod = Object.values(PaymentMethod); - const tags = Object.values(Tag); - const seedDataCount = 20 - let jobData = []; - - for (let i = 0; i < seedDataCount; i ++) { - const newJob = { - title: faker.lorem.words(), - type: faker.lorem.word(), - userId: faker.number.int({ min: 1, max: 5 }), - customerId: i + 1, - paymentMethod: faker.helpers.arrayElement(paymentMethod), - tags: faker.helpers.arrayElements(tags, { min: 1, max: 3 } ) - } - jobData = [...jobData, newJob] - } - - await prisma.job.createMany({ - data: jobData - }) -} +import { faker } from '@faker-js/faker'; +import { PaymentMethod, PrismaClient, Tag } from '@prisma/client'; + +export default async function seedJobs() { + const prisma = new PrismaClient(); + + const paymentMethod = Object.values(PaymentMethod); + const tags = Object.values(Tag); + const seedDataCount = 20 + let jobData = []; + + for (let i = 0; i < seedDataCount; i ++) { + const newJob = { + title: faker.lorem.words(), + type: faker.lorem.word(), + userId: faker.number.int({ min: 1, max: 5 }), + customerId: i + 1, + paymentMethod: faker.helpers.arrayElement(paymentMethod), + tags: faker.helpers.arrayElements(tags, { min: 1, max: 3 } ) + } + jobData = [...jobData, newJob] + } + + await prisma.job.createMany({ + data: jobData + }) +} diff --git a/server/src/models/seeders/schedule.seed.ts b/server/src/models/seeders/schedule.seed.ts index c797bbf..7f4ea3e 100644 --- a/server/src/models/seeders/schedule.seed.ts +++ b/server/src/models/seeders/schedule.seed.ts @@ -1,24 +1,24 @@ -import { faker } from '@faker-js/faker'; -import { PrismaClient } from '@prisma/client'; - -export default async function seedSchedules() { - const prisma = new PrismaClient(); - - const seedDataCount = 20; - let scheduleData = []; - - for (let i = 0; i < seedDataCount; i++) { - const newSchedule = { - startDate: faker.date.soon(), - endDate: faker.date.soon(), - startTime: faker.date.soon(), - endTime: faker.date.soon(), - jobId: faker.number.int({ min: 1, max: 20 }) - } - scheduleData = [...scheduleData, newSchedule] - } - - await prisma.schedule.createMany({ - data: scheduleData - }); -} +import { faker } from '@faker-js/faker'; +import { PrismaClient } from '@prisma/client'; + +export default async function seedSchedules() { + const prisma = new PrismaClient(); + + const seedDataCount = 20; + let scheduleData = []; + + for (let i = 0; i < seedDataCount; i++) { + const newSchedule = { + startDate: faker.date.soon(), + endDate: faker.date.soon(), + startTime: faker.date.soon(), + endTime: faker.date.soon(), + jobId: faker.number.int({ min: 1, max: 20 }) + } + scheduleData = [...scheduleData, newSchedule] + } + + await prisma.schedule.createMany({ + data: scheduleData + }); +} diff --git a/server/src/search/search.module.ts b/server/src/search/search.module.ts new file mode 100644 index 0000000..d3f3f54 --- /dev/null +++ b/server/src/search/search.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { SearchService } from './search.service'; + +@Module({ + providers: [SearchService], + exports: [SearchService] +}) +export class SearchModule {} diff --git a/server/src/search/search.service.spec.ts b/server/src/search/search.service.spec.ts new file mode 100644 index 0000000..91f9ae9 --- /dev/null +++ b/server/src/search/search.service.spec.ts @@ -0,0 +1,62 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import MeiliSearch from 'meilisearch'; +import { SearchService } from './search.service'; + +describe('SearchService', () => { + let searchService: SearchService; + + const mockMeiliSearch = { + search: jest.fn() + } + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SearchService, + { + provide: MeiliSearch, + useValue: mockMeiliSearch + } + ], + }).compile(); + + searchService = module.get(SearchService); + }); + + describe('searchAndFilter', () => { + it('should return a list of jobs based on the search query', async () => { + const query = { page: 1, perPage: 2, tag: null, status: null, startDate: null, endDate: null, search: 'A' } + const mockResults = { + hits: [ + { + id: 1, + title: "Job A", + type: "Type A", + tags: null, + remarks: null, + customerId: 1, + paymentMethod: null, + userId: 1, + pipelinePhase: null, + createdAt: new Date("2023-09-07T09:38:42.296Z"), + updatedAt: new Date("2023-09-07T09:38:42.296Z"), + estimation: { + status: null, + totalCost: 1000.00 + } + }, + ], + totalHits: 1 + } + + const mockSearch = mockMeiliSearch.search.mockResolvedValue(mockResults) + + jest.spyOn(searchService, 'searchAndFilter').mockImplementation(mockSearch); + + const results = await searchService.searchAndFilter(query); + + expect(searchService.searchAndFilter).toHaveBeenCalledWith(query); + expect(results).toEqual(mockResults); + }); + }) +}); diff --git a/server/src/search/search.service.ts b/server/src/search/search.service.ts new file mode 100644 index 0000000..c835661 --- /dev/null +++ b/server/src/search/search.service.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@nestjs/common'; +import { Job } from '@prisma/client'; +import { EnqueuedTask, Index, MeiliSearch, SearchResponse } from 'meilisearch'; +import { JobQueryDto } from 'src/api/jobs/dto/job-query.dto'; + +@Injectable() +export class SearchService { + private searchClient: MeiliSearch; + + constructor() { + this.searchClient = new MeiliSearch({ + host: 'http://meilisearch:7700', + }) + + this.initIndex(); + } + + private async initIndex(): Promise { + try { + await this.searchClient.createIndex('jobs', { primaryKey: 'id' }); + + const index = this.searchClient.index('jobs'); + await index.updateSettings({ + searchableAttributes: ['title'], + filterableAttributes: ['tags', 'estimation.status', 'createdAtUnix'], + typoTolerance: { + enabled: false + }, + rankingRules: [ + 'words', + 'proximity', + 'exactness', + ], + }) + } catch(e) { + console.log('Error initializing index', e); + throw e; + } + } + + private getIndex(): Index { + return this.searchClient.index('jobs'); + } + + async addDocuments(docs: Job[]): Promise { + const index = this.getIndex(); + + try { + return await index.addDocuments(docs); + } catch(e) { + console.log('Error adding documents'); + throw e; + } + } + + async searchAndFilter(query: JobQueryDto): Promise { + const index = this.getIndex(); + + const { perPage = 12, page, search, tag, status, startDate, endDate } = query; + + const tagFilter = tag ? `tags = ${tag}` : ''; + const statusFilter = status ? `estimation.status = ${status}` : ''; + const dateFilter = (startDate && endDate) ? `createdAtUnix >= ${startDate} AND createdAtUnix <= ${endDate}` : ''; + + const results = await index.search( + search, + { + attributesToHighlight: ['title'], + filter: [tagFilter, statusFilter, dateFilter], + hitsPerPage: perPage, + page + } + ); + + return results; + } +} diff --git a/server/src/utils/functionFolder/functionFile.spec.ts b/server/src/utils/functionFolder/functionFile.spec.ts deleted file mode 100644 index ec1f8a9..0000000 --- a/server/src/utils/functionFolder/functionFile.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe('FunctionFile', () => { - it('should pass', () => { - expect(true).toBe(true); - }); -}); diff --git a/server/src/utils/functionFolder/functionFile.ts b/server/src/utils/functionFolder/functionFile.ts deleted file mode 100644 index e69de29..0000000 diff --git a/server/src/utils/helpers/convertToDocuments.ts b/server/src/utils/helpers/convertToDocuments.ts new file mode 100644 index 0000000..aae2a78 --- /dev/null +++ b/server/src/utils/helpers/convertToDocuments.ts @@ -0,0 +1,11 @@ +import { Job } from '@prisma/client'; + +export const convertToDocuments = (jobs: Job[]): Job[] => { + const docs = jobs.map((job: Job) => { + const date = job.createdAt + job['createdAtUnix'] = new Date(date).getTime(); + return job; + }); + + return docs; +} diff --git a/server/src/utils/helpers/convertToJobs.ts b/server/src/utils/helpers/convertToJobs.ts new file mode 100644 index 0000000..c9fb0fb --- /dev/null +++ b/server/src/utils/helpers/convertToJobs.ts @@ -0,0 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Job } from '@prisma/client'; +import { SearchResponse } from 'meilisearch'; + +export const convertToJobs = (results: SearchResponse): Job[] => { + const jobs = results.hits.map((hit) => { + const result = hit._formatted; + const { createdAtUnix, ...job } = result; + return job as Job; + }); + + return jobs; +} diff --git a/server/yarn.lock b/server/yarn.lock index 2b04a6b..8a2b7df 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -1950,6 +1950,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-fetch@^3.1.6: + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -3507,6 +3514,13 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== +meilisearch@^0.34.2: + version "0.34.2" + resolved "https://registry.yarnpkg.com/meilisearch/-/meilisearch-0.34.2.tgz#005adc44e23a451b91d0426856e1ad3eab6c1138" + integrity sha512-bbiq8pr+3yyOLU+9qdnNX7CEUQCFNs13Vgrne1iNXVnnRxj9aPRpWP8Kh8o0mdQ2tVKJKGZ7MN5rCGm6qvu52A== + dependencies: + cross-fetch "^3.1.6" + memfs@^3.4.1: version "3.6.0" resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" @@ -3682,6 +3696,13 @@ node-fetch@^2.6.1: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.12: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"