Skip to content

Commit

Permalink
Delete parents/{code}/childs endpoints (#203)
Browse files Browse the repository at this point in the history
  • Loading branch information
fityannugroho authored Oct 15, 2023
2 parents d7cf6c1 + fec2fdc commit a0f7e54
Show file tree
Hide file tree
Showing 24 changed files with 256 additions and 1,111 deletions.
197 changes: 197 additions & 0 deletions docs/upgrading/upgrade-to-v4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
<h1>Upgrade to idn-area version 4</h1>

## 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 |
22 changes: 2 additions & 20 deletions src/district/__mocks__/district.service.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -34,19 +31,4 @@ export class MockDistrictService {
this.districts.find((district) => district.code === code) ?? null,
);
}

async findVillages(
districtCode: string,
{ sortBy = 'code', sortOrder }: SortOptions<Village> = {},
) {
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) });
}
}
68 changes: 3 additions & 65 deletions src/district/district.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 () => {
Expand All @@ -26,7 +24,7 @@ describe('DistrictController', () => {
providers: [
{
provide: DistrictService,
useValue: new MockDistrictService(districts, villages),
useValue: new MockDistrictService(districts),
},
],
}).compile();
Expand Down Expand Up @@ -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'),
);
});
});
});
33 changes: 1 addition & 32 deletions src/district/district.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ApiDataResponse } from '@/common/decorator/api-data-response.decorator';
import { Village } from '@/village/village.dto';
import {
Controller,
Get,
Expand All @@ -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';
Expand All @@ -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.',
Expand Down Expand Up @@ -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<PaginatedReturn<Village>> {
if ((await this.districtService.findByCode(code)) === null) {
throw new NotFoundException(`There are no district with code '${code}'`);
}

return this.districtService.findVillages(code, queries);
}
}
Loading

0 comments on commit a0f7e54

Please sign in to comment.