diff --git a/src/common/common.service.ts b/src/common/common.service.ts new file mode 100644 index 0000000..4ad15f3 --- /dev/null +++ b/src/common/common.service.ts @@ -0,0 +1,27 @@ +import { SortOptions, SortService } from '@/sort/sort.service'; + +export type CommonData = Record & { + code: string; + name: string; +}; + +export type FindOptions = SortOptions & { + name?: string; +}; + +export interface CommonService { + readonly sorter: SortService; + + /** + * If the name is empty, all data will be returned. + * Otherwise, it will only return the data with the matching name. + */ + find(options: FindOptions): Promise; + + /** + * Find a data by its code. + * + * @returns A data or `null`. + */ + findByCode(code: string): Promise; +} diff --git a/src/district/district.controller.ts b/src/district/district.controller.ts index 913747f..ff05e50 100644 --- a/src/district/district.controller.ts +++ b/src/district/district.controller.ts @@ -54,11 +54,7 @@ export class DistrictController { @ApiBadRequestResponse({ description: 'If there are invalid query values.' }) @Get() async find(@Query() queries?: DistrictFindQueries): Promise { - const { name, sortBy, sortOrder } = queries ?? {}; - return this.districtService.find(name, { - sortBy: sortBy, - sortOrder: sortOrder, - }); + return this.districtService.find(queries); } @ApiOperation({ description: 'Get a district by its code.' }) @@ -76,13 +72,13 @@ export class DistrictController { }) @Get(':code') async findByCode( - @Param() params: DistrictFindByCodeParams, + @Param() { code }: DistrictFindByCodeParams, ): Promise { - const { code } = params; const district = await this.districtService.findByCode(code); - if (district === null) + if (district === null) { throw new NotFoundException(`There are no district with code '${code}'`); + } return district; } @@ -116,18 +112,14 @@ export class DistrictController { }) @Get(':code/villages') async findVillage( - @Param() params: DistrictFindVillageParams, + @Param() { code }: DistrictFindVillageParams, @Query() queries?: DistrictFindVillageQueries, ): Promise { - const { code } = params; - const { sortBy, sortOrder } = queries ?? {}; - const villages = await this.districtService.findVillages(code, { - sortBy: sortBy, - sortOrder: sortOrder, - }); + const villages = await this.districtService.findVillages(code, queries); - if (villages === false) + if (villages === null) { throw new NotFoundException(`There are no district with code '${code}'`); + } return villages; } diff --git a/src/district/district.dto.ts b/src/district/district.dto.ts index 9ba8e86..e279acd 100644 --- a/src/district/district.dto.ts +++ b/src/district/district.dto.ts @@ -22,9 +22,9 @@ export class District { regencyCode: string; } -export class DistrictSortQuery extends SortQuery<'code' | 'name'> { +export class DistrictSortQuery extends SortQuery { @EqualsAny(['code', 'name']) - sortBy: 'code' | 'name'; + readonly sortBy?: 'code' | 'name'; } export class DistrictFindQueries extends IntersectionType( diff --git a/src/district/district.module.ts b/src/district/district.module.ts index c816cad..2f37e65 100644 --- a/src/district/district.module.ts +++ b/src/district/district.module.ts @@ -2,10 +2,12 @@ import { PrismaModule } from '@/prisma/prisma.module'; import { Module } from '@nestjs/common'; import { DistrictController } from './district.controller'; import { DistrictService } from './district.service'; +import { VillageModule } from '@/village/village.module'; @Module({ - imports: [PrismaModule], + imports: [PrismaModule, VillageModule], controllers: [DistrictController], providers: [DistrictService], + exports: [DistrictService], }) export class DistrictModule {} diff --git a/src/district/district.service.ts b/src/district/district.service.ts index 9c44e5e..dc7b834 100644 --- a/src/district/district.service.ts +++ b/src/district/district.service.ts @@ -1,33 +1,28 @@ -import { Injectable } from '@nestjs/common'; -import { District, Village } from '@prisma/client'; -import { PrismaService } from '@/prisma/prisma.service'; +import { CommonService, FindOptions } from '@/common/common.service'; import { getDBProviderFeatures } from '@/common/utils/db'; +import { PrismaService } from '@/prisma/prisma.service'; import { SortOptions, SortService } from '@/sort/sort.service'; - -type DistrictSortKeys = keyof District; +import { VillageService } from '@/village/village.service'; +import { Injectable } from '@nestjs/common'; +import { District, Village } from '@prisma/client'; @Injectable() -export class DistrictService { - private readonly sortService: SortService; +export class DistrictService implements CommonService { + readonly sorter: SortService; - constructor(private readonly prisma: PrismaService) { - this.sortService = new SortService({ + constructor( + private readonly prisma: PrismaService, + private readonly villageService: VillageService, + ) { + this.sorter = new SortService({ sortBy: 'code', sortOrder: 'asc', }); } - /** - * If the name is empty, all districts will be returned. - * Otherwise, it will only return the districts with the matching name. - * @param name Filter by district name (optional). - * @param sort The sort query (optional). - * @returns The array of district. - */ - async find( - name = '', - sort?: SortOptions, - ): Promise { + async find({ name, ...sortOptions }: FindOptions = {}): Promise< + District[] + > { return this.prisma.district.findMany({ where: { name: { @@ -37,16 +32,11 @@ export class DistrictService { }), }, }, - orderBy: this.sortService.object(sort), + orderBy: this.sorter.object(sortOptions), }); } - /** - * Find a district by its code. - * @param code The district code. - * @returns An district, or null if there are no match district. - */ - async findByCode(code: string): Promise { + async findByCode(code: string): Promise { return this.prisma.district.findUnique({ where: { code: code, @@ -57,23 +47,21 @@ export class DistrictService { /** * Find all villages in a district. * @param districtCode The district code. - * @param sort The sort query (optional). - * @returns Array of village in the match district, or `false` if there are no district found. + * @param sortOptions The sort options. + * @returns An array of villages, or `null` if there are no match district. */ async findVillages( districtCode: string, - sort?: SortOptions, - ): Promise { - const villages = await this.prisma.district + sortOptions?: SortOptions, + ): Promise { + return this.prisma.district .findUnique({ where: { code: districtCode, }, }) .villages({ - orderBy: this.sortService.object(sort), + orderBy: this.villageService.sorter.object(sortOptions), }); - - return villages ?? false; } } diff --git a/src/island/island.controller.ts b/src/island/island.controller.ts index ed8eccd..d0bc3d6 100644 --- a/src/island/island.controller.ts +++ b/src/island/island.controller.ts @@ -5,8 +5,6 @@ import { Param, Query, } from '@nestjs/common'; -import { IslandService } from './island.service'; -import { IslandFindByCodeParams, IslandFindQueries } from './island.dto'; import { ApiBadRequestResponse, ApiNotFoundResponse, @@ -16,7 +14,12 @@ import { ApiQuery, ApiTags, } from '@nestjs/swagger'; -import { Island } from '@prisma/client'; +import { + Island, + IslandFindByCodeParams, + IslandFindQueries, +} from './island.dto'; +import { IslandService } from './island.service'; @ApiTags('Island') @Controller('islands') @@ -50,12 +53,10 @@ export class IslandController { @ApiOkResponse({ description: 'Returns array of islands.' }) @ApiBadRequestResponse({ description: 'If there are invalid query values.' }) @Get() - async find(@Query() queries: IslandFindQueries) { - const { name, sortBy, sortOrder } = queries ?? {}; - return this.islandService.find(name, { - sortBy: sortBy, - sortOrder: sortOrder, - }); + async find(@Query() queries?: IslandFindQueries): Promise { + return (await this.islandService.find(queries)).map((island) => + this.islandService.addDecimalCoordinate(island), + ); } @ApiOperation({ description: 'Get an island by its code.' }) @@ -72,14 +73,13 @@ export class IslandController { description: 'If no island matches the `code`.', }) @Get(':code') - async findByCode(@Param() params: IslandFindByCodeParams): Promise { - const { code } = params; + async findByCode(@Param() { code }: IslandFindByCodeParams): Promise { const island = await this.islandService.findByCode(code); - if (!island) { + if (island === null) { throw new NotFoundException(`Island with code ${code} not found.`); } - return island; + return this.islandService.addDecimalCoordinate(island); } } diff --git a/src/island/island.dto.ts b/src/island/island.dto.ts index 36f59bd..ed033cf 100644 --- a/src/island/island.dto.ts +++ b/src/island/island.dto.ts @@ -39,11 +39,15 @@ export class Island { @IsNumberString() @Length(4, 4) regencyCode?: string; + + latitude?: number; + + longitude?: number; } -export class IslandSortQuery extends SortQuery<'code' | 'name' | 'coordinate'> { +export class IslandSortQuery extends SortQuery { @EqualsAny(['code', 'name', 'coordinate']) - sortBy: 'code' | 'name'; + readonly sortBy?: 'code' | 'name' | 'coordinate'; } export class IslandFindQueries extends IntersectionType( diff --git a/src/island/island.service.ts b/src/island/island.service.ts index 1f8878e..97b02cf 100644 --- a/src/island/island.service.ts +++ b/src/island/island.service.ts @@ -1,18 +1,18 @@ -import { Injectable } from '@nestjs/common'; -import { Island } from '@prisma/client'; -import { getDBProviderFeatures } from '@/common/utils/db'; +import { CommonService, FindOptions } from '@/common/common.service'; import { convertCoordinate } from '@/common/utils/coordinate'; -import { SortService, SortOptions } from '@/sort/sort.service'; +import { getDBProviderFeatures } from '@/common/utils/db'; +import { Island as IslandDTO } from '@/island/island.dto'; import { PrismaService } from '@/prisma/prisma.service'; - -export type IslandSortKeys = keyof Island; +import { SortService } from '@/sort/sort.service'; +import { Injectable } from '@nestjs/common'; +import { Island } from '@prisma/client'; @Injectable() -export class IslandService { - readonly sortService: SortService; +export class IslandService implements CommonService { + readonly sorter: SortService; constructor(private readonly prisma: PrismaService) { - this.sortService = new SortService({ + this.sorter = new SortService({ sortBy: 'code', sortOrder: 'asc', }); @@ -21,14 +21,16 @@ export class IslandService { /** * Add decimal latitude and longitude to the island object. */ - addDecimalCoordinate(island: Island) { + addDecimalCoordinate(island: Island): IslandDTO { const [latitude, longitude] = convertCoordinate(island.coordinate); return { ...island, latitude, longitude }; } - async find(name = '', sort?: SortOptions): Promise { - const islands = await this.prisma.island.findMany({ + async find({ name, ...sortOptions }: FindOptions = {}): Promise< + Island[] + > { + return this.prisma.island.findMany({ where: { name: { contains: name, @@ -37,28 +39,15 @@ export class IslandService { }), }, }, - orderBy: this.sortService.object(sort), + orderBy: this.sorter.object(sortOptions), }); - - return islands.map(this.addDecimalCoordinate); } - /** - * Find an island by its code. - * @param code The island code. - * @returns An island, or null if there are no match island. - */ async findByCode(code: string): Promise { - const island = await this.prisma.island.findUnique({ + return this.prisma.island.findUnique({ where: { code: code, }, }); - - if (island) { - return this.addDecimalCoordinate(island); - } - - return null; } } diff --git a/src/main.ts b/src/main.ts index 8e6e8d0..b5f28bf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -45,6 +45,8 @@ async function bootstrap() { app.useGlobalPipes( new ValidationPipe({ transform: true, + whitelist: true, + forbidNonWhitelisted: true, }), ); diff --git a/src/province/province.controller.ts b/src/province/province.controller.ts index 66498a9..32e3afc 100644 --- a/src/province/province.controller.ts +++ b/src/province/province.controller.ts @@ -58,11 +58,7 @@ export class ProvinceController { @ApiBadRequestResponse({ description: 'If there are invalid query values.' }) @Get() async find(@Query() queries?: ProvinceFindQueries): Promise { - const { name, sortBy, sortOrder } = queries ?? {}; - return this.provinceService.find(name, { - sortBy: sortBy, - sortOrder: sortOrder, - }); + return this.provinceService.find(queries); } @ApiOperation({ description: 'Get a province by its code.' }) @@ -78,13 +74,13 @@ export class ProvinceController { @ApiNotFoundResponse({ description: 'If there are no match province.' }) @Get(':code') async findByCode( - @Param() params: ProvinceFindByCodeParams, + @Param() { code }: ProvinceFindByCodeParams, ): Promise { - const { code } = params; const province = await this.provinceService.findByCode(code); - if (province === null) + if (province === null) { throw new NotFoundException(`There are no province with code '${code}'`); + } return province; } @@ -118,18 +114,14 @@ export class ProvinceController { }) @Get(':code/regencies') async findRegencies( - @Param() params: ProvinceFindRegencyParams, + @Param() { code }: ProvinceFindRegencyParams, @Query() queries?: ProvinceFindRegencyQueries, ): Promise { - const { code } = params; - const { sortBy, sortOrder } = queries ?? {}; - const regencies = await this.provinceService.findRegencies(code, { - sortBy: sortBy, - sortOrder: sortOrder, - }); + const regencies = await this.provinceService.findRegencies(code, queries); - if (regencies === false) + if (regencies === null) { throw new NotFoundException(`There are no province with code '${code}'`); + } return regencies; } diff --git a/src/province/province.dto.ts b/src/province/province.dto.ts index 8df355f..6fa9db3 100644 --- a/src/province/province.dto.ts +++ b/src/province/province.dto.ts @@ -17,9 +17,9 @@ export class Province { name: string; } -export class ProvinceSortQuery extends SortQuery<'code' | 'name'> { +export class ProvinceSortQuery extends SortQuery { @EqualsAny(['code', 'name']) - sortBy: 'code' | 'name'; + readonly sortBy?: 'code' | 'name'; } export class ProvinceFindQueries extends IntersectionType( diff --git a/src/province/province.module.ts b/src/province/province.module.ts index 54efff4..600cf81 100644 --- a/src/province/province.module.ts +++ b/src/province/province.module.ts @@ -2,9 +2,10 @@ import { PrismaModule } from '@/prisma/prisma.module'; import { Module } from '@nestjs/common'; import { ProvinceController } from './province.controller'; import { ProvinceService } from './province.service'; +import { RegencyModule } from '@/regency/regency.module'; @Module({ - imports: [PrismaModule], + imports: [PrismaModule, RegencyModule], controllers: [ProvinceController], providers: [ProvinceService], }) diff --git a/src/province/province.service.ts b/src/province/province.service.ts index bbe8436..fa5292f 100644 --- a/src/province/province.service.ts +++ b/src/province/province.service.ts @@ -1,33 +1,28 @@ -import { Injectable } from '@nestjs/common'; -import { Province, Regency } from '@prisma/client'; -import { PrismaService } from '@/prisma/prisma.service'; +import { CommonService, FindOptions } from '@/common/common.service'; import { getDBProviderFeatures } from '@/common/utils/db'; +import { PrismaService } from '@/prisma/prisma.service'; +import { RegencyService } from '@/regency/regency.service'; import { SortOptions, SortService } from '@/sort/sort.service'; - -type ProvinceSortKeys = keyof Province; +import { Injectable } from '@nestjs/common'; +import { Province, Regency } from '@prisma/client'; @Injectable() -export class ProvinceService { - private readonly sortService: SortService; +export class ProvinceService implements CommonService { + readonly sorter: SortService; - constructor(private readonly prisma: PrismaService) { - this.sortService = new SortService({ + constructor( + private readonly prisma: PrismaService, + private readonly regencyService: RegencyService, + ) { + this.sorter = new SortService({ sortBy: 'code', sortOrder: 'asc', }); } - /** - * If the name is empty, all provinces will be returned. - * Otherwise, it will only return the provinces with the matching name. - * @param name Filter by province name (optional). - * @param sort The sort query (optional). - * @returns The array of provinces. - */ - async find( - name = '', - sort?: SortOptions, - ): Promise { + async find({ name, ...sortOptions }: FindOptions = {}): Promise< + Province[] + > { return this.prisma.province.findMany({ where: { name: { @@ -37,16 +32,11 @@ export class ProvinceService { }), }, }, - orderBy: this.sortService.object(sort), + orderBy: this.sorter.object(sortOptions), }); } - /** - * Find a province by its code. - * @param code The province code. - * @returns An province, or `null` if there are no match province. - */ - async findByCode(code: string): Promise { + async findByCode(code: string): Promise { return this.prisma.province.findUnique({ where: { code: code, @@ -57,22 +47,21 @@ export class ProvinceService { /** * Find all regencies in a province. * @param provinceCode The province code. - * @returns Array of regency in the match province, or `false` if there are no province found. + * @param sortOptions The sort options. + * @returns An array of regencies, or `null` if there are no match province. */ async findRegencies( provinceCode: string, - sort?: SortOptions, - ): Promise { - const regencies = await this.prisma.province + sortOptions?: SortOptions, + ): Promise { + return this.prisma.province .findUnique({ where: { code: provinceCode, }, }) .regencies({ - orderBy: this.sortService.object(sort), + orderBy: this.regencyService.sorter.object(sortOptions), }); - - return regencies ?? false; } } diff --git a/src/regency/regency.controller.ts b/src/regency/regency.controller.ts index 0ca14be..d4d22de 100644 --- a/src/regency/regency.controller.ts +++ b/src/regency/regency.controller.ts @@ -55,11 +55,7 @@ export class RegencyController { @ApiBadRequestResponse({ description: 'If there are invalid query values.' }) @Get() async find(@Query() queries?: RegencyFindQueries): Promise { - const { name, sortBy, sortOrder } = queries ?? {}; - return this.regencyService.find(name, { - sortBy: sortBy, - sortOrder: sortOrder, - }); + return this.regencyService.find(queries); } @ApiOperation({ description: 'Get a regency by its code.' }) @@ -76,12 +72,14 @@ export class RegencyController { description: 'If no regency matches the `code`.', }) @Get(':code') - async findByCode(@Param() params: RegencyFindByCodeParams): Promise { - const { code } = params; + async findByCode( + @Param() { code }: RegencyFindByCodeParams, + ): Promise { const regency = await this.regencyService.findByCode(code); - if (regency === null) + if (regency === null) { throw new NotFoundException(`There are no regency with code '${code}'`); + } return regency; } @@ -115,18 +113,14 @@ export class RegencyController { }) @Get(':code/districts') async findDistrict( - @Param() params: RegencyFindDistrictParams, + @Param() { code }: RegencyFindDistrictParams, @Query() queries?: RegencyFindDistrictQueries, ): Promise { - const { code } = params; - const { sortBy, sortOrder } = queries ?? {}; - const districts = await this.regencyService.findDistrics(code, { - sortBy: sortBy, - sortOrder: sortOrder, - }); + const districts = await this.regencyService.findDistrics(code, queries); - if (districts === false) + if (districts === null) { throw new NotFoundException(`There are no regency with code '${code}'`); + } return districts; } @@ -160,18 +154,14 @@ export class RegencyController { }) @Get(':code/islands') async findIslands( - @Param() params: RegencyFindByCodeParams, + @Param() { code }: RegencyFindByCodeParams, @Query() queries: RegencyFindIslandsQueries, ) { - const { code } = params; - const { sortBy, sortOrder } = queries ?? {}; - const islands = await this.regencyService.findIslands(code, { - sortBy: sortBy, - sortOrder: sortOrder, - }); + const islands = await this.regencyService.findIslands(code, queries); - if (islands === false) + if (islands === null) { throw new NotFoundException(`There are no regency with code '${code}'`); + } return islands; } diff --git a/src/regency/regency.dto.ts b/src/regency/regency.dto.ts index 8e72a40..9e65b0c 100644 --- a/src/regency/regency.dto.ts +++ b/src/regency/regency.dto.ts @@ -23,9 +23,9 @@ export class Regency { provinceCode: string; } -export class RegencySortQuery extends SortQuery<'code' | 'name'> { +export class RegencySortQuery extends SortQuery { @EqualsAny(['code', 'name']) - sortBy: 'code' | 'name'; + readonly sortBy?: 'code' | 'name'; } export class RegencyFindQueries extends IntersectionType( diff --git a/src/regency/regency.module.ts b/src/regency/regency.module.ts index efb3561..ceb3ab3 100644 --- a/src/regency/regency.module.ts +++ b/src/regency/regency.module.ts @@ -3,10 +3,12 @@ import { IslandModule } from '../island/island.module'; import { RegencyController } from './regency.controller'; import { RegencyService } from './regency.service'; import { PrismaModule } from '@/prisma/prisma.module'; +import { DistrictModule } from '@/district/district.module'; @Module({ - imports: [PrismaModule, IslandModule], + imports: [PrismaModule, DistrictModule, IslandModule], controllers: [RegencyController], providers: [RegencyService], + exports: [RegencyService], }) export class RegencyModule {} diff --git a/src/regency/regency.service.ts b/src/regency/regency.service.ts index ebeca59..5ccf19e 100644 --- a/src/regency/regency.service.ts +++ b/src/regency/regency.service.ts @@ -1,37 +1,31 @@ -import { Injectable } from '@nestjs/common'; -import { District, Island, Regency } from '@prisma/client'; -import { PrismaService } from '@/prisma/prisma.service'; +import { CommonService, FindOptions } from '@/common/common.service'; import { getDBProviderFeatures } from '@/common/utils/db'; +import { DistrictService } from '@/district/district.service'; +import { Island as IslandDTO } from '@/island/island.dto'; +import { IslandService } from '@/island/island.service'; +import { PrismaService } from '@/prisma/prisma.service'; import { SortOptions, SortService } from '@/sort/sort.service'; -import { IslandService, IslandSortKeys } from '../island/island.service'; - -type RegencySortKeys = keyof Regency; +import { Injectable } from '@nestjs/common'; +import { District, Island, Regency } from '@prisma/client'; @Injectable() -export class RegencyService { - private readonly sortService: SortService; +export class RegencyService implements CommonService { + readonly sorter: SortService; constructor( private readonly prisma: PrismaService, + private readonly districtService: DistrictService, private readonly islandService: IslandService, ) { - this.sortService = new SortService({ + this.sorter = new SortService({ sortBy: 'code', sortOrder: 'asc', }); } - /** - * If the name is empty, all regencies will be returned. - * Otherwise, it will only return the regencies with the matching name. - * @param name Filter by regency name (optional). - * @param sort The sort query (optional). - * @returns The array of regencies. - */ - async find( - name = '', - sort?: SortOptions, - ): Promise { + async find({ name, ...sortOptions }: FindOptions = {}): Promise< + Regency[] + > { return this.prisma.regency.findMany({ where: { name: { @@ -41,16 +35,11 @@ export class RegencyService { }), }, }, - orderBy: this.sortService.object(sort), + orderBy: this.sorter.object(sortOptions), }); } - /** - * Find a regency by its code. - * @param code The regency code. - * @returns An regency, or null if there are no match regency. - */ - async findByCode(code: string): Promise { + async findByCode(code: string): Promise { return this.prisma.regency.findUnique({ where: { code: code, @@ -61,35 +50,34 @@ export class RegencyService { /** * Find all districts in a regency. * @param regencyCode The regency code. - * @param sort The sort query (optional). - * @returns Array of districts in the match regency, or `false` if there are no regency found. + * @param sortOptions The sort options. + * @returns An array of districts, or `null` if there are no match regency. */ async findDistrics( regencyCode: string, - sort?: SortOptions, - ): Promise { - const districts = await this.prisma.regency + sortOptions?: SortOptions, + ): Promise { + return this.prisma.regency .findUnique({ where: { code: regencyCode, }, }) .districts({ - orderBy: this.sortService.object(sort), + orderBy: this.districtService.sorter.object(sortOptions), }); - - return districts ?? false; } /** * Find all islands in a regency. * @param regencyCode The regency code. - * @returns Array of islands in the match regency, or `false` if there are no regency found. + * @param sortOptions The sort options. + * @returns An array of islands, or `null` if there are no match regency. */ async findIslands( regencyCode: string, - sort?: SortOptions, - ): Promise { + sortOptions?: SortOptions, + ): Promise { const islands = await this.prisma.regency .findUnique({ where: { @@ -97,11 +85,11 @@ export class RegencyService { }, }) .islands({ - orderBy: this.islandService.sortService.object(sort), + orderBy: this.islandService.sorter.object(sortOptions), }); if (!islands) { - return false; + return null; } return islands.map(this.islandService.addDecimalCoordinate); diff --git a/src/sort/sort.dto.ts b/src/sort/sort.dto.ts index 0dafecb..9d2f3c9 100644 --- a/src/sort/sort.dto.ts +++ b/src/sort/sort.dto.ts @@ -8,12 +8,15 @@ import { SortOptions } from './sort.service'; * You may need to inherit this class and use `@EqualsAny()` decorator * for `sortBy` property to accept only specific values. */ -export class SortQuery implements SortOptions { +export class SortQuery< + T extends Record = Record, +> implements SortOptions +{ @IsOptional() @IsString() - sortBy?: T; + readonly sortBy?: keyof T; @IsOptional() @EqualsAny(['asc', 'desc']) - sortOrder?: 'asc' | 'desc'; + readonly sortOrder?: 'asc' | 'desc'; } diff --git a/src/sort/sort.service.ts b/src/sort/sort.service.ts index f3b66fb..5535796 100644 --- a/src/sort/sort.service.ts +++ b/src/sort/sort.service.ts @@ -1,12 +1,12 @@ -export interface SortOptions { - sortBy?: T; +export interface SortOptions> { + sortBy?: keyof T; sortOrder?: 'asc' | 'desc'; } /** * Service to use `sortBy` and `sortOrder` query params. */ -export class SortService { +export class SortService> { defaultOptions: Required>; /** @@ -17,22 +17,6 @@ export class SortService { this.defaultOptions = defaultOptions; } - /** - * Generate sort query string. Minus `-` sign means descending order. - * - * For example: `-code` means sort by "code" field in descending order. - * @param options The sort options. If `null`, the default options will be used. - * @returns The sort query string. - */ - query(options?: SortOptions): string { - const { - sortBy = this.defaultOptions.sortBy, - sortOrder = this.defaultOptions.sortOrder, - } = options ?? this.defaultOptions; - - return `${sortOrder === 'desc' ? '-' : ''}${sortBy}`; - } - /** * Generate sort query object. * diff --git a/src/village/village.controller.ts b/src/village/village.controller.ts index e3f5380..61831fa 100644 --- a/src/village/village.controller.ts +++ b/src/village/village.controller.ts @@ -49,11 +49,7 @@ export class VillageController { @ApiBadRequestResponse({ description: 'If there are invalid query values.' }) @Get() async find(@Query() queries?: VillageFindQueries): Promise { - const { name, sortBy, sortOrder } = queries ?? {}; - return this.villageService.find(name, { - sortBy: sortBy, - sortOrder: sortOrder, - }); + return this.villageService.find(queries); } @ApiOperation({ description: 'Get a village by its code.' }) @@ -70,12 +66,14 @@ export class VillageController { description: 'If no village matches the `code`.', }) @Get(':code') - async findByCode(@Param() params: VillageFindByCodeParams): Promise { - const { code } = params; + async findByCode( + @Param() { code }: VillageFindByCodeParams, + ): Promise { const village = await this.villageService.findByCode(code); - if (village === null) + if (village === null) { throw new NotFoundException(`There are no village with code '${code}'`); + } return village; } diff --git a/src/village/village.dto.ts b/src/village/village.dto.ts index 28c9aef..fd34522 100644 --- a/src/village/village.dto.ts +++ b/src/village/village.dto.ts @@ -21,9 +21,9 @@ export class Village { districtCode: string; } -export class VillageSortQuery extends SortQuery<'code' | 'name'> { +export class VillageSortQuery extends SortQuery { @EqualsAny(['code', 'name']) - sortBy: 'code' | 'name'; + readonly sortBy?: 'code' | 'name'; } export class VillageFindQueries extends IntersectionType( diff --git a/src/village/village.module.ts b/src/village/village.module.ts index 77c9005..72b3b0a 100644 --- a/src/village/village.module.ts +++ b/src/village/village.module.ts @@ -7,5 +7,6 @@ import { VillageService } from './village.service'; imports: [PrismaModule], controllers: [VillageController], providers: [VillageService], + exports: [VillageService], }) export class VillageModule {} diff --git a/src/village/village.service.ts b/src/village/village.service.ts index a98caeb..72bfc32 100644 --- a/src/village/village.service.ts +++ b/src/village/village.service.ts @@ -1,33 +1,24 @@ +import { CommonService, FindOptions } from '@/common/common.service'; +import { getDBProviderFeatures } from '@/common/utils/db'; +import { PrismaService } from '@/prisma/prisma.service'; +import { SortService } from '@/sort/sort.service'; import { Injectable } from '@nestjs/common'; import { Village } from '@prisma/client'; -import { SortService, SortOptions } from '@/sort/sort.service'; -import { PrismaService } from '@/prisma/prisma.service'; -import { getDBProviderFeatures } from '@/common/utils/db'; - -type VillageSortKeys = keyof Village; @Injectable() -export class VillageService { - private readonly sortHelper: SortService; +export class VillageService implements CommonService { + readonly sorter: SortService; constructor(private readonly prisma: PrismaService) { - this.sortHelper = new SortService({ + this.sorter = new SortService({ sortBy: 'code', sortOrder: 'asc', }); } - /** - * If the name is empty, all villages will be returned. - * Otherwise, it will only return the villages with the matching name. - * @param name Filter by village name (optional). - * @param sort The sort query (optional). - * @returns The array of villages. - */ - async find( - name = '', - sort?: SortOptions, - ): Promise { + async find({ name, ...sortOptions }: FindOptions = {}): Promise< + Village[] + > { return this.prisma.village.findMany({ where: { name: { @@ -37,16 +28,11 @@ export class VillageService { }), }, }, - orderBy: this.sortHelper.object(sort), + orderBy: this.sorter.object(sortOptions), }); } - /** - * Find a village by its code. - * @param code The village code. - * @returns An village, or null if there are no match village. - */ - async findByCode(code: string): Promise { + async findByCode(code: string): Promise { return this.prisma.village.findUnique({ where: { code: code,