Skip to content

Commit

Permalink
Merge pull request #114 from fityannugroho/improve-code
Browse files Browse the repository at this point in the history
Improve sorting and validation
  • Loading branch information
fityannugroho authored Jul 17, 2023
2 parents 81ff9ae + 55c5adc commit d894620
Show file tree
Hide file tree
Showing 23 changed files with 212 additions and 274 deletions.
27 changes: 27 additions & 0 deletions src/common/common.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { SortOptions, SortService } from '@/sort/sort.service';

export type CommonData = Record<string, unknown> & {
code: string;
name: string;
};

export type FindOptions<T extends CommonData> = SortOptions<T> & {
name?: string;
};

export interface CommonService<T extends CommonData> {
readonly sorter: SortService<T>;

/**
* If the name is empty, all data will be returned.
* Otherwise, it will only return the data with the matching name.
*/
find(options: FindOptions<T>): Promise<T[]>;

/**
* Find a data by its code.
*
* @returns A data or `null`.
*/
findByCode(code: string): Promise<T | null>;
}
24 changes: 8 additions & 16 deletions src/district/district.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,7 @@ export class DistrictController {
@ApiBadRequestResponse({ description: 'If there are invalid query values.' })
@Get()
async find(@Query() queries?: DistrictFindQueries): Promise<District[]> {
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.' })
Expand All @@ -76,13 +72,13 @@ export class DistrictController {
})
@Get(':code')
async findByCode(
@Param() params: DistrictFindByCodeParams,
@Param() { code }: DistrictFindByCodeParams,
): Promise<District> {
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;
}
Expand Down Expand Up @@ -116,18 +112,14 @@ export class DistrictController {
})
@Get(':code/villages')
async findVillage(
@Param() params: DistrictFindVillageParams,
@Param() { code }: DistrictFindVillageParams,
@Query() queries?: DistrictFindVillageQueries,
): Promise<Village[]> {
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;
}
Expand Down
4 changes: 2 additions & 2 deletions src/district/district.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 3 additions & 1 deletion src/district/district.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
58 changes: 23 additions & 35 deletions src/district/district.service.ts
Original file line number Diff line number Diff line change
@@ -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<DistrictSortKeys>;
export class DistrictService implements CommonService<District> {
readonly sorter: SortService<District>;

constructor(private readonly prisma: PrismaService) {
this.sortService = new SortService<DistrictSortKeys>({
constructor(
private readonly prisma: PrismaService,
private readonly villageService: VillageService,
) {
this.sorter = new SortService<District>({
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<DistrictSortKeys>,
): Promise<District[]> {
async find({ name, ...sortOptions }: FindOptions<District> = {}): Promise<
District[]
> {
return this.prisma.district.findMany({
where: {
name: {
Expand All @@ -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<District> {
async findByCode(code: string): Promise<District | null> {
return this.prisma.district.findUnique({
where: {
code: code,
Expand All @@ -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<DistrictSortKeys>,
): Promise<false | Village[]> {
const villages = await this.prisma.district
sortOptions?: SortOptions<Village>,
): Promise<Village[] | null> {
return this.prisma.district
.findUnique({
where: {
code: districtCode,
},
})
.villages({
orderBy: this.sortService.object(sort),
orderBy: this.villageService.sorter.object(sortOptions),
});

return villages ?? false;
}
}
26 changes: 13 additions & 13 deletions src/island/island.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import {
Param,
Query,
} from '@nestjs/common';
import { IslandService } from './island.service';
import { IslandFindByCodeParams, IslandFindQueries } from './island.dto';
import {
ApiBadRequestResponse,
ApiNotFoundResponse,
Expand All @@ -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')
Expand Down Expand Up @@ -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<Island[]> {
return (await this.islandService.find(queries)).map((island) =>
this.islandService.addDecimalCoordinate(island),
);
}

@ApiOperation({ description: 'Get an island by its code.' })
Expand All @@ -72,14 +73,13 @@ export class IslandController {
description: 'If no island matches the `code`.',
})
@Get(':code')
async findByCode(@Param() params: IslandFindByCodeParams): Promise<Island> {
const { code } = params;
async findByCode(@Param() { code }: IslandFindByCodeParams): Promise<Island> {
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);
}
}
8 changes: 6 additions & 2 deletions src/island/island.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
43 changes: 16 additions & 27 deletions src/island/island.service.ts
Original file line number Diff line number Diff line change
@@ -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<IslandSortKeys>;
export class IslandService implements CommonService<Island> {
readonly sorter: SortService<Island>;

constructor(private readonly prisma: PrismaService) {
this.sortService = new SortService<IslandSortKeys>({
this.sorter = new SortService<Island>({
sortBy: 'code',
sortOrder: 'asc',
});
Expand All @@ -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<IslandSortKeys>): Promise<Island[]> {
const islands = await this.prisma.island.findMany({
async find({ name, ...sortOptions }: FindOptions<Island> = {}): Promise<
Island[]
> {
return this.prisma.island.findMany({
where: {
name: {
contains: name,
Expand All @@ -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<Island | null> {
const island = await this.prisma.island.findUnique({
return this.prisma.island.findUnique({
where: {
code: code,
},
});

if (island) {
return this.addDecimalCoordinate(island);
}

return null;
}
}
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ async function bootstrap() {
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
}),
);

Expand Down
Loading

0 comments on commit d894620

Please sign in to comment.