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
@@ -5,6 +5,8 @@ import {
NotFoundException,
} from '@nestjs/common';
import { and, asc, count, eq, ilike, inArray, or, SQL } from 'drizzle-orm';
import { toOrderClauses } from '../../common/http/response';
import { attachAuditUsers } from '../../database/load-user-refs';
import { DateTime } from '../../common/value-objects/date-time/date-time';
import { Status } from '../../common/value-objects/status/status';
import { DRIZZLE, type DrizzleDB } from '../../database/database.module';
@@ -30,6 +32,22 @@ import type {
UpdatePrivilegeInput,
} from './privilege';
const PRIVILEGE_ORDER_COLUMNS = {
id: privileges.id,
name: privileges.name,
code: privileges.code,
status: privileges.status,
createdAt: privileges.createdAt,
updatedAt: privileges.updatedAt,
};
const PRIVILEGE_KEY_ORDER_COLUMNS = {
id: privilegeKeys.id,
code: privilegeKeys.code,
label: privilegeKeys.label,
sortOrder: privilegeKeys.sortOrder,
};
@Injectable()
export class PrivilegesRepository {
constructor(@Inject(DRIZZLE) private readonly db: DrizzleDB) {}
@@ -47,12 +65,16 @@ export class PrivilegesRepository {
qb = this.extendListQuery(qb, filters);
const rows = await qb
.where(where)
.orderBy(asc(privileges.code))
.orderBy(
...toOrderClauses(PRIVILEGE_ORDER_COLUMNS, filters, [
{ column: 'code', type: 'ASC' },
]),
)
.limit(filters.limit)
.offset(filters.offset);
return {
data: rows.map((row) => this.toDomain(row)),
data: await this.hydrate(rows),
total: Number(totalRow?.total ?? 0),
};
}
@@ -75,7 +97,8 @@ export class PrivilegesRepository {
return null;
}
const details = await this.loadDetails(id);
return { ...this.toDomain(row), details };
const [base] = await this.hydrate([row]);
return { ...base, details };
}
async findByCode(code: string): Promise<Privilege | null> {
@@ -84,7 +107,7 @@ export class PrivilegesRepository {
.from(privileges)
.where(eq(privileges.code, code))
.limit(1);
return row ? this.toDomain(row) : null;
return row ? this.hydrateOne(row) : null;
}
async create(input: CreatePrivilegeInput): Promise<PrivilegeWithDetails> {
@@ -117,7 +140,8 @@ export class PrivilegesRepository {
}
const details = await this.loadDetails(row.id, tx);
return { ...this.toDomain(row), details };
const [base] = await this.hydrate([row]);
return { ...base, details };
});
} catch (error) {
this.rethrowUniqueViolation(error);
@@ -190,7 +214,8 @@ export class PrivilegesRepository {
}
const details = await this.loadDetails(id, tx);
return { ...this.toDomain(row), details };
const [base] = await this.hydrate([row]);
return { ...base, details };
});
} catch (error) {
this.rethrowUniqueViolation(error);
@@ -215,7 +240,7 @@ export class PrivilegesRepository {
if (!row) {
throw new NotFoundException('Privilege not found');
}
return this.toDomain(row);
return this.hydrateOne(row);
}
async bulkUpdateStatus(
@@ -297,7 +322,12 @@ export class PrivilegesRepository {
.select()
.from(privilegeKeys)
.where(where)
.orderBy(asc(privilegeKeys.sortOrder), asc(privilegeKeys.code))
.orderBy(
...toOrderClauses(PRIVILEGE_KEY_ORDER_COLUMNS, filters, [
{ column: 'sortOrder', type: 'ASC' },
{ column: 'code', type: 'ASC' },
]),
)
.limit(filters.limit)
.offset(filters.offset);
@@ -451,7 +481,7 @@ export class PrivilegesRepository {
}));
}
private toDomain(row: PrivilegeRow): Privilege {
private toDomain(row: PrivilegeRow) {
return {
id: row.id,
name: row.name,
@@ -464,6 +494,18 @@ export class PrivilegesRepository {
};
}
private async hydrate(rows: PrivilegeRow[]): Promise<Privilege[]> {
return attachAuditUsers(
this.db,
rows.map((row) => this.toDomain(row)),
);
}
private async hydrateOne(row: PrivilegeRow): Promise<Privilege> {
const [item] = await this.hydrate([row]);
return item;
}
private toKeyDomain(row: PrivilegeKeyRow): PrivilegeKey {
return {
id: row.id,