Skip to content

Commit 706e04b

Browse files
committed
feat(sort): implement sort by and sort direction to each endpoint
1 parent 6180447 commit 706e04b

7 files changed

Lines changed: 62 additions & 12 deletions

File tree

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,32 @@ All list endpoints support:
9494
- `limit` - Items per page (default: 10)
9595
- `name` - Search by name (partial match)
9696
- `code` - Filter by code (returns entity directly)
97+
- `sortBy` - Field to sort by (optional, defaults to `code`)
98+
- `sortDirection` - Sort direction: `ASC` or `DESC` (default: `ASC`)
9799
- `provinceCode` - Filter by parent province
98100
- `regencyCode` - Filter by parent regency
99101
- `districtCode` - Filter by parent district
100102

103+
#### Sorting Options
104+
105+
Each endpoint supports different sort fields:
106+
- **Provinces**: `code`, `province`
107+
- **Regencies**: `code`, `regency`, `provinceCode`, `type`
108+
- **Districts**: `code`, `district`, `regencyCode`
109+
- **Villages**: `code`, `village`, `districtCode`, `postalCode`
110+
111+
**Examples:**
112+
```bash
113+
# Sort provinces by name in ascending order
114+
GET /province?sortBy=province&sortDirection=ASC
115+
116+
# Sort regencies by name in descending order
117+
GET /regency?provinceCode=11&sortBy=regency&sortDirection=DESC
118+
119+
# Default behavior (no sorting params = sort by code ASC)
120+
GET /province
121+
```
122+
101123
## 🗄️ Database Setup (For Self-Hosting)
102124

