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:
@@ -1,3 +1,4 @@
|
||||
import type { UserRelation } from '../../../common/http/response';
|
||||
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';
|
||||
@@ -27,6 +28,8 @@ export type Customer = {
|
||||
readonly updatedAt: DateTime;
|
||||
readonly createdBy: string;
|
||||
readonly updatedBy: string;
|
||||
readonly createdByUser: UserRelation;
|
||||
readonly updatedByUser: UserRelation;
|
||||
};
|
||||
|
||||
export type CustomerContactInput = {
|
||||
@@ -79,6 +82,8 @@ export type ListCustomersFilters = {
|
||||
readonly nfcId?: string;
|
||||
readonly status?: string;
|
||||
readonly search?: string;
|
||||
readonly orderBy?: string;
|
||||
readonly orderType?: string;
|
||||
readonly limit: number;
|
||||
readonly offset: number;
|
||||
};
|
||||
|
||||
@@ -67,7 +67,13 @@ describe('CustomersRepository', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
where.mockImplementation(() => ({ limit, orderBy, returning }));
|
||||
where.mockImplementation(() =>
|
||||
Object.assign(Promise.resolve([{ id: 'user-1', username: 'admin' }]), {
|
||||
limit,
|
||||
orderBy,
|
||||
returning,
|
||||
}),
|
||||
);
|
||||
orderBy.mockImplementation(() => ({ limit }));
|
||||
limit.mockImplementation(() => ({ offset }));
|
||||
offset.mockResolvedValue([row]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -52,6 +52,8 @@ describe('CustomersService', () => {
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
createdBy: 'user-1',
|
||||
createdByUser: { id: 'user-1', username: 'admin' },
|
||||
updatedByUser: { id: 'user-1', username: 'admin' },
|
||||
updatedBy: 'user-1',
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import type { PaginationResponse } from '../../../common/http/response';
|
||||
import { toListPage } from '../../../common/http/response';
|
||||
import { pickUserRelation, 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';
|
||||
@@ -38,6 +38,8 @@ export type ListCustomersQuery = {
|
||||
readonly nfcId?: string;
|
||||
readonly status?: string;
|
||||
readonly search?: string;
|
||||
readonly orderBy?: string;
|
||||
readonly orderType?: string;
|
||||
readonly page?: number;
|
||||
readonly limit?: number;
|
||||
readonly offset?: number;
|
||||
@@ -85,6 +87,8 @@ export class CustomersService {
|
||||
nfcId: query.nfcId,
|
||||
status: query.status,
|
||||
search: query.search,
|
||||
orderBy: query.orderBy,
|
||||
orderType: query.orderType,
|
||||
limit: page.limit,
|
||||
offset: page.offset,
|
||||
});
|
||||
@@ -350,8 +354,8 @@ export class CustomersService {
|
||||
status: customer.status.value,
|
||||
createdAt: customer.createdAt.value,
|
||||
updatedAt: customer.updatedAt.value,
|
||||
createdBy: customer.createdBy,
|
||||
updatedBy: customer.updatedBy,
|
||||
createdBy: pickUserRelation(customer.createdByUser),
|
||||
updatedBy: pickUserRelation(customer.updatedByUser),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@ import {
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { PaginationQueryDto } from '../../../../common/http/response';
|
||||
import {
|
||||
PaginationQueryDto,
|
||||
UserRelationDto,
|
||||
} from '../../../../common/http/response';
|
||||
import { CORE_STATUSES } from '../../../../common/value-objects/status/status';
|
||||
import {
|
||||
CONTACT_JOB_TITLE_MAX_LENGTH,
|
||||
@@ -347,9 +350,9 @@ export class CustomerDto {
|
||||
@ApiProperty({ description: 'Unix ms' })
|
||||
updatedAt!: number;
|
||||
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
createdBy!: string;
|
||||
@ApiProperty({ type: UserRelationDto })
|
||||
createdBy!: UserRelationDto;
|
||||
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
updatedBy!: string;
|
||||
@ApiProperty({ type: UserRelationDto })
|
||||
updatedBy!: UserRelationDto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user