Enhance pagination and ordering capabilities in API responses

- Updated pagination-response and read-write-controllers documentation to include `orderBy` and `orderType` parameters for sorting results.
- Introduced new `order-clause` module to handle ordering logic, including validation for order types and columns.
- Enhanced `PaginationQueryDto` to support ordering fields in API requests.
- Updated various repository and service classes to implement ordering in database queries.
- Added unit tests for new ordering functionality and ensured existing tests cover the updated behavior.
- Refactored related DTOs to include user and code relations for better data representation in responses.
This commit is contained in:
shancheas
2026-08-27 13:09:41 +07:00
parent 790725e227
commit 4c45a4371e
85 changed files with 1824 additions and 365 deletions
@@ -82,6 +82,8 @@ export type ListBranchesFilters = {
readonly workingHoursStart?: string;
readonly workingHoursEnd?: string;
readonly search?: string;
readonly orderBy?: string;
readonly orderType?: string;
readonly limit: number;
readonly offset: number;
};
@@ -5,7 +5,8 @@ import {
Injectable,
NotFoundException,
} from '@nestjs/common';
import { and, asc, count, eq, ilike, inArray, or, SQL } from 'drizzle-orm';
import { and, count, eq, ilike, inArray, or, SQL } from 'drizzle-orm';
import { toOrderClauses } from '../../../common/http/response';
import { alias } from 'drizzle-orm/pg-core';
import { DateTime } from '../../../common/value-objects/date-time/date-time';
import { PhoneNumber } from '../../../common/value-objects/phone-number/phone-number';
@@ -24,6 +25,18 @@ import type {
UpdateBranchInput,
} from './branch';
const BRANCH_ORDER_COLUMNS = {
id: branches.id,
code: branches.code,
name: branches.name,
phone: branches.phone,
address: branches.address,
nfcId: branches.nfcId,
status: branches.status,
createdAt: branches.createdAt,
updatedAt: branches.updatedAt,
};
const createdByUsers = alias(users, 'created_by_users');
const updatedByUsers = alias(users, 'updated_by_users');
@@ -52,7 +65,11 @@ export class BranchesRepository {
qb = this.extendListQuery(qb, filters);
const rows = await qb
.where(where)
.orderBy(asc(branches.code))
.orderBy(
...toOrderClauses(BRANCH_ORDER_COLUMNS, filters, [
{ column: 'code', type: 'ASC' },
]),
)
.limit(filters.limit)
.offset(filters.offset);
@@ -4,7 +4,12 @@ import {
NotFoundException,
} from '@nestjs/common';
import type { PaginationResponse } from '../../../common/http/response';
import { toListPage } from '../../../common/http/response';
import {
pickRelation,
pickUserRelation,
DEFAULT_RELATION_FIELDS,
toListPage,
} from '../../../common/http/response';
import { InvalidPhoneNumberError } from '../../../common/value-objects/phone-number/invalid-phone-number.error';
import { PhoneNumber } from '../../../common/value-objects/phone-number/phone-number';
import { Status } from '../../../common/value-objects/status/status';
@@ -36,6 +41,8 @@ export type ListBranchesQuery = {
readonly workingHoursStart?: string;
readonly workingHoursEnd?: string;
readonly search?: string;
readonly orderBy?: string;
readonly orderType?: string;
readonly page?: number;
readonly limit?: number;
readonly offset?: number;
@@ -94,6 +101,8 @@ export class BranchesService {
workingHoursStart: query.workingHoursStart,
workingHoursEnd: query.workingHoursEnd,
search: query.search,
orderBy: query.orderBy,
orderType: query.orderType,
limit: page.limit,
offset: page.offset,
});
@@ -341,31 +350,15 @@ export class BranchesService {
workingHoursStart: branch.workingHoursStart,
workingHoursEnd: branch.workingHoursEnd,
nfcId: branch.nfcId,
division: this.toDivisionItem(branch.division),
division: pickRelation(branch.division, DEFAULT_RELATION_FIELDS),
status: branch.status.value,
createdAt: branch.createdAt.value,
updatedAt: branch.updatedAt.value,
createdBy: this.toUserItem(branch.createdByUser),
updatedBy: this.toUserItem(branch.updatedByUser),
createdBy: pickUserRelation(branch.createdByUser),
updatedBy: pickUserRelation(branch.updatedByUser),
};
}
private toDivisionItem(
division: { id: string; code: string; name: string } | null,
): { id: string; code: string; name: string } | null {
if (division == null) {
return null;
}
return { id: division.id, code: division.code, name: division.name };
}
private toUserItem(user: { id: string; username: string }): {
id: string;
username: string;
} {
return { id: user.id, username: user.username };
}
get visibleFields(): readonly string[] {
return VISIBLE_FIELDS;
}