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 { PhoneNumber } from '../../../common/value-objects/phone-number/phone-number';
import { Status } from '../../../common/value-objects/status/status';
@@ -26,6 +28,18 @@ import type {
UpdateCustomerInput,
} from './customer';
const CUSTOMER_ORDER_COLUMNS = {
id: customers.id,
code: customers.code,
name: customers.name,
phone: customers.phone,
address: customers.address,
nfcId: customers.nfcId,
status: customers.status,
createdAt: customers.createdAt,
updatedAt: customers.updatedAt,
};
type QueryExecutor = Pick<DrizzleDB, 'select' | 'insert' | 'delete'>;
@Injectable()
@@ -46,12 +60,16 @@ export class CustomersRepository {
qb = this.extendListQuery(qb, filters);
const rows = await qb
.where(where)
.orderBy(asc(customers.code))
.orderBy(
...toOrderClauses(CUSTOMER_ORDER_COLUMNS, filters, [
{ column: 'code', type: 'ASC' },
]),
)
.limit(filters.limit)
.offset(filters.offset);
return {
data: rows.map((row) => this.toDomain(row, [])),
data: await Promise.all(rows.map((row) => this.hydrate(row, []))),
total: Number(totalRow?.total ?? 0),
};
}
@@ -75,7 +93,7 @@ export class CustomersRepository {
return null;
}
const contacts = await this.selectContacts(this.db, id);
return this.toDomain(row, contacts);
return this.hydrate(row, contacts);
}
async findByCode(code: string): Promise<Customer | null> {
@@ -89,7 +107,7 @@ export class CustomersRepository {
return null;
}
const contacts = await this.selectContacts(this.db, row.id);
return this.toDomain(row, contacts);
return this.hydrate(row, contacts);
}
async create(input: CreateCustomerInput): Promise<Customer> {
@@ -104,7 +122,7 @@ export class CustomersRepository {
const row = inserted[0];
await this.replaceContacts(tx, row.id, input.contacts ?? []);
const contacts = await this.selectContacts(tx, row.id);
return this.toDomain(row, contacts);
return this.hydrate(row, contacts);
});
} catch (error) {
this.rethrowConstraintViolation(error);
@@ -170,7 +188,7 @@ export class CustomersRepository {
await this.replaceContacts(tx, id, input.contacts);
}
const contacts = await this.selectContacts(tx, id);
return this.toDomain(row, contacts);
return this.hydrate(row, contacts);
});
} catch (error) {
this.rethrowConstraintViolation(error);
@@ -197,7 +215,7 @@ export class CustomersRepository {
throw new NotFoundException('Customer not found');
}
const contacts = await this.selectContacts(this.db, id);
return this.toDomain(row, contacts);
return this.hydrate(row, contacts);
}
async bulkUpdateStatus(
@@ -437,10 +455,17 @@ export class CustomersRepository {
};
}
private toDomain(
private async hydrate(
row: CustomerRow,
contactRows: CustomerContactRow[],
): Customer {
): Promise<Customer> {
const [item] = await attachAuditUsers(this.db, [
this.toDomain(row, contactRows),
]);
return item;
}
private toDomain(row: CustomerRow, contactRows: CustomerContactRow[]) {
return {
id: row.id,
code: row.code,