mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
176 lines
5.9 KiB
TypeScript
176 lines
5.9 KiB
TypeScript
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
|
import { DataSource, EntityTarget, FindOptionsWhere, ObjectLiteral } from 'typeorm';
|
|
|
|
export type OrderField = 'displayOrder' | 'stepOrder';
|
|
|
|
@Injectable()
|
|
export class DisplayOrderService {
|
|
constructor(private readonly dataSource: DataSource) {}
|
|
|
|
async getMaxOrder<T extends ObjectLiteral>(
|
|
entity: EntityTarget<T>,
|
|
field: OrderField,
|
|
where?: FindOptionsWhere<T>,
|
|
): Promise<number> {
|
|
const repo = this.dataSource.getRepository(entity);
|
|
const qb = repo.createQueryBuilder('e').select(`MAX(e.${field})`, 'max');
|
|
if (where) {
|
|
Object.entries(where).forEach(([key, value]) => {
|
|
if (value !== undefined) {
|
|
qb.andWhere(`e.${key} = :${key}`, { [key]: value });
|
|
}
|
|
});
|
|
}
|
|
const row = await qb.getRawOne<{ max: string | null }>();
|
|
return row?.max ? Number(row.max) : 0;
|
|
}
|
|
|
|
async resolveCreateOrder<T extends ObjectLiteral>(
|
|
entity: EntityTarget<T>,
|
|
field: OrderField,
|
|
options: {
|
|
explicitOrder?: number;
|
|
insertAfterId?: string;
|
|
scopeWhere?: FindOptionsWhere<T>;
|
|
},
|
|
): Promise<number> {
|
|
const { explicitOrder, insertAfterId, scopeWhere } = options;
|
|
|
|
if (insertAfterId) {
|
|
if (explicitOrder !== undefined) {
|
|
throw new BadRequestException('Cannot set both explicit order and insertAfterId');
|
|
}
|
|
const repo = this.dataSource.getRepository(entity);
|
|
const after = await repo.findOne({
|
|
where: { id: insertAfterId, ...scopeWhere } as unknown as FindOptionsWhere<T>,
|
|
});
|
|
if (!after) {
|
|
throw new NotFoundException(`Record ${insertAfterId} not found in scope`);
|
|
}
|
|
const afterOrder = Number((after as Record<string, unknown>)[field]);
|
|
await this.shiftOrdersFrom(entity, field, afterOrder + 1, 1, scopeWhere);
|
|
return afterOrder + 1;
|
|
}
|
|
|
|
if (explicitOrder !== undefined) {
|
|
return explicitOrder;
|
|
}
|
|
|
|
const max = await this.getMaxOrder(entity, field, scopeWhere);
|
|
return max + 1;
|
|
}
|
|
|
|
async reorderByIds<T extends ObjectLiteral>(
|
|
entity: EntityTarget<T>,
|
|
field: OrderField,
|
|
ids: string[],
|
|
scopeWhere?: FindOptionsWhere<T>,
|
|
): Promise<void> {
|
|
const repo = this.dataSource.getRepository(entity);
|
|
const existing = await repo.find({
|
|
where: scopeWhere,
|
|
order: { [field]: 'ASC' } as never,
|
|
});
|
|
|
|
const scopedIds = new Set(existing.map((row) => String(row.id)));
|
|
if (ids.length !== scopedIds.size) {
|
|
throw new BadRequestException('Reorder list must include every item in scope exactly once');
|
|
}
|
|
for (const id of ids) {
|
|
if (!scopedIds.has(id)) {
|
|
throw new BadRequestException(`ID ${id} is not in the reorder scope`);
|
|
}
|
|
}
|
|
|
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
try {
|
|
for (let i = 0; i < ids.length; i++) {
|
|
await queryRunner.manager.update(entity, ids[i], { [field]: -(i + 1) } as never);
|
|
}
|
|
for (let i = 0; i < ids.length; i++) {
|
|
await queryRunner.manager.update(entity, ids[i], { [field]: i + 1 } as never);
|
|
}
|
|
await queryRunner.commitTransaction();
|
|
} catch (err) {
|
|
await queryRunner.rollbackTransaction();
|
|
throw err;
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
|
|
async moveOne<T extends ObjectLiteral>(
|
|
entity: EntityTarget<T>,
|
|
field: OrderField,
|
|
id: string,
|
|
direction: 'up' | 'down',
|
|
scopeWhere?: FindOptionsWhere<T>,
|
|
): Promise<void> {
|
|
const repo = this.dataSource.getRepository(entity);
|
|
const items = await repo.find({
|
|
where: scopeWhere,
|
|
order: { [field]: 'ASC' } as never,
|
|
});
|
|
|
|
const index = items.findIndex((row) => String(row.id) === id);
|
|
if (index === -1) {
|
|
throw new NotFoundException(`Record ${id} not found in scope`);
|
|
}
|
|
|
|
const targetIndex = direction === 'up' ? index - 1 : index + 1;
|
|
if (targetIndex < 0 || targetIndex >= items.length) {
|
|
throw new BadRequestException(`Cannot move ${direction}`);
|
|
}
|
|
|
|
const current = items[index] as Record<string, unknown>;
|
|
const neighbor = items[targetIndex] as Record<string, unknown>;
|
|
const currentOrder = Number(current[field]);
|
|
const neighborOrder = Number(neighbor[field]);
|
|
|
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
try {
|
|
await queryRunner.manager.update(entity, String(current.id), { [field]: -1 } as never);
|
|
await queryRunner.manager.update(entity, String(neighbor.id), { [field]: -2 } as never);
|
|
await queryRunner.manager.update(entity, String(current.id), { [field]: neighborOrder } as never);
|
|
await queryRunner.manager.update(entity, String(neighbor.id), { [field]: currentOrder } as never);
|
|
await queryRunner.commitTransaction();
|
|
} catch (err) {
|
|
await queryRunner.rollbackTransaction();
|
|
throw err;
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
|
|
private async shiftOrdersFrom<T extends ObjectLiteral>(
|
|
entity: EntityTarget<T>,
|
|
field: OrderField,
|
|
fromOrder: number,
|
|
delta: number,
|
|
scopeWhere?: FindOptionsWhere<T>,
|
|
): Promise<void> {
|
|
const repo = this.dataSource.getRepository(entity);
|
|
const orderColumn = repo.metadata.findColumnWithPropertyName(field)?.databaseName ?? field;
|
|
const qb = repo
|
|
.createQueryBuilder()
|
|
.update()
|
|
.set({ [field]: () => `"${orderColumn}" + ${delta}` } as never)
|
|
.where(`"${orderColumn}" >= :fromOrder`, { fromOrder });
|
|
|
|
if (scopeWhere) {
|
|
Object.entries(scopeWhere).forEach(([key, value]) => {
|
|
if (value !== undefined) {
|
|
const col = repo.metadata.findColumnWithPropertyName(key)?.databaseName ?? key;
|
|
qb.andWhere(`"${col}" = :scope_${key}`, { [`scope_${key}`]: value });
|
|
}
|
|
});
|
|
}
|
|
|
|
await qb.execute();
|
|
}
|
|
}
|