Skip to content

Commit 073015c

Browse files
Merge pull request #562 from NteinPrecious/feat/replace-hard-delete-with-soft-delete-and-add-asset-restore-endpoint
feat: replace hard delete with soft delete and add asset restore endp…
2 parents 0c8b782 + 6d2a16e commit 073015c

File tree

5 files changed

+56
-65
lines changed

5 files changed

+56
-65
lines changed

backend/src/assets/asset.entity.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
JoinColumn,
77
CreateDateColumn,
88
UpdateDateColumn,
9+
DeleteDateColumn,
910
} from 'typeorm';
1011
import { User } from '../users/user.entity';
1112
import { Department } from '../departments/department.entity';
@@ -108,4 +109,7 @@ export class Asset {
108109

109110
@UpdateDateColumn()
110111
updatedAt: Date;
112+
113+
@DeleteDateColumn({ nullable: true })
114+
deletedAt: Date | null;
111115
}

backend/src/assets/assets.controller.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ export class AssetsController {
4343

4444
@Get()
4545
@ApiOperation({ summary: 'List all assets with optional filters and pagination' })
46-
findAll(@Query() filters: AssetFiltersDto) {
47-
return this.service.findAll(filters);
46+
findAll(@Query() filters: AssetFiltersDto, @CurrentUser() user: User) {
47+
return this.service.findAll(filters, user);
4848
}
4949

5050
@Get(':id')
@@ -86,10 +86,17 @@ export class AssetsController {
8686

8787
@Delete(':id')
8888
@HttpCode(HttpStatus.NO_CONTENT)
89-
@ApiOperation({ summary: 'Delete an asset' })
89+
@ApiOperation({ summary: 'Soft-delete an asset' })
9090
@Roles(UserRole.ADMIN, UserRole.MANAGER)
91-
remove(@Param('id') id: string) {
92-
return this.service.remove(id);
91+
remove(@Param('id') id: string, @CurrentUser() user: User) {
92+
return this.service.remove(id, user);
93+
}
94+
95+
@Post(':id/restore')
96+
@ApiOperation({ summary: 'Restore a soft-deleted asset (ADMIN only)' })
97+
@Roles(UserRole.ADMIN)
98+
restore(@Param('id') id: string, @CurrentUser() user: User) {
99+
return this.service.restore(id, user);
93100
}
94101

95102
@Get(':id/history')

backend/src/assets/assets.service.ts

Lines changed: 30 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
2-
import { PaginatedResponse } from '../common/dto/paginated-response.dto';
1+
import { Injectable, Logger, NotFoundException, ForbiddenException } from '@nestjs/common';
32
import { InjectRepository } from '@nestjs/typeorm';
43
import { Repository } from 'typeorm';
54
import { ConfigService } from '@nestjs/config';
@@ -19,6 +18,7 @@ import { UpdateMaintenanceDto } from './dto/update-maintenance.dto';
1918
import { CreateDocumentDto } from './dto/create-document.dto';
2019
import { DuplicateAssetDto } from './dto/duplicate-asset.dto';
2120
import { AssetStatus, AssetHistoryAction, StellarStatus } from './enums';
21+
import { UserRole } from '../users/user.entity';
2222
import { DepartmentsService } from '../departments/departments.service';
2323
import { CategoriesService } from '../categories/categories.service';
2424
import { UsersService } from '../users/users.service';
@@ -51,8 +51,12 @@ export class AssetsService {
5151
private readonly storageService: StorageService,
5252
) {}
5353

54-
async findAll(filters: AssetFiltersDto): Promise<PaginatedResponse<Asset>> {
55-
const { search, status, condition, categoryId, departmentId, page = 1, limit = 20 } = filters;
54+
async findAll(filters: AssetFiltersDto, currentUser?: User): Promise<{ data: Asset[]; total: number; page: number; limit: number }> {
55+
const { search, status, condition, categoryId, departmentId, page = 1, limit = 20, includeDeleted = false } = filters;
56+
57+
if (includeDeleted && currentUser?.role !== UserRole.ADMIN) {
58+
throw new ForbiddenException('Only admins can view deleted assets');
59+
}
5660

5761
const qb = this.assetsRepo
5862
.createQueryBuilder('asset')
@@ -62,6 +66,10 @@ export class AssetsService {
6266
.leftJoinAndSelect('asset.createdBy', 'createdBy')
6367
.leftJoinAndSelect('asset.updatedBy', 'updatedBy');
6468

69+
if (includeDeleted) {
70+
qb.withDeleted();
71+
}
72+
6573
if (search) {
6674
// Multi-column ILIKE search — covers name, assetId, serialNumber, location, notes,
6775
// category.name, department.name (case-insensitive, partial match).
@@ -70,13 +78,9 @@ export class AssetsService {
7078
// CREATE INDEX CONCURRENTLY idx_assets_name_gin ON assets USING gin(to_tsvector('english', name));
7179
// CREATE INDEX CONCURRENTLY idx_assets_serial_gin ON assets USING gin(to_tsvector('english', coalesce("serialNumber", '')));
7280
qb.andWhere(
73-
`(asset.name ILIKE :search
74-
OR asset.assetId ILIKE :search
75-
OR asset.serialNumber ILIKE :search
76-
OR asset.location ILIKE :search
77-
OR asset.notes ILIKE :search
78-
OR category.name ILIKE :search
79-
OR department.name ILIKE :search)`,
81+
`(asset.name ILIKE :search OR asset.assetId ILIKE :search OR asset.serialNumber ILIKE :search
82+
OR asset.location ILIKE :search OR asset.notes ILIKE :search
83+
OR category.name ILIKE :search OR department.name ILIKE :search)`,
8084
{ search: `%${search}%` },
8185
);
8286
}
@@ -249,56 +253,24 @@ export class AssetsService {
249253
return this.findOne(id);
250254
}
251255

252-
async duplicate(id: string, dto: DuplicateAssetDto, currentUser: User): Promise<Asset[]> {
253-
const source = await this.findOne(id);
254-
const quantity = dto.quantity ?? 1;
255-
const results: Asset[] = [];
256-
257-
for (let i = 0; i < quantity; i++) {
258-
const newAssetId = await this.generateAssetId();
259-
const copy = this.assetsRepo.create({
260-
assetId: newAssetId,
261-
name: dto.name ?? source.name,
262-
description: source.description,
263-
category: source.category,
264-
department: source.department,
265-
assignedTo: null,
266-
serialNumber: quantity === 1 && dto.serialNumber ? dto.serialNumber : null,
267-
purchaseDate: source.purchaseDate,
268-
purchasePrice: source.purchasePrice,
269-
currentValue: source.currentValue,
270-
warrantyExpiration: source.warrantyExpiration,
271-
status: AssetStatus.ACTIVE,
272-
condition: source.condition,
273-
location: source.location,
274-
manufacturer: source.manufacturer,
275-
model: source.model,
276-
tags: source.tags,
277-
notes: source.notes,
278-
customFields: source.customFields,
279-
imageUrls: source.imageUrls,
280-
createdBy: currentUser,
281-
updatedBy: currentUser,
282-
});
283-
284-
const saved = await this.assetsRepo.save(copy);
285-
await this.logHistory(
286-
saved,
287-
AssetHistoryAction.CREATED,
288-
`Duplicated from ${source.assetId}`,
289-
null,
290-
null,
291-
currentUser,
292-
);
293-
results.push(await this.findOne(saved.id));
294-
}
295-
296-
return results;
256+
async remove(id: string, currentUser: User): Promise<void> {
257+
await this.findOne(id);
258+
await this.assetsRepo.softDelete(id);
259+
await this.logHistory(
260+
{ id } as Asset,
261+
AssetHistoryAction.DELETED,
262+
'Asset soft-deleted',
263+
null,
264+
null,
265+
currentUser,
266+
);
297267
}
298268

299-
async remove(id: string): Promise<void> {
269+
async restore(id: string, currentUser: User): Promise<Asset> {
270+
await this.assetsRepo.restore(id);
300271
const asset = await this.findOne(id);
301-
await this.assetsRepo.remove(asset);
272+
await this.logHistory(asset, AssetHistoryAction.RESTORED, 'Asset restored', null, null, currentUser);
273+
return asset;
302274
}
303275

304276
async getHistory(assetId: string): Promise<AssetHistory[]> {

backend/src/assets/dto/asset-filters.dto.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ApiPropertyOptional } from '@nestjs/swagger';
2-
import { IsOptional, IsEnum, IsString, IsNumber, Min } from 'class-validator';
3-
import { Type } from 'class-transformer';
2+
import { IsOptional, IsEnum, IsString, IsNumber, Min, IsBoolean } from 'class-validator';
3+
import { Type, Transform } from 'class-transformer';
44
import { AssetStatus, AssetCondition } from '../enums';
55

66
export class AssetFiltersDto {
@@ -42,4 +42,10 @@ export class AssetFiltersDto {
4242
@IsOptional()
4343
@Type(() => Number)
4444
limit?: number = 20;
45+
46+
@ApiPropertyOptional({ default: false, description: 'Include soft-deleted assets (ADMIN only)' })
47+
@IsBoolean()
48+
@IsOptional()
49+
@Transform(({ value }) => value === 'true' || value === true)
50+
includeDeleted?: boolean = false;
4551
}

backend/src/assets/enums.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export enum AssetHistoryAction {
2121
MAINTENANCE = 'MAINTENANCE',
2222
NOTE_ADDED = 'NOTE_ADDED',
2323
DOCUMENT_UPLOADED = 'DOCUMENT_UPLOADED',
24+
DELETED = 'DELETED',
25+
RESTORED = 'RESTORED',
2426
}
2527

2628
export enum StellarStatus {

0 commit comments

Comments
 (0)