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
@@ -4,10 +4,12 @@ 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 { 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';
import { attachAuditUsers } from '../../../database/load-user-refs';
import { divisions, type DivisionRow } from '../../../database/schema';
import type {
CreateDivisionInput,
@@ -16,6 +18,15 @@ import type {
UpdateDivisionInput,
} from './division';
const DIVISION_ORDER_COLUMNS = {
id: divisions.id,
name: divisions.name,
code: divisions.code,
status: divisions.status,
createdAt: divisions.createdAt,
updatedAt: divisions.updatedAt,
};
@Injectable()
export class DivisionsRepository {
constructor(@Inject(DRIZZLE) private readonly db: DrizzleDB) {}
@@ -34,12 +45,16 @@ export class DivisionsRepository {
qb = this.extendListQuery(qb, filters);
const rows = await qb
.where(where)
.orderBy(asc(divisions.code))
.orderBy(
...toOrderClauses(DIVISION_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),
};
}
@@ -59,7 +74,7 @@ export class DivisionsRepository {
.where(eq(divisions.id, id))
.limit(1);
const row = rows[0];
return row ? this.toDomain(row) : null;
return row ? this.hydrateOne(row) : null;
}
async findByCode(code: string): Promise<Division | null> {
@@ -69,7 +84,7 @@ export class DivisionsRepository {
.where(eq(divisions.code, code))
.limit(1);
const row = rows[0];
return row ? this.toDomain(row) : null;
return row ? this.hydrateOne(row) : null;
}
async create(input: CreateDivisionInput): Promise<Division> {
@@ -89,7 +104,7 @@ export class DivisionsRepository {
})
.returning();
const row = inserted[0];
return this.toDomain(row);
return this.hydrateOne(row);
} catch (error) {
this.rethrowUniqueViolation(error);
}
@@ -139,7 +154,7 @@ export class DivisionsRepository {
.where(eq(divisions.id, id))
.returning();
const row = updated[0];
return this.toDomain(row);
return this.hydrateOne(row);
} catch (error) {
this.rethrowUniqueViolation(error);
}
@@ -164,7 +179,7 @@ export class DivisionsRepository {
if (!row) {
throw new NotFoundException('Division not found');
}
return this.toDomain(row);
return this.hydrateOne(row);
}
async bulkUpdateStatus(
@@ -243,17 +258,25 @@ export class DivisionsRepository {
return parts.length === 1 ? parts[0] : and(...parts);
}
private toDomain(row: DivisionRow): Division {
return {
id: row.id,
name: row.name,
code: row.code,
status: Status.create(row.status),
createdAt: DateTime.fromUnixMs(row.createdAt),
updatedAt: DateTime.fromUnixMs(row.updatedAt),
createdBy: row.createdBy,
updatedBy: row.updatedBy,
};
private async hydrate(rows: DivisionRow[]): Promise<Division[]> {
return attachAuditUsers(
this.db,
rows.map((row) => ({
id: row.id,
name: row.name,
code: row.code,
status: Status.create(row.status),
createdAt: DateTime.fromUnixMs(row.createdAt),
updatedAt: DateTime.fromUnixMs(row.updatedAt),
createdBy: row.createdBy,
updatedBy: row.updatedBy,
})),
);
}
private async hydrateOne(row: DivisionRow): Promise<Division> {
const [item] = await this.hydrate([row]);
return item;
}
private rethrowUniqueViolation(error: unknown): never {