mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
feat: bulk transcations delete
This commit is contained in:
38
packages/server/src/modules/Items/BulkDeleteItems.service.ts
Normal file
38
packages/server/src/modules/Items/BulkDeleteItems.service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
import { castArray, uniq } from 'lodash';
|
||||
import { DeleteItemService } from './DeleteItem.service';
|
||||
|
||||
@Injectable()
|
||||
export class BulkDeleteItemsService {
|
||||
constructor(private readonly deleteItemService: DeleteItemService) { }
|
||||
|
||||
/**
|
||||
* Deletes multiple items.
|
||||
* @param {number | Array<number>} itemIds - The item id or ids.
|
||||
* @param {Knex.Transaction} trx - The transaction.
|
||||
*/
|
||||
async bulkDeleteItems(
|
||||
itemIds: number | Array<number>,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
const itemsIds = uniq(castArray(itemIds));
|
||||
|
||||
// Use PromisePool to delete items sequentially (concurrency: 1)
|
||||
// to avoid potential database locks and maintain transaction integrity
|
||||
const results = await PromisePool.withConcurrency(1)
|
||||
.for(itemsIds)
|
||||
.process(async (itemId: number) => {
|
||||
await this.deleteItemService.deleteItem(itemId, trx);
|
||||
});
|
||||
|
||||
// Check if there were any errors
|
||||
if (results.errors && results.errors.length > 0) {
|
||||
// If needed, you can throw an error here or handle errors individually
|
||||
// For now, we'll let individual errors bubble up
|
||||
throw results.errors[0].raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ import { ItemEstimatesResponseDto } from './dtos/ItemEstimatesResponse.dto';
|
||||
import { ItemBillsResponseDto } from './dtos/ItemBillsResponse.dto';
|
||||
import { ItemReceiptsResponseDto } from './dtos/ItemReceiptsResponse.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
import {
|
||||
BulkDeleteItemsDto,
|
||||
ValidateBulkDeleteItemsResponseDto,
|
||||
} from './dtos/BulkDeleteItems.dto';
|
||||
|
||||
@Controller('/items')
|
||||
@ApiTags('Items')
|
||||
@@ -39,6 +43,7 @@ import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
@ApiExtraModels(ItemBillsResponseDto)
|
||||
@ApiExtraModels(ItemEstimatesResponseDto)
|
||||
@ApiExtraModels(ItemReceiptsResponseDto)
|
||||
@ApiExtraModels(ValidateBulkDeleteItemsResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
export class ItemsController extends TenantController {
|
||||
constructor(private readonly itemsApplication: ItemsApplicationService) {
|
||||
@@ -340,4 +345,35 @@ export class ItemsController extends TenantController {
|
||||
const itemId = parseInt(id, 10);
|
||||
return this.itemsApplication.getItemReceiptsTransactions(itemId);
|
||||
}
|
||||
|
||||
@Post('validate-bulk-delete')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Validates which items can be deleted and returns counts of deletable and non-deletable items.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'Validation completed. Returns counts and IDs of deletable and non-deletable items.',
|
||||
schema: {
|
||||
$ref: getSchemaPath(ValidateBulkDeleteItemsResponseDto),
|
||||
},
|
||||
})
|
||||
async validateBulkDeleteItems(
|
||||
@Body() bulkDeleteDto: BulkDeleteItemsDto,
|
||||
): Promise<ValidateBulkDeleteItemsResponseDto> {
|
||||
return this.itemsApplication.validateBulkDeleteItems(bulkDeleteDto.ids);
|
||||
}
|
||||
|
||||
@Post('bulk-delete')
|
||||
@ApiOperation({ summary: 'Deletes multiple items in bulk.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The items have been successfully deleted.',
|
||||
})
|
||||
async bulkDeleteItems(
|
||||
@Body() bulkDeleteDto: BulkDeleteItemsDto,
|
||||
): Promise<void> {
|
||||
return this.itemsApplication.bulkDeleteItems(bulkDeleteDto.ids);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||
import { InventoryAdjustmentsModule } from '../InventoryAdjutments/InventoryAdjustments.module';
|
||||
import { ItemsExportable } from './ItemsExportable.service';
|
||||
import { ItemsImportable } from './ItemsImportable.service';
|
||||
import { BulkDeleteItemsService } from './BulkDeleteItems.service';
|
||||
import { ValidateBulkDeleteItemsService } from './ValidateBulkDeleteItems.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -41,8 +43,10 @@ import { ItemsImportable } from './ItemsImportable.service';
|
||||
TransformerInjectable,
|
||||
ItemsEntriesService,
|
||||
ItemsExportable,
|
||||
ItemsImportable
|
||||
ItemsImportable,
|
||||
BulkDeleteItemsService,
|
||||
ValidateBulkDeleteItemsService,
|
||||
],
|
||||
exports: [ItemsEntriesService, ItemsExportable, ItemsImportable],
|
||||
})
|
||||
export class ItemsModule {}
|
||||
export class ItemsModule { }
|
||||
|
||||
@@ -13,6 +13,8 @@ import { GetItemsService } from './GetItems.service';
|
||||
import { IItemsFilter } from './types/Items.types';
|
||||
import { EditItemDto, CreateItemDto } from './dtos/Item.dto';
|
||||
import { GetItemsQueryDto } from './dtos/GetItemsQuery.dto';
|
||||
import { BulkDeleteItemsService } from './BulkDeleteItems.service';
|
||||
import { ValidateBulkDeleteItemsService } from './ValidateBulkDeleteItems.service';
|
||||
|
||||
@Injectable()
|
||||
export class ItemsApplicationService {
|
||||
@@ -25,7 +27,9 @@ export class ItemsApplicationService {
|
||||
private readonly getItemService: GetItemService,
|
||||
private readonly getItemsService: GetItemsService,
|
||||
private readonly itemTransactionsService: ItemTransactionsService,
|
||||
) {}
|
||||
private readonly bulkDeleteItemsService: BulkDeleteItemsService,
|
||||
private readonly validateBulkDeleteItemsService: ValidateBulkDeleteItemsService,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Creates a new item.
|
||||
@@ -134,4 +138,27 @@ export class ItemsApplicationService {
|
||||
async getItemReceiptsTransactions(itemId: number): Promise<any> {
|
||||
return this.itemTransactionsService.getItemReceiptTransactions(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates which items can be deleted in bulk.
|
||||
* @param {number[]} itemIds - Array of item IDs to validate
|
||||
* @returns {Promise<{deletableCount: number, nonDeletableCount: number, deletableIds: number[], nonDeletableIds: number[]}>}
|
||||
*/
|
||||
async validateBulkDeleteItems(itemIds: number[]): Promise<{
|
||||
deletableCount: number;
|
||||
nonDeletableCount: number;
|
||||
deletableIds: number[];
|
||||
nonDeletableIds: number[];
|
||||
}> {
|
||||
return this.validateBulkDeleteItemsService.validateBulkDeleteItems(itemIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes multiple items in bulk.
|
||||
* @param {number[]} itemIds - Array of item IDs to delete
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async bulkDeleteItems(itemIds: number[]): Promise<void> {
|
||||
return this.bulkDeleteItemsService.bulkDeleteItems(itemIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||
import { DeleteItemService } from './DeleteItem.service';
|
||||
import { ModelHasRelationsError } from '@/common/exceptions/ModelHasRelations.exception';
|
||||
|
||||
@Injectable()
|
||||
export class ValidateBulkDeleteItemsService {
|
||||
constructor(
|
||||
private readonly deleteItemService: DeleteItemService,
|
||||
@Inject(TENANCY_DB_CONNECTION)
|
||||
private readonly tenantKnex: () => Knex,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Validates which items from the provided IDs can be deleted.
|
||||
* Uses the actual deleteItem service to validate, ensuring the same validation logic.
|
||||
* Uses a transaction that is always rolled back to ensure no database changes.
|
||||
* @param {number[]} itemIds - Array of item IDs to validate
|
||||
* @returns {Promise<{deletableCount: number, nonDeletableCount: number, deletableIds: number[], nonDeletableIds: number[]}>}
|
||||
*/
|
||||
public async validateBulkDeleteItems(itemIds: number[]): Promise<{
|
||||
deletableCount: number;
|
||||
nonDeletableCount: number;
|
||||
deletableIds: number[];
|
||||
nonDeletableIds: number[];
|
||||
}> {
|
||||
// Create a transaction that will be rolled back
|
||||
const trx = await this.tenantKnex().transaction({
|
||||
isolationLevel: 'read uncommitted',
|
||||
});
|
||||
|
||||
try {
|
||||
const deletableIds: number[] = [];
|
||||
const nonDeletableIds: number[] = [];
|
||||
|
||||
// Check each item to see if it can be deleted by attempting deletion in transaction
|
||||
for (const itemId of itemIds) {
|
||||
try {
|
||||
// Attempt to delete the item using the deleteItem service with the transaction
|
||||
// This will use the exact same validation logic as the actual delete
|
||||
await this.deleteItemService.deleteItem(itemId, trx);
|
||||
|
||||
// If deletion succeeds, item is deletable
|
||||
deletableIds.push(itemId);
|
||||
} catch (error) {
|
||||
// If error occurs, check the type of error
|
||||
if (error instanceof ModelHasRelationsError) {
|
||||
// Item has associated transactions/relations, cannot be deleted
|
||||
nonDeletableIds.push(itemId);
|
||||
} else {
|
||||
// Other errors (e.g., item not found), also mark as non-deletable
|
||||
nonDeletableIds.push(itemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always rollback the transaction to ensure no changes are persisted
|
||||
await trx.rollback();
|
||||
|
||||
return {
|
||||
deletableCount: deletableIds.length,
|
||||
nonDeletableCount: nonDeletableIds.length,
|
||||
deletableIds,
|
||||
nonDeletableIds,
|
||||
};
|
||||
} catch (error) {
|
||||
// Rollback in case of any error
|
||||
await trx.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { IsArray, IsInt, ArrayNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class BulkDeleteItemsDto {
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@IsInt({ each: true })
|
||||
@ApiProperty({
|
||||
description: 'Array of item IDs to delete',
|
||||
type: [Number],
|
||||
example: [1, 2, 3],
|
||||
})
|
||||
ids: number[];
|
||||
}
|
||||
|
||||
export class ValidateBulkDeleteItemsResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'Number of items that can be deleted',
|
||||
example: 2,
|
||||
})
|
||||
deletableCount: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Number of items that cannot be deleted',
|
||||
example: 1,
|
||||
})
|
||||
nonDeletableCount: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'IDs of items that can be deleted',
|
||||
type: [Number],
|
||||
example: [1, 2],
|
||||
})
|
||||
deletableIds: number[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'IDs of items that cannot be deleted',
|
||||
type: [Number],
|
||||
example: [3],
|
||||
})
|
||||
nonDeletableIds: number[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user