103125
### Step 1: Create Supabase Project
@@ -188,7 +210,7 @@ In Vercel dashboard (or `.env` file for local development), add:
188210
DATABASE_URL=postgresql://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres?pgbouncer=true
189211
NODE_ENV=production
190212
APP_ENV=production
191-
DATABASE_SCHEMA=aloe # Optional: only if using a custom schema
213+
DATABASE_SCHEMA=konoland-schema # Optional: only if using a custom schema
192214
```
193215

194216
## 🛠️ Local Development

src/common/dto/pagination.dto.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IsOptional, IsInt, Min, Max } from 'class-validator';
1+
import { IsOptional, IsInt, Min, Max, IsString, IsIn } from 'class-validator';
22
import { Type } from 'class-transformer';
33

44
export class PaginationDTO {
@@ -13,5 +13,13 @@ export class PaginationDTO {
1313
@IsInt()
1414
@Min(1)
1515
page?: number = 1;
16+
17+
@IsOptional()
18+
@IsString()
19+
sortBy?: string;
20+
21+
@IsOptional()
22+
@IsIn(['ASC', 'DESC', 'asc', 'desc'])
23+
sortDirection?: 'ASC' | 'DESC' = 'ASC';
1624
}
1725

src/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ async function createNestApp(): Promise<express.Express> {
3030
// For Vercel serverless function
3131
export default async function handler(req: express.Request, res: express.Response) {
3232
try {
33-
const app = await createNestApp();
34-
return app(req, res);
33+
const app = await createNestApp();
34+
return app(req, res);
3535
} catch (error) {
3636
console.error('Serverless function error:', error);
3737
res.status(500).json({

src/modules/district/district.service.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export class DistrictService {
1212
) {}
1313

1414
async getDistricts(getDistrictsQuery: GetDistrictsDTO) {
15-
const { limit = 10, page = 1, name, code, regencyCode } = getDistrictsQuery;
15+
const { limit = 10, page = 1, name, code, regencyCode, sortBy, sortDirection = 'ASC' } = getDistrictsQuery;
1616

1717
// If code is provided, return the entity directly (backward compatibility)
1818
if (code) {
@@ -30,12 +30,17 @@ export class DistrictService {
3030
where.regencyCode = regencyCode;
3131
}
3232

33+
// Define allowed sort fields to prevent SQL injection
34+
const allowedSortFields = ['code', 'district', 'regencyCode'];
35+
const orderField = sortBy && allowedSortFields.includes(sortBy) ? sortBy : 'code';
36+
const orderDirection = sortDirection.toUpperCase() as 'ASC' | 'DESC';
37+
3338
const [districts, total] = await this.districtRepository.findAndCount({
3439
where,
3540
relations: ['regency'],
3641
take: limit,
3742
skip: (page - 1) * limit,
38-
order: { code: 'ASC' },
43+
order: { [orderField]: orderDirection },
3944
});
4045

4146
return {

src/modules/province/province.service.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export class ProvinceService {
1212
) {}
1313

1414
async getProvinces(getProvincesQuery: GetProvincesDTO) {
15-
const { limit = 10, page = 1, name, code } = getProvincesQuery;
15+
const { limit = 10, page = 1, name, code, sortBy, sortDirection = 'ASC' } = getProvincesQuery;
1616

1717
// If code is provided, return the entity directly (backward compatibility)
1818
if (code) {
@@ -26,11 +26,16 @@ export class ProvinceService {
2626
where.province = Like(`%${name}%`);
2727
}
2828

29+
// Define allowed sort fields to prevent SQL injection
30+
const allowedSortFields = ['code', 'province'];
31+
const orderField = sortBy && allowedSortFields.includes(sortBy) ? sortBy : 'code';
32+
const orderDirection = sortDirection.toUpperCase() as 'ASC' | 'DESC';
33+
2934
const [provinces, total] = await this.provinceRepository.findAndCount({
3035
where,
3136
take: limit,
3237
skip: (page - 1) * limit,
33-
order: { code: 'ASC' },
38+
order: { [orderField]: orderDirection },
3439
});
3540

3641
return {

src/modules/regency/regency.service.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export class RegencyService {
1212
) {}
1313

1414
async getRegencies(getRegenciesQuery: GetRegenciesDTO) {
15-
const { limit = 10, page = 1, name, code, provinceCode } = getRegenciesQuery;
15+
const { limit = 10, page = 1, name, code, provinceCode, sortBy, sortDirection = 'ASC' } = getRegenciesQuery;
1616

1717
// If code is provided, return the entity directly (backward compatibility)
1818
if (code) {
@@ -30,12 +30,17 @@ export class RegencyService {
3030
where.provinceCode = provinceCode;
3131
}
3232

33+
// Define allowed sort fields to prevent SQL injection
34+
const allowedSortFields = ['code', 'regency', 'provinceCode', 'type'];
35+
const orderField = sortBy && allowedSortFields.includes(sortBy) ? sortBy : 'code';
36+
const orderDirection = sortDirection.toUpperCase() as 'ASC' | 'DESC';
37+
3338
const [regencies, total] = await this.regencyRepository.findAndCount({
3439
where,
3540
relations: ['province'],
3641
take: limit,
3742
skip: (page - 1) * limit,
38-
order: { code: 'ASC' },
43+
order: { [orderField]: orderDirection },
3944
});
4045

4146
return {

src/modules/village/village.service.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export class VillageService {
1212
) {}
1313

1414
async getVillages(getVillagesQuery: GetVillagesDTO) {
15-
const { limit = 10, page = 1, name, code, districtCode } = getVillagesQuery;
15+
const { limit = 10, page = 1, name, code, districtCode, sortBy, sortDirection = 'ASC' } = getVillagesQuery;
1616

1717
// If code is provided, return the entity directly (backward compatibility)
1818
if (code) {
@@ -30,12 +30,17 @@ export class VillageService {
3030
where.districtCode = districtCode;
3131
}
3232

33+
// Define allowed sort fields to prevent SQL injection
34+
const allowedSortFields = ['code', 'village', 'districtCode', 'postalCode'];
35+
const orderField = sortBy && allowedSortFields.includes(sortBy) ? sortBy : 'code';
36+
const orderDirection = sortDirection.toUpperCase() as 'ASC' | 'DESC';
37+
3338
const [villages, total] = await this.villageRepository.findAndCount({
3439
where,
3540
relations: ['district'],
3641
take: limit,
3742
skip: (page - 1) * limit,
38-
order: { code: 'ASC' },
43+
order: { [orderField]: orderDirection },
3944
});
4045

4146
return {

0 commit comments

Comments
 (0)