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,7 +4,13 @@ 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 {
catalogRelationFromMap,
loadProductRelationMap,
} from '../../../database/load-catalog-refs';
import { attachSalesHeaderRelations } from '../shared/attach-sales-relations';
import { DateTime } from '../../../common/value-objects/date-time/date-time';
import { Decimal } from '../../../common/value-objects/decimal/decimal';
import { Status } from '../../../common/value-objects/status/status';
@@ -29,6 +35,15 @@ import type {
UpdateSalesRequestInput,
} from './sales-request';
const SALES_REQUEST_ORDER_COLUMNS = {
id: salesRequests.id,
code: salesRequests.code,
date: salesRequests.date,
status: salesRequests.status,
createdAt: salesRequests.createdAt,
updatedAt: salesRequests.updatedAt,
};
type QueryExecutor = Pick<DrizzleDB, 'select' | 'insert' | 'delete'>;
@Injectable()
@@ -50,11 +65,15 @@ export class SalesRequestsRepository {
.select()
.from(salesRequests)
.where(where)
.orderBy(asc(salesRequests.code))
.orderBy(
...toOrderClauses(SALES_REQUEST_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.map((row) => this.toDomain(row, [], []))),
total: Number(totalRows[0]?.total ?? 0),
};
}
@@ -71,7 +90,7 @@ export class SalesRequestsRepository {
}
const products = await this.selectProducts(this.db, id);
const images = await this.selectImages(this.db, id);
return this.toDomain(row, products, images);
return this.hydrateOne(row, products, images);
}
async create(input: CreateSalesRequestInput): Promise<SalesRequest> {
@@ -96,7 +115,7 @@ export class SalesRequestsRepository {
await this.replaceImages(tx, row.id, input.images ?? []);
const products = await this.selectProducts(tx, row.id);
const images = await this.selectImages(tx, row.id);
return this.toDomain(row, products, images);
return this.hydrateOne(row, products, images);
});
} catch (error) {
this.rethrowConstraintViolation(error);
@@ -158,7 +177,7 @@ export class SalesRequestsRepository {
}
const products = await this.selectProducts(tx, id);
const images = await this.selectImages(tx, id);
return this.toDomain(row, products, images);
return this.hydrateOne(row, products, images);
});
} catch (error) {
this.rethrowConstraintViolation(error);
@@ -186,7 +205,7 @@ export class SalesRequestsRepository {
}
const products = await this.selectProducts(this.db, id);
const images = await this.selectImages(this.db, id);
return this.toDomain(row, products, images);
return this.hydrateOne(row, products, images);
}
async bulkUpdateStatus(
@@ -374,6 +393,7 @@ export class SalesRequestsRepository {
products: productRows.map((line) => ({
id: line.id,
productId: line.productId,
product: null,
quantity: Decimal.create(line.quantity),
price: Decimal.create(line.price),
})),
@@ -387,9 +407,39 @@ export class SalesRequestsRepository {
updatedAt: DateTime.fromUnixMs(row.updatedAt),
createdBy: row.createdBy,
updatedBy: row.updatedBy,
salesPerson: null,
branch: null,
division: null,
customer: null,
createdByUser: { id: row.createdBy, username: '' },
updatedByUser: { id: row.updatedBy, username: '' },
};
}
private async hydrate(items: SalesRequest[]): Promise<SalesRequest[]> {
const withHeader = await attachSalesHeaderRelations(this.db, items);
const productIds = withHeader.flatMap((item) =>
item.products.map((line) => line.productId),
);
const products = await loadProductRelationMap(this.db, productIds);
return withHeader.map((item) => ({
...item,
products: item.products.map((line) => ({
...line,
product: catalogRelationFromMap(products, line.productId),
})),
}));
}
private async hydrateOne(
row: SalesRequestRow,
products: SalesRequestProductRow[],
images: SalesRequestImageRow[],
): Promise<SalesRequest> {
const [item] = await this.hydrate([this.toDomain(row, products, images)]);
return item;
}
private rethrowConstraintViolation(error: unknown): never {
if (error instanceof NotFoundException) {
throw error;