Files
trackgo-be/src/modules/configuration/divisions/divisions.repository.ts
T
shancheas 4c45a4371e 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.
2026-08-27 13:09:41 +07:00

298 lines
8.0 KiB
TypeScript

import {
ConflictException,
Inject,
Injectable,
NotFoundException,
} from '@nestjs/common';
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,
Division,
ListDivisionsFilters,
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) {}
async list(
filters: ListDivisionsFilters,
): Promise<{ data: Division[]; total: number }> {
const where = this.buildListWhere(filters);
const totalRows = await this.db
.select({ total: count() })
.from(divisions)
.where(where);
const totalRow = totalRows[0];
let qb = this.db.select().from(divisions).$dynamic();
qb = this.extendListQuery(qb, filters);
const rows = await qb
.where(where)
.orderBy(
...toOrderClauses(DIVISION_ORDER_COLUMNS, filters, [
{ column: 'code', type: 'ASC' },
]),
)
.limit(filters.limit)
.offset(filters.offset);
return {
data: await this.hydrate(rows),
total: Number(totalRow?.total ?? 0),
};
}
/**
* Hook for modules to add joins/extra predicates without forking list.
*/
extendListQuery<T>(qb: T, filters: ListDivisionsFilters): T {
void filters;
return qb;
}
async findById(id: string): Promise<Division | null> {
const rows: DivisionRow[] = await this.db
.select()
.from(divisions)
.where(eq(divisions.id, id))
.limit(1);
const row = rows[0];
return row ? this.hydrateOne(row) : null;
}
async findByCode(code: string): Promise<Division | null> {
const rows = await this.db
.select()
.from(divisions)
.where(eq(divisions.code, code))
.limit(1);
const row = rows[0];
return row ? this.hydrateOne(row) : null;
}
async create(input: CreateDivisionInput): Promise<Division> {
const now = DateTime.fromUnixMs(Date.now());
const status = input.status ?? Status.create(Status.DEFAULT);
try {
const inserted = await this.db
.insert(divisions)
.values({
name: input.name,
code: input.code,
status: status.value,
createdAt: now.value,
updatedAt: now.value,
createdBy: input.userId,
updatedBy: input.userId,
})
.returning();
const row = inserted[0];
return this.hydrateOne(row);
} catch (error) {
this.rethrowUniqueViolation(error);
}
}
async createMany(inputs: CreateDivisionInput[]): Promise<number> {
if (inputs.length === 0) {
return 0;
}
const now = DateTime.fromUnixMs(Date.now());
try {
await this.db.transaction(async (tx) => {
for (const input of inputs) {
const status = input.status ?? Status.create(Status.DEFAULT);
await tx.insert(divisions).values({
name: input.name,
code: input.code,
status: status.value,
createdAt: now.value,
updatedAt: now.value,
createdBy: input.userId,
updatedBy: input.userId,
});
}
});
return inputs.length;
} catch (error) {
this.rethrowUniqueViolation(error);
}
}
async update(id: string, input: UpdateDivisionInput): Promise<Division> {
const existing = await this.findById(id);
if (!existing) {
throw new NotFoundException('Division not found');
}
const now = DateTime.fromUnixMs(Date.now());
try {
const updated = await this.db
.update(divisions)
.set({
name: input.name ?? existing.name,
code: input.code ?? existing.code,
updatedAt: now.value,
updatedBy: input.userId,
})
.where(eq(divisions.id, id))
.returning();
const row = updated[0];
return this.hydrateOne(row);
} catch (error) {
this.rethrowUniqueViolation(error);
}
}
async updateStatus(
id: string,
status: Status,
userId: string,
): Promise<Division> {
const now = DateTime.fromUnixMs(Date.now());
const updated = await this.db
.update(divisions)
.set({
status: status.value,
updatedAt: now.value,
updatedBy: userId,
})
.where(eq(divisions.id, id))
.returning();
const row = updated[0];
if (!row) {
throw new NotFoundException('Division not found');
}
return this.hydrateOne(row);
}
async bulkUpdateStatus(
ids: string[],
status: Status,
userId: string,
): Promise<number> {
if (ids.length === 0) {
return 0;
}
const now = DateTime.fromUnixMs(Date.now());
const rows = await this.db
.update(divisions)
.set({
status: status.value,
updatedAt: now.value,
updatedBy: userId,
})
.where(inArray(divisions.id, ids))
.returning({ id: divisions.id });
return rows.length;
}
async delete(id: string): Promise<void> {
try {
const deleted = await this.db
.delete(divisions)
.where(eq(divisions.id, id))
.returning({ id: divisions.id });
if (deleted.length === 0) {
throw new NotFoundException('Division not found');
}
} catch (error) {
this.rethrowForeignKeyViolation(error);
}
}
async bulkDelete(ids: string[]): Promise<number> {
if (ids.length === 0) {
return 0;
}
try {
const deleted = await this.db
.delete(divisions)
.where(inArray(divisions.id, ids))
.returning({ id: divisions.id });
return deleted.length;
} catch (error) {
this.rethrowForeignKeyViolation(error);
}
}
private buildListWhere(filters: ListDivisionsFilters): SQL | undefined {
const parts: SQL[] = [];
if (filters.name) {
parts.push(ilike(divisions.name, `%${filters.name}%`));
}
if (filters.code) {
parts.push(ilike(divisions.code, `%${filters.code}%`));
}
if (filters.status) {
parts.push(eq(divisions.status, filters.status));
}
if (filters.search) {
const search = or(
ilike(divisions.name, `%${filters.search}%`),
ilike(divisions.code, `%${filters.search}%`),
);
if (search) {
parts.push(search);
}
}
if (parts.length === 0) {
return undefined;
}
return parts.length === 1 ? parts[0] : and(...parts);
}
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 {
const err = error as { code?: string };
if (err.code === '23505') {
throw new ConflictException('Division code already exists');
}
throw error;
}
private rethrowForeignKeyViolation(error: unknown): never {
const err = error as { code?: string };
if (err.code === '23503') {
throw new ConflictException('Division is referenced by other records');
}
throw error;
}
}