Skip to content

Commit

Permalink
feat: add verse search feature and make index for vers column
Browse files Browse the repository at this point in the history
  • Loading branch information
the-sabra committed Sep 17, 2024
1 parent d029397 commit 907a60c
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 29 deletions.
2 changes: 1 addition & 1 deletion src/shared/paginated-request.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class paginatedRequest {

@IsInt({ message: 'Take must be an integer' })
@IsPositive({ message: 'Take must be a positive integer' })
@Max(20, { message: 'Take cannot exceed 20' })
@Max(35, { message: 'Take cannot exceed 20' })
@Transform(({ value }) => parseInt(value))
take: number;
}
12 changes: 9 additions & 3 deletions src/verse/dto/filter-get-verse.dto.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { IsInt, Min } from 'class-validator';
import { IsInt, IsOptional, IsString, Min } from 'class-validator';
import { Transform } from 'class-transformer';
import { paginatedRequest } from 'src/shared/paginated-request.dto';

export class GetVerseFilterDto {
export class GetVerseFilterDto extends paginatedRequest {
@IsInt()
@Min(1)
@Transform(({ value }) => parseInt(value))
surah_id: number;
@IsOptional()
surah_id?: number;

@IsOptional()
@IsString()
name?: string;
}
4 changes: 3 additions & 1 deletion src/verse/entities/verse.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ export class Verse {
@PrimaryGeneratedColumn()
id: number;

@Column('text')
//for full text search
@Column({ type: 'text', charset: 'utf8mb4' })
@Index('IDX_VERS', { fulltext: true })
vers: string;

@Column('int')
Expand Down
20 changes: 14 additions & 6 deletions src/verse/verse.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe('VerseController', () => {
useValue: {
create: jest.fn(),
getSurahVerses: jest.fn(),
getVerse: jest.fn(), // Add this line if `getVerse` is also needed elsewhere
},
},
],
Expand Down Expand Up @@ -55,8 +56,12 @@ describe('VerseController', () => {
});

describe('getSurahVerses', () => {
it('should call verseService.getSurahVerses with the correct parameters', async () => {
const getVerseFilterDto: GetVerseFilterDto = { surah_id: 1 };
it('should call verseService.getVerse with the correct parameters', async () => {
const getVerseFilterDto: GetVerseFilterDto = {
surah_id: 1,
page: 1,
take: 10,
};
const verses: Verse[] = [
{
id: 1,
Expand All @@ -75,15 +80,18 @@ describe('VerseController', () => {
surah: null,
},
];
const getSurahVersesResult = { verses, totalVerseNumber: verses.length };
const getSurahVersesResult = {
verses,
totalData: 27,
totalPages: 2,
} as any;

jest
.spyOn(verseService, 'getSurahVerses')
.spyOn(verseService, 'getVerse')
.mockResolvedValueOnce(getSurahVersesResult);

const result = await verseController.getSurahVerses(getVerseFilterDto);

expect(verseService.getSurahVerses).toHaveBeenCalledWith(1);
expect(verseService.getVerse).toHaveBeenCalledWith(getVerseFilterDto);
expect(result).toEqual(getSurahVersesResult);
});
});
Expand Down
2 changes: 1 addition & 1 deletion src/verse/verse.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class VerseController {

@Get('/surah')
getSurahVerses(@Query() getVerseFilterDto: GetVerseFilterDto) {
return this.verseService.getSurahVerses(getVerseFilterDto.surah_id);
return this.verseService.getVerse(getVerseFilterDto);
}

@Get('/random')
Expand Down
6 changes: 5 additions & 1 deletion src/verse/verse.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ import { Verse } from './entities/verse.entity';
providers: [VerseService],
exports: [VerseService],
})
export class VerseModule {}
export class VerseModule {
constructor(private readonly verseService: VerseService) {
this.verseService.initialVerses();
}
}
100 changes: 91 additions & 9 deletions src/verse/verse.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Verse } from './entities/verse.entity';
import { CreateVerseDto } from './dto/create-verse.dto';
import { Repository } from 'typeorm';
import { InternalServerErrorException, Logger } from '@nestjs/common';
import { GetVerseFilterDto } from './dto/filter-get-verse.dto';

jest.mock('../../quran.json', () => [
{
Expand Down Expand Up @@ -84,22 +85,103 @@ describe('VerseService', () => {
});
});

describe('getSurahVerses', () => {
it('should return verses of a surah', async () => {
const surahId = 1;
const verses: Verse[] = [];
describe('getVerse', () => {
const getVerseFilterDto: GetVerseFilterDto = {
surah_id: 1,
name: 'Allah',
page: 1,
take: 10,
};

const verses: Verse[] = [
{
id: 1,
vers: 'In the name of Allah, the Most Merciful, the Most Compassionate.',
verse_number: 1,
vers_lang: 'ar',
surah_id: 1,
} as Verse,
];

const queryBuilderMock = {
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue(verses),
getCount: jest.fn().mockResolvedValue(1),
leftJoinAndSelect: jest.fn().mockReturnThis(),
};

it('should return filtered verses with pagination', async () => {
const queryBuilderMock = {
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue(verses),
getCount: jest.fn().mockResolvedValue(1),
leftJoinAndSelect: jest.fn().mockReturnThis(),
};

jest
.spyOn(verseRepository, 'createQueryBuilder')
.mockReturnValue(queryBuilderMock as any);

jest.spyOn(verseRepository, 'find').mockResolvedValueOnce(verses);
const result = await verseService.getVerse(getVerseFilterDto);

const result = await verseService.getSurahVerses(surahId);
expect(queryBuilderMock.where).toHaveBeenCalledWith('1 = 1');
expect(queryBuilderMock.andWhere).toHaveBeenCalledWith(
'verse.surah_id = :surah_id',
{ surah_id: 1 },
);
expect(queryBuilderMock.andWhere).toHaveBeenCalledWith(
'MATCH(verse.vers) AGAINST(:name IN NATURAL LANGUAGE MODE)',
{ name: 'Allah' },
);
expect(queryBuilderMock.skip).toHaveBeenCalledWith(0);
expect(queryBuilderMock.take).toHaveBeenCalledWith(10);
expect(queryBuilderMock.getMany).toHaveBeenCalled();
expect(queryBuilderMock.getCount).toHaveBeenCalled();

expect(verseRepository.find).toHaveBeenCalledWith({
where: { surah_id: surahId },
expect(result).toEqual({
verses,
totalData: 1,
totalPages: 1,
});
});

it('should return all verses if no filters are provided', async () => {
const basicFilterDto: GetVerseFilterDto = {
page: 1,
take: 10,
};

jest
.spyOn(verseRepository, 'createQueryBuilder')
.mockReturnValue(queryBuilderMock as any);

const result = await verseService.getVerse(basicFilterDto);

expect(queryBuilderMock.where).toHaveBeenCalledWith('1 = 1');
expect(queryBuilderMock.andWhere).not.toHaveBeenCalledWith(
'verse.surah_id = :surah_id',
expect.anything(),
);
expect(queryBuilderMock.andWhere).not.toHaveBeenCalledWith(
'MATCH(verse.vers) AGAINST(:name IN NATURAL LANGUAGE MODE)',
expect.anything(),
);

expect(queryBuilderMock.skip).toHaveBeenCalledWith(0);
expect(queryBuilderMock.take).toHaveBeenCalledWith(10);
expect(queryBuilderMock.getMany).toHaveBeenCalled();
expect(queryBuilderMock.getCount).toHaveBeenCalled();

expect(result).toEqual({
verses,
totalVerseNumber: verses.length,
totalData: 1,
totalPages: 1,
});
});
});
Expand Down
37 changes: 30 additions & 7 deletions src/verse/verse.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Verse } from './entities/verse.entity';
import { Repository } from 'typeorm';
import { randomInt } from 'node:crypto';
import { GetVerseFilterDto } from './dto/filter-get-verse.dto';

@Injectable()
export class VerseService {
Expand All @@ -25,15 +26,37 @@ export class VerseService {
}
}

async getSurahVerses(surah_id: number) {
const verses = await this.verseRepository.find({
where: { surah_id },
});
const result = {
async getVerse(getVerseFilterDto: GetVerseFilterDto) {
const { surah_id, name, page, take } = getVerseFilterDto;
const skip = (page - 1) * take;
const query = this.verseRepository
.createQueryBuilder('verse')
.leftJoinAndSelect('verse.surah', 'surah')
.where(`1 = 1`);

if (surah_id) {
query.andWhere('verse.surah_id = :surah_id', { surah_id });
}

if (name) {
query.andWhere(
'MATCH(verse.vers) AGAINST(:name IN NATURAL LANGUAGE MODE)',
{ name },
);
}

const [verses, totalData] = await Promise.all([
query.skip(skip).take(take).getMany(),
query.getCount(),
]);

const totalPages = Math.ceil(totalData / take);

return {
verses,
totalVerseNumber: verses.length,
totalData,
totalPages,
};
return result;
}

async initialVerses() {
Expand Down

0 comments on commit 907a60c

Please sign in to comment.