Skip to content

Commit

Permalink
feat: add parent data into GET /{area}/{code} endpoints (#307)
Browse files Browse the repository at this point in the history
  • Loading branch information
fityannugroho authored Feb 26, 2024
1 parent f54aee0 commit 2948a21
Show file tree
Hide file tree
Showing 20 changed files with 428 additions and 77 deletions.
8 changes: 6 additions & 2 deletions src/district/district.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
District,
DistrictFindByCodeParams,
DistrictFindQueries,
DistrictWithParent,
} from './district.dto';
import { DistrictService } from './district.service';
import { ApiPaginatedResponse } from '@/common/decorator/api-paginated-response.decorator';
Expand Down Expand Up @@ -48,15 +49,18 @@ export class DistrictController {
}

@ApiOperation({ description: 'Get a district by its code.' })
@ApiDataResponse({ model: District, description: 'Returns a district.' })
@ApiDataResponse({
model: DistrictWithParent,
description: 'Returns a district.',
})
@ApiBadRequestResponse({ description: 'If the `code` is invalid.' })
@ApiNotFoundResponse({
description: 'If no district matches the `code`.',
})
@Get(':code')
async findByCode(
@Param() { code }: DistrictFindByCodeParams,
): Promise<District> {
): Promise<DistrictWithParent> {
const district = await this.districtService.findByCode(code);

if (district === null) {
Expand Down
9 changes: 9 additions & 0 deletions src/district/district.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
} from '@nestjs/swagger';
import { IsNotEmpty, IsNumberString, Length, MaxLength } from 'class-validator';
import { PaginationQuery } from '@/common/dto/pagination.dto';
import { Regency } from '@/regency/regency.dto';
import { Province } from '@/province/province.dto';

export class District {
@IsNotEmpty()
Expand Down Expand Up @@ -46,3 +48,10 @@ export class DistrictFindQueries extends IntersectionType(
export class DistrictFindByCodeParams extends PickType(District, [
'code',
] as const) {}

export class DistrictWithParent extends District {
parent: {
regency: Regency;
province: Province;
};
}
44 changes: 32 additions & 12 deletions src/district/district.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { getDistricts } from '@common/utils/data';
import { getDistricts, getProvinces, getRegencies } from '@common/utils/data';
import { getDBProviderFeatures } from '@common/utils/db';
import { SortOrder } from '@/sort/sort.dto';
import { Test, TestingModule } from '@nestjs/testing';
import { District } from '@prisma/client';
import { District, Province, Regency } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { DistrictService } from './district.service';
import { mockPrismaService } from '@/prisma/__mocks__/prisma.service';

describe('DistrictService', () => {
let districts: District[];
let regencies: Regency[];
let provinces: Province[];
let service: DistrictService;
let prismaService: PrismaService;

beforeAll(async () => {
districts = await getDistricts();
regencies = await getRegencies();
provinces = await getProvinces();
});

beforeEach(async () => {
Expand Down Expand Up @@ -159,31 +163,47 @@ describe('DistrictService', () => {
const result = await service.findByCode(testCode);

expect(findUniqueSpy).toHaveBeenCalledTimes(1);
expect(findUniqueSpy).toHaveBeenCalledWith({
where: {
code: testCode,
},
});
expect(findUniqueSpy).toHaveBeenCalledWith(
expect.objectContaining({ where: { code: testCode } }),
);
expect(result).toBeNull();
});

it('should return a district', async () => {
const testCode = '110101';
const expectedDistrict = districts.find((d) => d.code === testCode);
const expectedRegency = regencies.find(
(r) => r.code === expectedDistrict.regencyCode,
);
const expectedProvince = provinces.find(
(p) => p.code === expectedRegency.provinceCode,
);

const findUniqueSpy = vitest
.spyOn(prismaService.district, 'findUnique')
.mockResolvedValue(expectedDistrict);
.mockResolvedValue({
...expectedDistrict,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
regency: {
...expectedRegency,
province: expectedProvince,
},
});

const result = await service.findByCode(testCode);

expect(findUniqueSpy).toHaveBeenCalledTimes(1);
expect(findUniqueSpy).toHaveBeenCalledWith({
where: {
code: testCode,
expect(findUniqueSpy).toHaveBeenCalledWith(
expect.objectContaining({ where: { code: testCode } }),
);
expect(result).toEqual({
...expectedDistrict,
parent: {
regency: expectedRegency,
province: expectedProvince,
},
});
expect(result).toEqual(expectedDistrict);
});
});
});
27 changes: 24 additions & 3 deletions src/district/district.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { PrismaService } from '@/prisma/prisma.service';
import { SortService } from '@/sort/sort.service';
import { Injectable } from '@nestjs/common';
import { District } from '@prisma/client';
import { DistrictFindQueries } from './district.dto';
import { DistrictFindQueries, DistrictWithParent } from './district.dto';

@Injectable()
export class DistrictService {
Expand Down Expand Up @@ -44,9 +44,30 @@ export class DistrictService {
});
}

async findByCode(code: string): Promise<District | null> {
return this.prisma.district.findUnique({
async findByCode(code: string): Promise<DistrictWithParent | null> {
const res = await this.prisma.district.findUnique({
where: { code },
include: {
regency: {
include: {
province: true,
},
},
},
});

if (!res) {
return null;
}

const {
regency: { province, ...regency },
...district
} = res;

return {
...district,
parent: { regency, province },
};
}
}
12 changes: 9 additions & 3 deletions src/island/island.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
Island,
IslandFindByCodeParams,
IslandFindQueries,
IslandWithParent,
} from './island.dto';
import { IslandService } from './island.service';
import { ApiPaginatedResponse } from '@/common/decorator/api-paginated-response.decorator';
Expand Down Expand Up @@ -55,19 +56,24 @@ export class IslandController {
}

@ApiOperation({ description: 'Get an island by its code.' })
@ApiDataResponse({ model: Island, description: 'Returns an island.' })
@ApiDataResponse({
model: IslandWithParent,
description: 'Returns an island.',
})
@ApiBadRequestResponse({ description: 'If the `code` is invalid.' })
@ApiNotFoundResponse({
description: 'If no island matches the `code`.',
})
@Get(':code')
async findByCode(@Param() { code }: IslandFindByCodeParams): Promise<Island> {
async findByCode(
@Param() { code }: IslandFindByCodeParams,
): Promise<IslandWithParent> {
const island = await this.islandService.findByCode(code);

if (island === null) {
throw new NotFoundException(`Island with code ${code} not found.`);
}

return this.islandService.addDecimalCoordinate(island);
return island;
}
}
11 changes: 10 additions & 1 deletion src/island/island.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
import { EqualsAny } from '../common/decorator/EqualsAny';
import { IsNotSymbol } from '../common/decorator/IsNotSymbol';
import { PaginationQuery } from '@/common/dto/pagination.dto';
import { Regency } from '@/regency/regency.dto';
import { Province } from '@/province/province.dto';

export class Island {
@IsNotEmpty()
Expand Down Expand Up @@ -59,7 +61,7 @@ export class Island {
Providing an empty string will filter islands that are not part of any regency.`,
example: '1101',
})
regencyCode?: string;
regencyCode?: string | null;

@ApiProperty({ example: 3.317622222222222 })
latitude?: number;
Expand All @@ -82,3 +84,10 @@ export class IslandFindQueries extends IntersectionType(
export class IslandFindByCodeParams extends PickType(Island, [
'code',
] as const) {}

export class IslandWithParent extends Island {
parent: {
regency?: Regency | null;
province: Province;
};
}
92 changes: 83 additions & 9 deletions src/island/island.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Test, TestingModule } from '@nestjs/testing';
import { IslandService } from './island.service';
import { PrismaService } from '@/prisma/prisma.service';
import { Island } from '@prisma/client';
import { Island, Province, Regency } from '@prisma/client';
import { getDBProviderFeatures } from '@common/utils/db';
import { SortOrder } from '@/sort/sort.dto';
import { mockPrismaService } from '@/prisma/__mocks__/prisma.service';
import { getProvinces, getRegencies } from '@common/utils/data';

const islands: readonly Island[] = [
{
Expand Down Expand Up @@ -60,14 +61,24 @@ const islands: readonly Island[] = [
describe('IslandService', () => {
let service: IslandService;
let prismaService: PrismaService;
let provinces: Province[];
let regencies: Regency[];

beforeAll(async () => {
provinces = await getProvinces();
regencies = await getRegencies();
});

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
IslandService,
{
provide: PrismaService,
useValue: mockPrismaService('Island', islands),
useValue: {
...mockPrismaService('Island', islands),
province: mockPrismaService('Province', provinces).province,
},
},
],
}).compile();
Expand Down Expand Up @@ -213,18 +224,79 @@ describe('IslandService', () => {
it('should return an island', async () => {
const testCode = '110140001';
const expectedIsland = islands.find((i) => i.code === testCode);
const expectedRegency = regencies.find(
(r) => r.code === expectedIsland.regencyCode,
);
const expectedProvince = provinces.find(
(p) => p.code === expectedRegency.provinceCode,
);

const findUniqueSpy = vitest
.spyOn(prismaService.island, 'findUnique')
.mockResolvedValue(expectedIsland);
.mockResolvedValue({
...expectedIsland,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
regency: { ...expectedRegency, province: expectedProvince },
});

const result = await service.findByCode(testCode);

expect(findUniqueSpy).toHaveBeenCalledTimes(1);
expect(findUniqueSpy).toHaveBeenCalledWith({
where: { code: testCode },
expect(findUniqueSpy).toHaveBeenCalledWith(
expect.objectContaining({
where: { code: testCode },
}),
);
expect(result).toEqual({
...service.addDecimalCoordinate(expectedIsland),
parent: {
regency: expectedRegency,
province: expectedProvince,
},
});
});

it('should return an island without regency', async () => {
const testCode = '120040001';
const expectedIsland = islands.find((i) => i.code === testCode);
const expectedProvince = provinces.find(
(p) => p.code === testCode.slice(0, 2),
);

const findUniqueIslandSpy = vitest
.spyOn(prismaService.island, 'findUnique')
.mockResolvedValue({
...expectedIsland,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
regency: null,
});

const findUniqueProvinceSpy = vitest
.spyOn(prismaService.province, 'findUnique')
.mockResolvedValue(expectedProvince);

const result = await service.findByCode(testCode);

expect(findUniqueIslandSpy).toHaveBeenCalledTimes(1);
expect(findUniqueIslandSpy).toHaveBeenCalledWith(
expect.objectContaining({
where: { code: testCode },
}),
);
expect(findUniqueProvinceSpy).toHaveBeenCalledWith(
expect.objectContaining({
where: { code: testCode.slice(0, 2) },
}),
);
expect(result).toEqual({
...service.addDecimalCoordinate(expectedIsland),
parent: {
regency: null,
province: expectedProvince,
},
});
expect(result).toEqual(expectedIsland);
});

it('should return null if the island is not found', async () => {
Expand All @@ -237,9 +309,11 @@ describe('IslandService', () => {
const result = await service.findByCode(testCode);

expect(findUniqueSpy).toHaveBeenCalledTimes(1);
expect(findUniqueSpy).toHaveBeenCalledWith({
where: { code: testCode },
});
expect(findUniqueSpy).toHaveBeenCalledWith(
expect.objectContaining({
where: { code: testCode },
}),
);
expect(result).toBeNull();
});
});
Expand Down
Loading

0 comments on commit 2948a21

Please sign in to comment.