diff --git a/docs/upgrading/upgrade-to-v4.md b/docs/upgrading/upgrade-to-v4.md new file mode 100644 index 0000000..eaf353c --- /dev/null +++ b/docs/upgrading/upgrade-to-v4.md @@ -0,0 +1,197 @@ +

Upgrade to idn-area version 4

+ +## Changed response format + +Every endpoint now returns an object with the following properties: + +| Property | Type | Description | Available On | +| --- | --- | --- | --- | +| `statusCode` | `number` | HTTP status code | Always | +| `message` | `string` or array of `string` | The message of the response | Always | +| `error` | `string` | The kind of error | Error | +| `data` | `object` or array of `object` | The data of the response | Success | +| `meta` | `object` or `undefined` | The meta data of the response (optional) | Success | + +For example: + +- Get a province (success response) + + ``` + GET /provinces/32 + ``` + ```json + { + "statusCode": 200, + "message": "OK", + "data": { + "code": "32", + "name": "JAWA BARAT" + } + } + ``` + +- Get provinces (success response) + + ``` + GET /provinces + ``` + ```json + { + "statusCode": 200, + "message": "OK", + "data": [ + { + "code": "11", + "name": "ACEH" + }, + { + "code": "12", + "name": "SUMATERA UTARA" + }, + ... + ], + "meta": { + "total": 10, + "pagination": { + "total": 37, + "pages": { + "first": 1, + "last": 2, + "current": 1, + "previous": null, + "next": 2 + } + } + } + } + ``` + + > The endpoint above implements new pagination feature. See [pagination](#pagination) for details. + +- A bad request (error response) + + ``` + GET /provinces/ab + ``` + ```json + { + "statusCode": 400, + "message": ["code must be a number string"], + "error": "Bad Request" + } + ``` + +## Removed endpoints + +The following endpoints have been removed: + +- `GET /provinces/{code}/regencies` +- `GET /regencies/{code}/districts` +- `GET /regencies/{code}/islands` +- `GET /districts/{code}/villages` + +You need to use the equivalent endpoints with the [`parentCode` query](#new-parentcode-query). + +> [!WARNING] +> If you try to access the removed endpoints above, you will get a `404 Not Found` response. + +## New `parentCode` query + +The `parentCode` query parameter is added to the following endpoints: + +| Endpoint | `parentCode` Query | Example | +| --- | --- | --- | +| `GET /regencies` | `provinceCode` | `GET /regencies?provinceCode=32` | +| `GET /districts` | `regencyCode` | `GET /districts?regencyCode=3201` | +| `GET /islands` | `regencyCode` | `GET /islands?regencyCode=3201` | +| `GET /villages` | `districtCode` | `GET /villages?districtCode=3201010` | + +This table below shows the [removed endpoints](#removed-endpoints) and its equivalent endpoints using the `parentCode` query. + +| Deleted Endpoint | Equivalent Endpoint | +|--------|--------| +| `GET /provinces/{code}/regencies` | **`GET /regencies?provinceCode={code}`** | +| `GET /regencies/{code}/districts` | **`GET /districts?regencyCode={code}`** | +| `GET /regencies/{code}/islands` | **`GET /islands?regencyCode={code}`** | +| `GET /districts/{code}/villages` | **`GET /villages?districtCode={code}`** | + +Below is an example to get all regencies in province with code `32`: + +``` +GET /regencies?provinceCode=32 +``` +```json +{ + "statusCode": 200, + "message": "OK", + "data": [ + { + "code": "3201", + "name": "KAB. BOGOR", + "provinceCode": "32" + }, + { + "code": "3202", + "name": "KAB. SUKABUMI", + "provinceCode": "32" + }, + ... + ], + "meta": { + "total": 10, + "pagination": { + "total": 27, + "pages": { + "first": 1, + "last": 3, + "current": 1, + "previous": null, + "next": 2 + } + } + } +} +``` + +> The endpoint above implements new pagination feature. See [pagination](#pagination) for details. + +## `name` query now optional + +Before version 4, the `name` query parameter is required, so you can't get all data without specifying the `name` query parameter. + +Now, the `name` query parameter is optional. This change affects the following endpoints: + +- `GET /regencies` +- `GET /districts` +- `GET /islands` +- `GET /villages` + +## Pagination + +We introduce a new pagination feature. This feature is implemented in the following endpoints: + +- `GET /provinces` +- `GET /regencies` +- `GET /districts` +- `GET /islands` +- `GET /villages` + +You can use the **`page` and `limit`** query parameters to specify the page number and the number of data per page. If you don't specify the `page` and `limit` query parameters, the default value will be used (`page=1` and `limit=10`). + +``` +GET /provinces?page=2&limit=37 +``` + +The response will contain a `meta` object with the following properties: + +| Property | Type | Description | +| --- | --- | --- | +| `total` | `number` | The total number of data | +| `pagination` | `object` | | +| `pagination.total` | `number` | The number of available data with the current query | +| `pagination.pages` | `object` | | +| `pagination.pages.first` | `number` | The first page number | +| `pagination.pages.last` | `number` | The last page number | +| `pagination.pages.current` | `number` or `null` | The current page number or `null` if the `page` query is exceeded the last page | +| `pagination.pages.previous` | `number` or `null` | The previous page number or `null` if the current page is the first page | +| `pagination.pages.next` | `number` or `null` | The next page number or `null` if the current page is the last page | diff --git a/src/district/__mocks__/district.service.ts b/src/district/__mocks__/district.service.ts index 96f79f0..b569673 100644 --- a/src/district/__mocks__/district.service.ts +++ b/src/district/__mocks__/district.service.ts @@ -1,15 +1,12 @@ import { sortArray } from '@/common/utils/array'; -import { SortOptions } from '@/sort/sort.service'; -import { District, Village } from '@prisma/client'; +import { District } from '@prisma/client'; import { DistrictFindQueries } from '../district.dto'; export class MockDistrictService { readonly districts: District[]; - readonly villages: Village[]; - constructor(districts: District[], villages: Village[]) { + constructor(districts: District[]) { this.districts = districts; - this.villages = villages; } async find({ @@ -34,19 +31,4 @@ export class MockDistrictService { this.districts.find((district) => district.code === code) ?? null, ); } - - async findVillages( - districtCode: string, - { sortBy = 'code', sortOrder }: SortOptions = {}, - ) { - if (this.districts.every((p) => p.code !== districtCode)) { - return null; - } - - const res = this.villages.filter( - (village) => village.districtCode === districtCode, - ); - - return Promise.resolve({ data: sortArray(res, sortBy, sortOrder) }); - } } diff --git a/src/district/district.controller.spec.ts b/src/district/district.controller.spec.ts index 6277273..9433be1 100644 --- a/src/district/district.controller.spec.ts +++ b/src/district/district.controller.spec.ts @@ -1,9 +1,9 @@ import { getValues, sortArray } from '@/common/utils/array'; -import { getDistricts, getVillages } from '@/common/utils/data'; +import { getDistricts } from '@/common/utils/data'; import { SortOrder } from '@/sort/sort.dto'; import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { District, Village } from '@prisma/client'; +import { District } from '@prisma/client'; import { MockDistrictService } from './__mocks__/district.service'; import { DistrictController } from './district.controller'; import { DistrictService } from './district.service'; @@ -12,12 +12,10 @@ describe('DistrictController', () => { const testDistrictCode = '110101'; let districts: District[]; - let villages: Village[]; let controller: DistrictController; beforeAll(async () => { districts = await getDistricts(); - villages = await getVillages(); }); beforeEach(async () => { @@ -26,7 +24,7 @@ describe('DistrictController', () => { providers: [ { provide: DistrictService, - useValue: new MockDistrictService(districts, villages), + useValue: new MockDistrictService(districts), }, ], }).compile(); @@ -149,64 +147,4 @@ describe('DistrictController', () => { ).rejects.toThrowError(NotFoundException); }); }); - - describe('findVillages', () => { - let expectedVillages: Village[]; - - beforeAll(() => { - expectedVillages = villages.filter( - (p) => p.districtCode === testDistrictCode, - ); - }); - - it('should return all villages in the matching district', async () => { - const { data } = await controller.findVillages({ - code: testDistrictCode, - }); - - for (const village of data) { - expect(village).toEqual( - expect.objectContaining({ - code: expect.stringMatching( - new RegExp(`^${testDistrictCode}\\d{4}$`), - ), - name: expect.any(String), - districtCode: testDistrictCode, - }), - ); - } - - expect(data).toHaveLength( - villages.filter((p) => p.districtCode === testDistrictCode).length, - ); - }); - - it('should throw NotFoundException if there is no matching district', async () => { - await expect( - controller.findVillages({ code: '000000' }), - ).rejects.toThrowError(NotFoundException); - }); - - it('should return all villages in the matching district sorted by name ascending', async () => { - const { data } = await controller.findVillages( - { code: testDistrictCode }, - { sortBy: 'name' }, - ); - - expect(getValues(data, 'code')).toEqual( - getValues(sortArray(expectedVillages, 'name'), 'code'), - ); - }); - - it('should return all villages in the matching district sorted by name descending', async () => { - const { data } = await controller.findVillages( - { code: testDistrictCode }, - { sortBy: 'name', sortOrder: SortOrder.DESC }, - ); - - expect(getValues(data, 'code')).toEqual( - getValues(sortArray(expectedVillages, 'name', SortOrder.DESC), 'code'), - ); - }); - }); }); diff --git a/src/district/district.controller.ts b/src/district/district.controller.ts index 4aef19a..da37efd 100644 --- a/src/district/district.controller.ts +++ b/src/district/district.controller.ts @@ -1,5 +1,4 @@ import { ApiDataResponse } from '@/common/decorator/api-data-response.decorator'; -import { Village } from '@/village/village.dto'; import { Controller, Get, @@ -18,8 +17,6 @@ import { District, DistrictFindByCodeParams, DistrictFindQueries, - DistrictFindVillageParams, - DistrictFindVillageQueries, } from './district.dto'; import { DistrictService } from './district.service'; import { ApiPaginatedResponse } from '@/common/decorator/api-paginated-response.decorator'; @@ -30,7 +27,7 @@ import { PaginatedReturn } from '@/common/interceptor/paginate.interceptor'; export class DistrictController { constructor(private readonly districtService: DistrictService) {} - @ApiOperation({ description: 'Get districts by its name.' }) + @ApiOperation({ description: 'Get the districts.' }) @ApiQuery({ name: 'sortBy', description: 'Sort by district code or name.', @@ -68,32 +65,4 @@ export class DistrictController { return district; } - - @ApiOperation({ description: 'Get all villages in a district.' }) - @ApiQuery({ - name: 'sortBy', - description: 'Sort villages by its code or name.', - required: false, - type: 'string', - example: 'code', - }) - @ApiPaginatedResponse({ - model: Village, - description: 'Returns array of villages.', - }) - @ApiBadRequestResponse({ description: 'If the `code` is invalid.' }) - @ApiNotFoundResponse({ - description: 'If there are no district match with the `code`.', - }) - @Get(':code/villages') - async findVillages( - @Param() { code }: DistrictFindVillageParams, - @Query() queries?: DistrictFindVillageQueries, - ): Promise> { - if ((await this.districtService.findByCode(code)) === null) { - throw new NotFoundException(`There are no district with code '${code}'`); - } - - return this.districtService.findVillages(code, queries); - } } diff --git a/src/district/district.dto.ts b/src/district/district.dto.ts index 1eb31e4..ce01d42 100644 --- a/src/district/district.dto.ts +++ b/src/district/district.dto.ts @@ -8,7 +8,6 @@ import { PickType, } from '@nestjs/swagger'; import { IsNotEmpty, IsNumberString, Length } from 'class-validator'; -import { VillageSortQuery } from '../village/village.dto'; import { PaginationQuery } from '@/common/dto/pagination.dto'; export class District { @@ -48,12 +47,3 @@ export class DistrictFindQueries extends IntersectionType( export class DistrictFindByCodeParams extends PickType(District, [ 'code', ] as const) {} - -export class DistrictFindVillageParams extends PickType(District, [ - 'code', -] as const) {} - -export class DistrictFindVillageQueries extends IntersectionType( - VillageSortQuery, - PaginationQuery, -) {} diff --git a/src/district/district.service.spec.ts b/src/district/district.service.spec.ts index e4b7709..fce24a2 100644 --- a/src/district/district.service.spec.ts +++ b/src/district/district.service.spec.ts @@ -1,31 +1,23 @@ +import { getDistricts } from '@/common/utils/data'; +import { getDBProviderFeatures } from '@/common/utils/db'; +import { SortOrder } from '@/sort/sort.dto'; import { Test, TestingModule } from '@nestjs/testing'; -import { District, Village } from '@prisma/client'; +import { District } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { DistrictService } from './district.service'; -import { VillageService } from '@/village/village.service'; -import { getDBProviderFeatures } from '@/common/utils/db'; -import { SortOrder } from '@/sort/sort.dto'; - -const districts: readonly District[] = [ - { code: '110101', name: 'Bakongan', regencyCode: '1101' }, - { code: '110102', name: 'Kluet Utara', regencyCode: '1101' }, - { code: '110103', name: 'Kluet Selatan', regencyCode: '1101' }, -]; - -const villages: readonly Village[] = [ - { code: '1101012001', name: 'Desa 1', districtCode: '110101' }, - { code: '1101012002', name: 'Desa 2', districtCode: '110101' }, - { code: '1212121001', name: 'Kampung Karet', districtCode: '121212' }, - { code: '1212121002', name: 'Kampung Berkah', districtCode: '121212' }, -] as const; describe('DistrictService', () => { + let districts: District[]; let service: DistrictService; let prismaService: PrismaService; + beforeAll(async () => { + districts = await getDistricts(); + }); + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [DistrictService, PrismaService, VillageService], + providers: [DistrictService, PrismaService], }).compile(); service = module.get(DistrictService); @@ -187,43 +179,4 @@ describe('DistrictService', () => { expect(result).toEqual(expectedDistrict); }); }); - - describe('findVillages', async () => { - const getPaginatorOptions = (testCode: string) => ({ - model: 'Village', - paginate: { page: undefined, limit: undefined }, - args: { where: { districtCode: testCode } }, - }); - - it('should return empty array if there is no match district code', async () => { - const testCode = '999999'; - - const paginatorSpy = vitest - .spyOn(prismaService, 'paginator') - .mockResolvedValue({ data: [] }); - - const result = await service.findVillages(testCode); - - expect(paginatorSpy).toHaveBeenCalledTimes(1); - expect(paginatorSpy).toHaveBeenCalledWith(getPaginatorOptions(testCode)); - expect(result.data).toEqual([]); - }); - - it('should return all villages in a district', async () => { - const testCode = '110101'; - const expectedVillages = villages.filter( - (v) => v.districtCode === testCode, - ); - - const paginatorSpy = vitest - .spyOn(prismaService, 'paginator') - .mockResolvedValue({ data: expectedVillages }); - - const result = await service.findVillages(testCode); - - expect(paginatorSpy).toHaveBeenCalledTimes(1); - expect(paginatorSpy).toHaveBeenCalledWith(getPaginatorOptions(testCode)); - expect(result.data).toEqual(expectedVillages); - }); - }); }); diff --git a/src/district/district.service.ts b/src/district/district.service.ts index 7db47c1..362feab 100644 --- a/src/district/district.service.ts +++ b/src/district/district.service.ts @@ -1,21 +1,16 @@ -import { PaginationQuery } from '@/common/dto/pagination.dto'; import { PaginatedReturn } from '@/common/interceptor/paginate.interceptor'; import { getDBProviderFeatures } from '@/common/utils/db'; import { PrismaService } from '@/prisma/prisma.service'; -import { SortOptions, SortService } from '@/sort/sort.service'; -import { VillageService } from '@/village/village.service'; +import { SortService } from '@/sort/sort.service'; import { Injectable } from '@nestjs/common'; -import { District, Village } from '@prisma/client'; +import { District } from '@prisma/client'; import { DistrictFindQueries } from './district.dto'; @Injectable() export class DistrictService { readonly sorter: SortService; - constructor( - private readonly prisma: PrismaService, - private readonly villageService: VillageService, - ) { + constructor(private readonly prisma: PrismaService) { this.sorter = new SortService({ sortBy: 'code', sortOrder: 'asc', @@ -54,28 +49,4 @@ export class DistrictService { where: { code }, }); } - - /** - * Find all villages in a district. - * @param districtCode The district code. - * @param options The options. - * @returns Paginated array of villages, `[]` if there are no match district. - */ - async findVillages( - districtCode: string, - options?: SortOptions & PaginationQuery, - ): Promise> { - const { page, limit, sortBy, sortOrder } = options ?? {}; - - return this.prisma.paginator({ - model: 'Village', - paginate: { page, limit }, - args: { - where: { districtCode }, - ...((sortBy || sortOrder) && { - orderBy: this.villageService.sorter.object({ sortBy, sortOrder }), - }), - }, - }); - } } diff --git a/src/island/island.controller.ts b/src/island/island.controller.ts index 50af02a..7b9aff1 100644 --- a/src/island/island.controller.ts +++ b/src/island/island.controller.ts @@ -28,7 +28,7 @@ export class IslandController { constructor(private readonly islandService: IslandService) {} @ApiOperation({ - description: 'Get the islands by its name.', + description: 'Get the islands.', }) @ApiQuery({ name: 'sortBy', diff --git a/src/province/__mocks__/province.service.ts b/src/province/__mocks__/province.service.ts index a403744..783aa9d 100644 --- a/src/province/__mocks__/province.service.ts +++ b/src/province/__mocks__/province.service.ts @@ -1,15 +1,12 @@ import { sortArray } from '@/common/utils/array'; -import { SortOptions } from '@/sort/sort.service'; -import { Province, Regency } from '@prisma/client'; +import { Province } from '@prisma/client'; import { ProvinceFindQueries } from '../province.dto'; export class MockProvinceService { readonly provinces: readonly Province[]; - readonly regencies: readonly Regency[]; - constructor(provinces: Province[], regencies: Regency[]) { + constructor(provinces: Province[]) { this.provinces = provinces; - this.regencies = regencies; } async find({ @@ -29,15 +26,4 @@ export class MockProvinceService { this.provinces.find((province) => province.code === code) ?? null, ); } - - async findRegencies( - provinceCode: string, - { sortBy = 'code', sortOrder }: SortOptions = {}, - ) { - const res = this.regencies.filter( - (regency) => regency.provinceCode === provinceCode, - ); - - return Promise.resolve({ data: sortArray(res, sortBy, sortOrder) }); - } } diff --git a/src/province/province.controller.spec.ts b/src/province/province.controller.spec.ts index b4ee5a8..5c65615 100644 --- a/src/province/province.controller.spec.ts +++ b/src/province/province.controller.spec.ts @@ -1,9 +1,9 @@ import { getValues, sortArray } from '@/common/utils/array'; -import { getProvinces, getRegencies } from '@/common/utils/data'; +import { getProvinces } from '@/common/utils/data'; import { SortOrder } from '@/sort/sort.dto'; import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Province, Regency } from '@prisma/client'; +import { Province } from '@prisma/client'; import { MockProvinceService } from './__mocks__/province.service'; import { ProvinceController } from './province.controller'; import { ProvinceService } from './province.service'; @@ -13,11 +13,9 @@ describe('ProvinceController', () => { let controller: ProvinceController; let provinces: Province[]; - let regencies: Regency[]; beforeAll(async () => { provinces = await getProvinces(); - regencies = await getRegencies(); }); beforeEach(async () => { @@ -26,7 +24,7 @@ describe('ProvinceController', () => { providers: [ { provide: ProvinceService, - useValue: new MockProvinceService(provinces, regencies), + useValue: new MockProvinceService(provinces), }, ], }).compile(); @@ -113,61 +111,4 @@ describe('ProvinceController', () => { ); }); }); - - describe('findRegencies', () => { - let expectedRegencies: Regency[]; - - beforeAll(() => { - expectedRegencies = regencies.filter( - (r) => r.provinceCode === testProvCode, - ); - }); - - it('should return all regencies in the matching province', async () => { - const { data } = await controller.findRegencies({ - code: testProvCode, - }); - - for (const regency of data) { - expect(regency).toEqual( - expect.objectContaining({ - code: expect.stringMatching(new RegExp(`^${testProvCode}\\d{2}$`)), - name: expect.any(String), - provinceCode: testProvCode, - }), - ); - } - - expect(data).toEqual(expect.arrayContaining([])); - expect(data).toHaveLength(expectedRegencies.length); - }); - - it('should throw NotFoundException if there is no matching province', async () => { - await expect( - controller.findRegencies({ code: '00' }), - ).rejects.toThrowError(NotFoundException); - }); - - it('should return all regencies in the matching province sorted by name ascending', async () => { - const { data } = await controller.findRegencies( - { code: testProvCode }, - { sortBy: 'name', sortOrder: SortOrder.ASC }, - ); - - expect(getValues(data, 'code')).toEqual( - getValues(sortArray(expectedRegencies, 'name'), 'code'), - ); - }); - - it('should return all regencies in the matching province sorted by name descending', async () => { - const { data } = await controller.findRegencies( - { code: testProvCode }, - { sortBy: 'name', sortOrder: SortOrder.DESC }, - ); - - expect(getValues(data, 'code')).toEqual( - getValues(sortArray(expectedRegencies, 'name', SortOrder.DESC), 'code'), - ); - }); - }); }); diff --git a/src/province/province.controller.ts b/src/province/province.controller.ts index 70217d6..566ab9a 100644 --- a/src/province/province.controller.ts +++ b/src/province/province.controller.ts @@ -1,5 +1,4 @@ import { ApiDataResponse } from '@/common/decorator/api-data-response.decorator'; -import { Regency } from '@/regency/regency.dto'; import { Controller, Get, @@ -18,8 +17,6 @@ import { Province, ProvinceFindByCodeParams, ProvinceFindQueries, - ProvinceFindRegencyParams, - ProvinceFindRegencyQueries, } from './province.dto'; import { ProvinceService } from './province.service'; import { PaginatedReturn } from '@/common/interceptor/paginate.interceptor'; @@ -69,32 +66,4 @@ export class ProvinceController { return province; } - - @ApiOperation({ description: 'Get all regencies in a province.' }) - @ApiQuery({ - name: 'sortBy', - description: 'Sort regencies by its code or name.', - required: false, - type: 'string', - example: 'code', - }) - @ApiPaginatedResponse({ - model: Regency, - description: 'Returns array of regencies.', - }) - @ApiBadRequestResponse({ description: 'If the `code` is invalid.' }) - @ApiNotFoundResponse({ - description: 'If there are no province match with the `code`.', - }) - @Get(':code/regencies') - async findRegencies( - @Param() { code }: ProvinceFindRegencyParams, - @Query() queries?: ProvinceFindRegencyQueries, - ): Promise> { - if ((await this.provinceService.findByCode(code)) === null) { - throw new NotFoundException(`There are no province with code '${code}'`); - } - - return this.provinceService.findRegencies(code, queries); - } } diff --git a/src/province/province.dto.ts b/src/province/province.dto.ts index ed1510b..8eac025 100644 --- a/src/province/province.dto.ts +++ b/src/province/province.dto.ts @@ -8,7 +8,6 @@ import { PickType, } from '@nestjs/swagger'; import { IsNotEmpty, IsNumberString, Length } from 'class-validator'; -import { RegencySortQuery } from '../regency/regency.dto'; import { PaginationQuery } from '@/common/dto/pagination.dto'; export class Province { @@ -39,12 +38,3 @@ export class ProvinceFindQueries extends IntersectionType( export class ProvinceFindByCodeParams extends PickType(Province, [ 'code', ] as const) {} - -export class ProvinceFindRegencyParams extends PickType(Province, [ - 'code', -] as const) {} - -export class ProvinceFindRegencyQueries extends IntersectionType( - RegencySortQuery, - PaginationQuery, -) {} diff --git a/src/province/province.service.spec.ts b/src/province/province.service.spec.ts index ee3b083..c747117 100644 --- a/src/province/province.service.spec.ts +++ b/src/province/province.service.spec.ts @@ -1,45 +1,23 @@ +import { getProvinces } from '@/common/utils/data'; import { getDBProviderFeatures } from '@/common/utils/db'; -import { DistrictService } from '@/district/district.service'; -import { IslandService } from '@/island/island.service'; import { PrismaService } from '@/prisma/prisma.service'; -import { RegencyService } from '@/regency/regency.service'; import { SortOrder } from '@/sort/sort.dto'; -import { VillageService } from '@/village/village.service'; import { Test, TestingModule } from '@nestjs/testing'; -import { Province, Regency } from '@prisma/client'; +import { Province } from '@prisma/client'; import { ProvinceService } from './province.service'; -const provinces: readonly Province[] = [ - { code: '11', name: 'ACEH' }, - { code: '12', name: 'SUMATERA UTARA' }, - { code: '32', name: 'JAWA BARAT' }, - { code: '33', name: 'JAWA TENGAH' }, - { code: '34', name: 'DI YOGYAKARTA' }, - { code: '35', name: 'JAWA TIMUR' }, -] as const; - -const regencies: readonly Regency[] = [ - { code: '1101', name: 'KABUPATEN ACEH SELATAN', provinceCode: '11' }, - { code: '1102', name: 'KABUPATEN ACEH TENGGARA', provinceCode: '11' }, - { code: '1201', name: 'KABUPATEN TAPANULI TENGAH', provinceCode: '12' }, - { code: '1202', name: 'KABUPATEN TAPANULI UTARA', provinceCode: '12' }, - { code: '1271', name: 'KOTA MEDAN', provinceCode: '12' }, -] as const; - describe('ProvinceService', () => { + let provinces: readonly Province[]; let provinceService: ProvinceService; let prismaService: PrismaService; + beforeAll(async () => { + provinces = await getProvinces(); + }); + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [ - ProvinceService, - PrismaService, - RegencyService, - DistrictService, - IslandService, - VillageService, - ], + providers: [ProvinceService, PrismaService], }).compile(); provinceService = module.get(ProvinceService); @@ -176,58 +154,4 @@ describe('ProvinceService', () => { expect(result).toBeNull(); }); }); - - describe('findRegencies', () => { - const getPaginatorOptions = (testCode: string) => ({ - model: 'Regency', - paginate: { limit: undefined, page: undefined }, - args: { where: { provinceCode: testCode }, orderBy: { code: 'asc' } }, - }); - - it('should return all regencies in a province', async () => { - const testCode = '11'; - const expectedRegencies = regencies.filter( - (r) => r.provinceCode === testCode, - ); - - const paginatorSpy = vitest - .spyOn(prismaService, 'paginator') - .mockResolvedValue({ data: expectedRegencies }); - - const result = await provinceService.findRegencies(testCode); - - expect(paginatorSpy).toHaveBeenCalledTimes(1); - expect(paginatorSpy).toHaveBeenCalledWith(getPaginatorOptions(testCode)); - - expect(result.data).toEqual(expectedRegencies); - }); - - it('should return empty array if there is no match province code', async () => { - const testCode = '9999'; - - const paginatorSpy = vitest - .spyOn(prismaService, 'paginator') - .mockResolvedValue({ data: [] }); - - const result = await provinceService.findRegencies(testCode); - - expect(paginatorSpy).toHaveBeenCalledTimes(1); - expect(paginatorSpy).toHaveBeenCalledWith(getPaginatorOptions(testCode)); - expect(result.data).toEqual([]); - }); - - it.todo( - 'should return regencies sorted by name in ascending order', - async () => { - // Test implementation goes here - }, - ); - - it.todo( - 'should return regencies sorted by name in descending order', - async () => { - // Test implementation goes here - }, - ); - }); }); diff --git a/src/province/province.service.ts b/src/province/province.service.ts index 68ff26b..13ff114 100644 --- a/src/province/province.service.ts +++ b/src/province/province.service.ts @@ -1,21 +1,16 @@ -import { PaginationQuery } from '@/common/dto/pagination.dto'; import { PaginatedReturn } from '@/common/interceptor/paginate.interceptor'; 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'; +import { SortService } from '@/sort/sort.service'; import { Injectable } from '@nestjs/common'; -import { Province, Regency } from '@prisma/client'; +import { Province } from '@prisma/client'; import { ProvinceFindQueries } from './province.dto'; @Injectable() export class ProvinceService { readonly sorter: SortService; - constructor( - private readonly prisma: PrismaService, - private readonly regencyService: RegencyService, - ) { + constructor(private readonly prisma: PrismaService) { this.sorter = new SortService({ sortBy: 'code', sortOrder: 'asc', @@ -56,26 +51,4 @@ export class ProvinceService { }, }); } - - /** - * Find all regencies in a province. - * @param provinceCode The province code. - * @param options The options. - * @returns An array of regencies, `[]` if there are no match province. - */ - async findRegencies( - provinceCode: string, - options?: SortOptions & PaginationQuery, - ): Promise> { - const { sortBy, sortOrder, page, limit } = options ?? {}; - - return this.prisma.paginator({ - model: 'Regency', - args: { - where: { provinceCode }, - orderBy: this.regencyService.sorter.object({ sortBy, sortOrder }), - }, - paginate: { page, limit }, - }); - } } diff --git a/src/regency/__mocks__/regency.service.ts b/src/regency/__mocks__/regency.service.ts index d5979a0..dab8a7f 100644 --- a/src/regency/__mocks__/regency.service.ts +++ b/src/regency/__mocks__/regency.service.ts @@ -1,18 +1,12 @@ import { sortArray } from '@/common/utils/array'; -import { convertCoordinate } from '@/common/utils/coordinate'; -import { SortOptions } from '@/sort/sort.service'; -import { District, Island, Regency } from '@prisma/client'; +import { Regency } from '@prisma/client'; import { RegencyFindQueries } from '../regency.dto'; export class MockRegencyService { readonly regencies: readonly Regency[]; - readonly districts: readonly District[]; - readonly islands: readonly Island[]; - constructor(regencies: Regency[], districts: District[], islands: Island[]) { + constructor(regencies: Regency[]) { this.regencies = regencies; - this.districts = districts; - this.islands = islands; } async find({ @@ -37,37 +31,4 @@ export class MockRegencyService { this.regencies.find((regency) => regency.code === code) ?? null, ); } - - async findDistricts( - regencyCode: string, - { sortBy = 'code', sortOrder }: SortOptions = {}, - ) { - if (this.regencies.every((r) => r.code !== regencyCode)) { - return null; - } - - const res = this.districts.filter( - (district) => district.regencyCode === regencyCode, - ); - - return Promise.resolve({ data: sortArray(res, sortBy, sortOrder) }); - } - - async findIslands( - regencyCode: string, - { sortBy = 'code', sortOrder }: SortOptions = {}, - ) { - if (this.regencies.every((r) => r.code !== regencyCode)) { - return null; - } - - const res = this.islands - .filter((island) => island.regencyCode === regencyCode) - .map((island) => { - const [latitude, longitude] = convertCoordinate(island.coordinate); - return { ...island, latitude, longitude }; - }); - - return Promise.resolve({ data: sortArray(res, sortBy, sortOrder) }); - } } diff --git a/src/regency/regency.controller.spec.ts b/src/regency/regency.controller.spec.ts index c25b9e3..7fea3c6 100644 --- a/src/regency/regency.controller.spec.ts +++ b/src/regency/regency.controller.spec.ts @@ -1,9 +1,9 @@ import { getValues, sortArray } from '@/common/utils/array'; -import { getDistricts, getIslands, getRegencies } from '@/common/utils/data'; +import { getRegencies } from '@/common/utils/data'; import { SortOrder } from '@/sort/sort.dto'; import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { District, Island, Regency } from '@prisma/client'; +import { Regency } from '@prisma/client'; import { MockRegencyService } from './__mocks__/regency.service'; import { RegencyController } from './regency.controller'; import { RegencyService } from './regency.service'; @@ -12,14 +12,10 @@ describe('RegencyController', () => { const testRegencyCode = '1101'; let regencies: Regency[]; - let districts: District[]; - let islands: Island[]; let controller: RegencyController; beforeAll(async () => { regencies = await getRegencies(); - districts = await getDistricts(); - islands = await getIslands(); }); beforeEach(async () => { @@ -28,7 +24,7 @@ describe('RegencyController', () => { providers: [ { provide: RegencyService, - useValue: new MockRegencyService(regencies, districts, islands), + useValue: new MockRegencyService(regencies), }, ], }).compile(); @@ -147,150 +143,4 @@ describe('RegencyController', () => { ).rejects.toThrowError(NotFoundException); }); }); - - describe('findDistricts', () => { - let expectedDistricts: District[]; - - beforeAll(() => { - expectedDistricts = districts.filter( - (r) => r.regencyCode === testRegencyCode, - ); - }); - - it('should return all districts in the matching regency', async () => { - const { data } = await controller.findDistricts({ - code: testRegencyCode, - }); - - for (const district of data) { - expect(district).toEqual( - expect.objectContaining({ - code: expect.stringMatching( - new RegExp(`^${testRegencyCode}\\d{2}$`), - ), - name: expect.any(String), - regencyCode: testRegencyCode, - }), - ); - } - - expect(data).toHaveLength(expectedDistricts.length); - }); - - it('should throw NotFoundException if there is no matching regency', async () => { - await expect( - controller.findDistricts({ code: '0000' }), - ).rejects.toThrowError(NotFoundException); - }); - - it('should return all districts in the matching regency sorted by name ascending', async () => { - const { data } = await controller.findDistricts( - { code: testRegencyCode }, - { sortBy: 'name', sortOrder: SortOrder.ASC }, - ); - - expect(getValues(data, 'code')).toEqual( - getValues(sortArray(expectedDistricts, 'name'), 'code'), - ); - }); - - it('should return all districts in the matching regency sorted by name descending', async () => { - const { data } = await controller.findDistricts( - { code: testRegencyCode }, - { sortBy: 'name', sortOrder: SortOrder.DESC }, - ); - - expect(getValues(data, 'code')).toEqual( - getValues(sortArray(expectedDistricts, 'name', SortOrder.DESC), 'code'), - ); - }); - }); - - describe('findIslands', () => { - let expectedIslands: Island[]; - - beforeAll(() => { - expectedIslands = islands.filter( - (r) => r.regencyCode === testRegencyCode, - ); - }); - - it('should return all islands in the matching regency', async () => { - const { data } = await controller.findIslands({ - code: testRegencyCode, - }); - - for (const island of data) { - expect(island).toEqual( - expect.objectContaining({ - code: expect.stringMatching( - new RegExp(`^${testRegencyCode}\\d{5}$`), - ), - coordinate: expect.any(String), - isOutermostSmall: expect.any(Boolean), - isPopulated: expect.any(Boolean), - latitude: expect.any(Number), - longitude: expect.any(Number), - name: expect.any(String), - regencyCode: testRegencyCode, - }), - ); - } - - expect(data).toHaveLength(expectedIslands.length); - }); - - it('should throw NotFoundException if there is no matching regency', async () => { - await expect( - controller.findIslands({ code: '0000' }), - ).rejects.toThrowError(NotFoundException); - }); - - it('should return all islands in the matching regency sorted by name ascending', async () => { - const { data } = await controller.findIslands( - { code: testRegencyCode }, - { sortBy: 'name', sortOrder: SortOrder.ASC }, - ); - - expect(getValues(data, 'code')).toEqual( - getValues(sortArray(expectedIslands, 'name'), 'code'), - ); - }); - - it('should return all islands in the matching regency sorted by name descending', async () => { - const { data } = await controller.findIslands( - { code: testRegencyCode }, - { sortBy: 'name', sortOrder: SortOrder.DESC }, - ); - - expect(getValues(data, 'code')).toEqual( - getValues(sortArray(expectedIslands, 'name', SortOrder.DESC), 'code'), - ); - }); - - it('should return all islands in the matching regency sorted by coordinate ascending', async () => { - const { data } = await controller.findIslands( - { code: testRegencyCode }, - { sortBy: 'coordinate', sortOrder: SortOrder.ASC }, - ); - - expect(getValues(data, 'code')).toEqual( - getValues(sortArray(expectedIslands, 'coordinate'), 'code'), - ); - }); - - it('should return all islands in the matching regency sorted by coordinate descending', async () => { - const { data } = await controller.findIslands( - { code: testRegencyCode }, - { sortBy: 'coordinate', sortOrder: SortOrder.DESC }, - ); - - expect(getValues(data, 'code')).toEqual( - getValues( - sortArray(expectedIslands, 'coordinate', SortOrder.DESC), - 'code', - ), - ); - }); - }); }); diff --git a/src/regency/regency.controller.ts b/src/regency/regency.controller.ts index 230b4db..ab4b95b 100644 --- a/src/regency/regency.controller.ts +++ b/src/regency/regency.controller.ts @@ -1,7 +1,5 @@ import { ApiDataResponse } from '@/common/decorator/api-data-response.decorator'; import { ApiPaginatedResponse } from '@/common/decorator/api-paginated-response.decorator'; -import { District } from '@/district/district.dto'; -import { Island } from '@/island/island.dto'; import { Controller, Get, @@ -19,9 +17,6 @@ import { import { Regency, RegencyFindByCodeParams, - RegencyFindDistrictParams, - RegencyFindDistrictQueries, - RegencyFindIslandsQueries, RegencyFindQueries, } from './regency.dto'; import { RegencyService } from './regency.service'; @@ -70,60 +65,4 @@ export class RegencyController { return regency; } - - @ApiOperation({ description: 'Get all districts in a regency.' }) - @ApiQuery({ - name: 'sortBy', - description: 'Sort districts by its code or name.', - required: false, - type: 'string', - example: 'code', - }) - @ApiPaginatedResponse({ - model: District, - description: 'Returns array of districts.', - }) - @ApiBadRequestResponse({ description: 'If the `code` is invalid.' }) - @ApiNotFoundResponse({ - description: 'If there are no regency match with the `code`.', - }) - @Get(':code/districts') - async findDistricts( - @Param() { code }: RegencyFindDistrictParams, - @Query() queries?: RegencyFindDistrictQueries, - ): Promise> { - if ((await this.regencyService.findByCode(code)) === null) { - throw new NotFoundException(`There are no regency with code '${code}'`); - } - - return this.regencyService.findDistricts(code, queries); - } - - @ApiOperation({ description: 'Get all islands in a regency.' }) - @ApiQuery({ - name: 'sortBy', - description: 'Sort islands by its code, name, or coordinate.', - required: false, - type: 'string', - example: 'code', - }) - @ApiPaginatedResponse({ - model: Island, - description: 'Returns array of islands.', - }) - @ApiBadRequestResponse({ description: 'If the `code` is invalid.' }) - @ApiNotFoundResponse({ - description: 'If there are no regency match with the `code`.', - }) - @Get(':code/islands') - async findIslands( - @Param() { code }: RegencyFindByCodeParams, - @Query() queries?: RegencyFindIslandsQueries, - ) { - if ((await this.regencyService.findByCode(code)) === null) { - throw new NotFoundException(`There are no regency with code '${code}'`); - } - - return this.regencyService.findIslands(code, queries); - } } diff --git a/src/regency/regency.dto.ts b/src/regency/regency.dto.ts index db668c4..e25d1de 100644 --- a/src/regency/regency.dto.ts +++ b/src/regency/regency.dto.ts @@ -8,8 +8,6 @@ import { PickType, } from '@nestjs/swagger'; import { IsNotEmpty, IsNumberString, Length } from 'class-validator'; -import { DistrictSortQuery } from '../district/district.dto'; -import { IslandSortQuery } from '../island/island.dto'; import { PaginationQuery } from '@/common/dto/pagination.dto'; export class Regency { @@ -52,17 +50,3 @@ export class RegencyFindQueries extends IntersectionType( export class RegencyFindByCodeParams extends PickType(Regency, [ 'code', ] as const) {} - -export class RegencyFindDistrictParams extends PickType(Regency, [ - 'code', -] as const) {} - -export class RegencyFindDistrictQueries extends IntersectionType( - DistrictSortQuery, - PaginationQuery, -) {} - -export class RegencyFindIslandsQueries extends IntersectionType( - IslandSortQuery, - PaginationQuery, -) {} diff --git a/src/regency/regency.service.spec.ts b/src/regency/regency.service.spec.ts index 06679cd..c5ff936 100644 --- a/src/regency/regency.service.spec.ts +++ b/src/regency/regency.service.spec.ts @@ -1,83 +1,23 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PrismaService } from '@/prisma/prisma.service'; import { RegencyService } from './regency.service'; -import { DistrictService } from '@/district/district.service'; -import { IslandService } from '@/island/island.service'; -import { District, Island, Regency } from '@prisma/client'; -import { VillageService } from '@/village/village.service'; +import { Regency } from '@prisma/client'; import { getDBProviderFeatures } from '@/common/utils/db'; import { SortOrder } from '@/sort/sort.dto'; - -const regencies: readonly Regency[] = [ - { code: '1101', name: 'KABUPATEN ACEH SELATAN', provinceCode: '11' }, - { code: '1102', name: 'KABUPATEN ACEH TENGGARA', provinceCode: '11' }, - { code: '1201', name: 'KABUPATEN TAPANULI TENGAH', provinceCode: '12' }, - { code: '1202', name: 'KABUPATEN TAPANULI UTARA', provinceCode: '12' }, - { code: '1271', name: 'KOTA MEDAN', provinceCode: '12' }, -] as const; - -const districts: readonly District[] = [ - { code: '110101', name: 'Bakongan', regencyCode: '1101' }, - { code: '110102', name: 'Kluet Utara', regencyCode: '1101' }, - { code: '110103', name: 'Kluet Selatan', regencyCode: '1101' }, -]; - -const islands: readonly Island[] = [ - { - code: '110140001', - coordinate: '03°19\'03.44" N 097°07\'41.73" E', - isOutermostSmall: false, - isPopulated: false, - name: 'Pulau Batukapal', - regencyCode: '1101', - }, - { - code: '110140002', - coordinate: '03°24\'55.00" N 097°04\'21.00" E', - isOutermostSmall: false, - isPopulated: false, - name: 'Pulau Batutunggal', - regencyCode: '1101', - }, - { - code: '110140003', - coordinate: '02°52\'54.99" N 097°31\'07.00" E', - isOutermostSmall: false, - isPopulated: false, - name: 'Pulau Kayee', - regencyCode: '1101', - }, - { - code: '110140004', - coordinate: '02°54\'25.11" N 097°26\'18.51" E', - isOutermostSmall: false, - isPopulated: true, - name: 'Pulau Mangki Palsu', - regencyCode: '1101', - }, - { - code: '110140005', - coordinate: '02°53\'16.00" N 097°30\'54.00" E', - isOutermostSmall: true, - isPopulated: false, - name: 'Pulau Tengku Palsu', - regencyCode: '1101', - }, -] as const; +import { getRegencies } from '@/common/utils/data'; describe('RegencyService', () => { + let regencies: Regency[]; let service: RegencyService; let prismaService: PrismaService; + beforeAll(async () => { + regencies = await getRegencies(); + }); + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [ - RegencyService, - PrismaService, - DistrictService, - IslandService, - VillageService, - ], + providers: [RegencyService, PrismaService], }).compile(); service = module.get(RegencyService); @@ -233,122 +173,4 @@ describe('RegencyService', () => { expect(result).toBeNull(); }); }); - - describe('findDistricts', () => { - const getPaginatorOptions = (testCode: string) => ({ - model: 'District', - paginate: { page: undefined, limit: undefined }, - args: { where: { regencyCode: testCode }, orderBy: { code: 'asc' } }, - }); - - it('should return all districts in a regency', async () => { - const testCode = '1101'; - const expectedDistricts = districts.filter( - (d) => d.regencyCode === testCode, - ); - - const paginatorSpy = vitest - .spyOn(prismaService, 'paginator') - .mockResolvedValue({ data: [...expectedDistricts] }); - - const result = await service.findDistricts(testCode); - - expect(paginatorSpy).toHaveBeenCalledTimes(1); - expect(paginatorSpy).toHaveBeenCalledWith(getPaginatorOptions(testCode)); - expect(result.data).toEqual(expectedDistricts); - }); - - it('should return empty array if there is no match regency code', async () => { - const testCode = '9999'; - - const paginatorSpy = vitest - .spyOn(prismaService, 'paginator') - .mockResolvedValue({ data: [] }); - - const result = await service.findDistricts(testCode); - - expect(paginatorSpy).toHaveBeenCalledTimes(1); - expect(paginatorSpy).toHaveBeenCalledWith(getPaginatorOptions(testCode)); - expect(result.data).toEqual([]); - }); - - it.todo('should sort districts by name in ascending order', async () => { - const testCode = '1101'; - const expectedDistricts = [...districts].sort((a, b) => - a.name.localeCompare(b.name), - ); - - const findUniqueSpy = vitest - .spyOn(prismaService.regency, 'findUnique') - .mockReturnValue({ - districts: vitest.fn().mockResolvedValue(expectedDistricts), - } as any); - - const result = await service.findDistricts(testCode, { - sortBy: 'name', - }); - - expect(findUniqueSpy).toHaveBeenCalledTimes(1); - expect(findUniqueSpy).toHaveBeenCalledWith({ - where: { code: testCode }, - }); - // TODO: test if findUnique().districts() is called with the sort options - expect(result).toEqual(expectedDistricts); - }); - - it.todo('should sort districts by name in descending order', async () => { - // - }); - }); - - describe('findIslands', () => { - const getPaginatorOptions = (testCode: string) => ({ - model: 'Island', - paginate: { page: undefined, limit: undefined }, - args: { where: { regencyCode: testCode }, orderBy: { code: 'asc' } }, - }); - - it('should return all islands in a regency', async () => { - const testCode = '1101'; - const expectedIslands = islands.filter((i) => i.regencyCode === testCode); - - const paginatorSpy = vitest - .spyOn(prismaService, 'paginator') - .mockResolvedValue({ data: expectedIslands }); - - const result = await service.findIslands(testCode); - - expect(paginatorSpy).toHaveBeenCalledTimes(1); - expect(paginatorSpy).toHaveBeenCalledWith(getPaginatorOptions(testCode)); - - for (const island of result.data) { - expect(island).toEqual({ - ...island, - latitude: expect.any(Number), - longitude: expect.any(Number), - }); - } - }); - - it('should return empty array if there is no match regency code', async () => { - const testCode = '9999'; - const paginatorSpy = vitest - .spyOn(prismaService, 'paginator') - .mockResolvedValue({ data: [] }); - - const result = await service.findIslands(testCode); - - expect(paginatorSpy).toHaveBeenCalledTimes(1); - expect(paginatorSpy).toHaveBeenCalledWith(getPaginatorOptions(testCode)); - expect(result.data).toEqual([]); - }); - - it.todo('should sort islands by code in ascending order ', async () => { - // - }); - - it.todo('should sort islands by code in descending order', async () => { - // - }); - }); }); diff --git a/src/regency/regency.service.ts b/src/regency/regency.service.ts index 098682f..8a2c175 100644 --- a/src/regency/regency.service.ts +++ b/src/regency/regency.service.ts @@ -1,24 +1,16 @@ -import { PaginationQuery } from '@/common/dto/pagination.dto'; import { PaginatedReturn } from '@/common/interceptor/paginate.interceptor'; 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 { SortService } from '@/sort/sort.service'; import { Injectable } from '@nestjs/common'; -import { District, Island, Regency } from '@prisma/client'; +import { Regency } from '@prisma/client'; import { RegencyFindQueries } from './regency.dto'; @Injectable() export class RegencyService { readonly sorter: SortService; - constructor( - private readonly prisma: PrismaService, - private readonly districtService: DistrictService, - private readonly islandService: IslandService, - ) { + constructor(private readonly prisma: PrismaService) { this.sorter = new SortService({ sortBy: 'code', sortOrder: 'asc', @@ -58,51 +50,4 @@ export class RegencyService { }, }); } - - /** - * Find all districts in a regency. - * @param regencyCode The regency code. - * @param options The options. - * @returns Paginated array of districts, `[]` if there are no match regency. - */ - async findDistricts( - regencyCode: string, - options?: SortOptions & PaginationQuery, - ): Promise> { - const { page, limit, sortBy, sortOrder } = options ?? {}; - - return this.prisma.paginator({ - model: 'District', - args: { - where: { regencyCode }, - orderBy: this.districtService.sorter.object({ sortBy, sortOrder }), - }, - paginate: { page, limit }, - }); - } - - /** - * Find all islands in a regency. - * @param regencyCode The regency code. - * @param options The options. - * @returns Paginated array of islands, `[]` if there are no match regency. - */ - async findIslands( - regencyCode: string, - options?: SortOptions & PaginationQuery, - ): Promise> { - const { page, limit, sortBy, sortOrder } = options ?? {}; - - const res = await this.prisma.paginator({ - model: 'Island', - paginate: { page, limit }, - args: { - where: { regencyCode }, - orderBy: this.islandService.sorter.object({ sortBy, sortOrder }), - }, - }); - - const islands = res.data.map(this.islandService.addDecimalCoordinate); - return { ...res, data: islands }; - } } diff --git a/src/village/village.controller.ts b/src/village/village.controller.ts index 581adb6..7e95841 100644 --- a/src/village/village.controller.ts +++ b/src/village/village.controller.ts @@ -27,7 +27,7 @@ import { PaginatedReturn } from '@/common/interceptor/paginate.interceptor'; export class VillageController { constructor(private readonly villageService: VillageService) {} - @ApiOperation({ description: 'Get villages by its name.' }) + @ApiOperation({ description: 'Get the villages.' }) @ApiQuery({ name: 'sortBy', description: 'Sort by village code or name.', diff --git a/test/district.e2e-spec.ts b/test/district.e2e-spec.ts index 2f9c4e8..83586b3 100644 --- a/test/district.e2e-spec.ts +++ b/test/district.e2e-spec.ts @@ -1,6 +1,6 @@ -import { District, Village } from '@prisma/client'; +import { District } from '@prisma/client'; import { AppTester } from './helper/app-tester'; -import { districtRegex, villageRegex } from './helper/data-regex'; +import { districtRegex } from './helper/data-regex'; describe('District (e2e)', () => { const baseUrl = '/districts'; @@ -105,40 +105,6 @@ describe('District (e2e)', () => { }); }); - describe(`GET ${baseUrl}/{code}/villages`, () => { - it('should return 400 if the `code` is invalid', async () => { - await tester.expectBadCode( - (code) => `${baseUrl}/${code}/villages`, - badDistrictCodes, - ); - }); - - it('should return 400 if any villages sort query is invalid', async () => { - await tester.expectBadSortQuery( - (sortQueryStr) => `${baseUrl}?${sortQueryStr}`, - ['', 'unknown'], - ); - }); - - it('should return 404 if the `code` does not exist', async () => { - await tester.expectNotFound(`${baseUrl}/000000/villages`); - }); - - it('should return all villages in the district with the `code`', async () => { - const villages = await tester.expectData( - `${baseUrl}/${testCode}/villages`, - ); - - villages.forEach((village) => { - expect(village).toEqual({ - code: expect.stringMatching(villageRegex.code), - name: expect.stringMatching(villageRegex.name), - districtCode: testCode, - }); - }); - }); - }); - afterAll(async () => { await tester.closeApp(); }); diff --git a/test/province.e2e-spec.ts b/test/province.e2e-spec.ts index 4382bcd..f9e1de0 100644 --- a/test/province.e2e-spec.ts +++ b/test/province.e2e-spec.ts @@ -1,6 +1,6 @@ -import { Province, Regency } from '@prisma/client'; +import { Province } from '@prisma/client'; import { AppTester } from './helper/app-tester'; -import { provinceRegex, regencyRegex } from './helper/data-regex'; +import { provinceRegex } from './helper/data-regex'; describe('Province (e2e)', () => { const baseUrl = '/provinces'; @@ -89,36 +89,6 @@ describe('Province (e2e)', () => { }); }); - describe(`GET ${baseUrl}/{code}/regencies`, () => { - it('should return 400 if the `code` is invalid', async () => { - await tester.expectBadCode( - (code) => `${baseUrl}/${code}/regencies`, - badProvinceCodes, - ); - }); - - it('should return 400 if regencies sort query is invalid', async () => { - await tester.expectBadSortQuery( - (sortQueryStr) => `${baseUrl}/${testCode}/regencies?${sortQueryStr}`, - ['', 'unknown'], - ); - }); - - it('should return all regencies from specific province', async () => { - const regencies = await tester.expectData( - `${baseUrl}/${testCode}/regencies`, - ); - - regencies.forEach((regency) => { - expect(regency).toEqual({ - code: expect.stringMatching(regencyRegex.code), - name: expect.stringMatching(regencyRegex.name), - provinceCode: testCode, - }); - }); - }); - }); - afterAll(async () => { await tester.closeApp(); }); diff --git a/test/regency.e2e-spec.ts b/test/regency.e2e-spec.ts index f368b5e..d239477 100644 --- a/test/regency.e2e-spec.ts +++ b/test/regency.e2e-spec.ts @@ -1,6 +1,6 @@ -import { District, Island, Regency } from '@prisma/client'; +import { Regency } from '@prisma/client'; import { AppTester } from './helper/app-tester'; -import { districtRegex, islandRegex, regencyRegex } from './helper/data-regex'; +import { regencyRegex } from './helper/data-regex'; describe('Regency (e2e)', () => { const baseUrl = '/regencies'; @@ -90,81 +90,6 @@ describe('Regency (e2e)', () => { }); }); - describe(`GET ${baseUrl}/{code}/districts`, () => { - it('should return 400 if the `code` is invalid', async () => { - await tester.expectBadCode( - (code) => `${baseUrl}/${code}/districts`, - badRegencyCodes, - ); - }); - - it('should return 400 if any districts sort query is invalid', async () => { - await tester.expectBadSortQuery( - (sortQueryStr) => `${baseUrl}?${sortQueryStr}`, - ['', 'unknown'], - ); - }); - - it('should return 404 if the `code` does not match with any regency', async () => { - await tester.expectNotFound(`${baseUrl}/0000/districts`); - }); - - it('should return all districts from specific regency', async () => { - const districts = await tester.expectData( - `${baseUrl}/${testCode}/districts`, - ); - - districts.forEach((district) => { - expect(district).toEqual({ - code: expect.stringMatching(districtRegex.code), - name: expect.stringMatching(districtRegex.name), - regencyCode: testCode, - }); - }); - }); - }); - - describe(`GET ${baseUrl}/{code}/islands`, () => { - it('should return 400 if the `code` is invalid', async () => { - await tester.expectBadCode( - (code) => `${baseUrl}/${code}/islands`, - badRegencyCodes, - ); - }); - - it('should return 404 if the `code` does not match with any regency', async () => { - await tester.expectNotFound(`${baseUrl}/0000/islands`); - }); - - it('should return all islands from specific regency', async () => { - const testCode = '1101'; - const islands = await tester.expectData( - `${baseUrl}/${testCode}/islands`, - ); - - islands.forEach((island) => { - expect(island).toEqual({ - code: expect.stringMatching(islandRegex.code), - coordinate: expect.stringMatching(islandRegex.coordinate), - isOutermostSmall: expect.any(Boolean), - isPopulated: expect.any(Boolean), - latitude: expect.any(Number), - longitude: expect.any(Number), - name: expect.stringMatching(islandRegex.name), - regencyCode: testCode, - }); - }); - }); - - it('should return empty array if there are no any island in the regency', async () => { - const islands = await tester.expectData( - `${baseUrl}/1102/islands`, - ); - - expect(islands).toEqual([]); - }); - }); - afterAll(async () => { await tester.closeApp(); });