refactor(nestjs): import module

This commit is contained in:
Ahmed Bouhuolia
2025-04-12 08:38:29 +02:00
parent 1bcee9293c
commit 5bfff51093
18 changed files with 271 additions and 139 deletions

View File

@@ -0,0 +1,126 @@
import { Response, NextFunction } from 'express';
import { FileInterceptor } from '@nestjs/platform-express';
import { defaultTo } from 'lodash';
import {
Controller,
Post,
Get,
Body,
Param,
Query,
Res,
Next,
UseInterceptors,
UploadedFile,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { ImportResourceApplication } from './ImportResourceApplication';
import { uploadImportFileMulterOptions } from './ImportMulter.utils';
import { parseJsonSafe } from '@/utils/parse-json';
@Controller('import')
@ApiTags('import')
export class ImportController {
constructor(private readonly importResourceApp: ImportResourceApplication) {}
/**
* Imports xlsx/csv to the given resource type.
*/
@Post('/file')
@ApiOperation({ summary: 'Upload import file' })
@ApiResponse({ status: 200, description: 'File uploaded successfully' })
@UseInterceptors(
FileInterceptor('file', { storage: uploadImportFileMulterOptions }),
)
async fileUpload(
@Res() res: Response,
@Next() next: NextFunction,
@UploadedFile() file: Express.Multer.File,
@Body('resource') resource: string,
@Body('params') rawParams?: string,
) {
const params = defaultTo(parseJsonSafe(rawParams), {});
try {
const data = await this.importResourceApp.import(
resource,
file.filename,
params,
);
return res.status(200).send(data);
} catch (error) {
next(error);
}
}
/**
* Maps the columns of the imported file.
*/
@Post('/:import_id/mapping')
@ApiOperation({ summary: 'Map import columns' })
@ApiResponse({ status: 200, description: 'Mapping successful' })
async mapping(
@Res() res: Response,
@Param('import_id') importId: string,
@Body('mapping')
mapping: Array<{ group?: string; from: string; to: string }>,
) {
const result = await this.importResourceApp.mapping(importId, mapping);
return res.status(200).send(result);
}
/**
* Preview the imported file before actual importing.
*/
@Get('/:import_id/preview')
@ApiOperation({ summary: 'Preview import data' })
@ApiResponse({ status: 200, description: 'Preview data' })
async preview(@Res() res: Response, @Param('import_id') importId: string) {
const preview = await this.importResourceApp.preview(importId);
return res.status(200).send(preview);
}
/**
* Importing the imported file to the application storage.
*/
@Post('/:import_id/import')
@ApiOperation({ summary: 'Process import' })
@ApiResponse({ status: 200, description: 'Import processed successfully' })
async import(@Res() res: Response, @Param('import_id') importId: string) {
const result = await this.importResourceApp.process(importId);
return res.status(200).send(result);
}
/**
* Retrieves the csv/xlsx sample sheet of the given resource name.
*/
@Get('/sample')
@ApiOperation({ summary: 'Get import sample' })
@ApiResponse({ status: 200, description: 'Sample data' })
async downloadImportSample(
@Res() res: Response,
@Query('resource') resource: string,
@Query('format') format?: 'csv' | 'xlsx',
) {
const result = await this.importResourceApp.sample(resource, format);
return res.status(200).send(result);
}
/**
* Retrieves the import file meta.
*/
@Get('/:import_id')
@ApiOperation({ summary: 'Get import metadata' })
@ApiResponse({ status: 200, description: 'Import metadata' })
async getImportFileMeta(
@Res() res: Response,
@Param('import_id') importId: string,
) {
const result = await this.importResourceApp.importMeta(importId);
return res.status(200).send(result);
}
}

View File

@@ -12,17 +12,17 @@ import { ImportFileMapping } from './ImportFileMapping';
import { ImportFileDataValidator } from './ImportFileDataValidator';
import { ImportFileDataTransformer } from './ImportFileDataTransformer';
import { ImportFileCommon } from './ImportFileCommon';
import { ImportableResources } from './ImportableResources';
import { ResourceModule } from '../Resource/Resource.module';
import { TenancyModule } from '../Tenancy/Tenancy.module';
import { AccountsModule } from '../Accounts/Accounts.module';
import { ImportController } from './Import.controller';
import { ImportableRegistry } from './ImportableRegistry';
@Module({
imports: [ResourceModule, TenancyModule, AccountsModule],
providers: [
ImportAls,
ImportSampleService,
ImportableResources,
ImportResourceApplication,
ImportDeleteExpiredFiles,
ImportFileUploadService,
@@ -34,7 +34,9 @@ import { AccountsModule } from '../Accounts/Accounts.module';
ImportFileDataValidator,
ImportFileDataTransformer,
ImportFileCommon,
ImportableRegistry
],
controllers: [ImportController],
exports: [ImportAls],
})
export class ImportModule {}

View File

@@ -10,18 +10,18 @@ import {
ImportableContext,
} from './interfaces';
import { getUniqueImportableValue, trimObject } from './_utils';
import { ImportableResources } from './ImportableResources';
import { ResourceService } from '../Resource/ResourceService';
import { Injectable } from '@nestjs/common';
import { ServiceError } from '../Items/ServiceError';
import { ImportModelShape } from './models/Import';
import { ImportableRegistry } from './ImportableRegistry';
@Injectable()
export class ImportFileCommon {
constructor(
private readonly importFileValidator: ImportFileDataValidator,
private readonly importable: ImportableResources,
private readonly resource: ResourceService,
private readonly importableRegistry: ImportableRegistry,
) {}
/**
@@ -39,9 +39,9 @@ export class ImportFileCommon {
const resourceFields = this.resource.getResourceFields2(
importFile.resource,
);
const ImportableRegistry = this.importable.registry;
const importable = ImportableRegistry.getImportable(importFile.resource);
const importable = await this.importableRegistry.getImportable(
importFile.resource,
);
const concurrency = importable.concurrency || 10;
const success: ImportOperSuccess[] = [];
@@ -67,10 +67,7 @@ export class ImportFileCommon {
);
try {
// Run the importable function and listen to the errors.
const data = await importable.importable(
transformedDTO,
trx,
);
const data = await importable.importable(transformedDTO, trx);
success.push({ index, data });
} catch (err) {
if (err instanceof ServiceError) {
@@ -112,9 +109,8 @@ export class ImportFileCommon {
resourceName: string,
params: Record<string, any>,
) {
const ImportableRegistry = this.importable.registry;
const importable = ImportableRegistry.getImportable(resourceName);
const importable =
await this.importableRegistry.getImportable(resourceName);
const yupSchema = importable.paramsValidationSchema();
try {
@@ -137,8 +133,8 @@ export class ImportFileCommon {
resourceName: string,
params: Record<string, any>,
) {
const ImportableRegistry = this.importable.registry;
const importable = ImportableRegistry.getImportable(resourceName);
const importable =
await this.importableRegistry.getImportable(resourceName);
await importable.validateParams(params);
}
@@ -149,9 +145,12 @@ export class ImportFileCommon {
* @param {Record<string, any>} params
* @returns
*/
public transformParams(resourceName: string, params: Record<string, any>) {
const ImportableRegistry = this.importable.registry;
const importable = ImportableRegistry.getImportable(resourceName);
public async transformParams(
resourceName: string,
params: Record<string, any>,
) {
const importable =
await this.importableRegistry.getImportable(resourceName);
return importable.transformParams(params);
}

View File

@@ -1,6 +1,7 @@
import bluebird from 'bluebird';
import * as bluebird from 'bluebird';
import { isUndefined, pickBy, set } from 'lodash';
import { Knex } from 'knex';
import { Injectable } from '@nestjs/common';
import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
import {
valueParser,
@@ -12,7 +13,6 @@ import {
} from './_utils';
import { ResourceService } from '../Resource/ResourceService';
import { CurrencyParsingDTOs } from './_constants';
import { Injectable } from '@nestjs/common';
@Injectable()
export class ImportFileDataTransformer {

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common';
import { ImportInsertError, ResourceMetaFieldsMap } from './interfaces';
import { ERRORS, convertFieldsToYupValidation } from './_utils';
import { Injectable } from '@nestjs/common';
import { IModelMeta } from '@/interfaces/Model';
import { ServiceError } from '../Items/ServiceError';

View File

@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { ImportModel } from './models/Import';
import { ImportFileMetaTransformer } from './ImportFileMetaTransformer';
import { Inject, Injectable } from '@nestjs/common';
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
import { TenancyContext } from '../Tenancy/TenancyContext.service';

View File

@@ -0,0 +1,39 @@
import * as Multer from 'multer';
import * as path from 'path';
import { ServiceError } from '../Items/ServiceError';
export const getImportsStoragePath = () => {
return path.join(global.__static_dirname, `/imports`);
};
export function allowSheetExtensions(req, file, cb) {
if (
file.mimetype !== 'text/csv' &&
file.mimetype !== 'application/vnd.ms-excel' &&
file.mimetype !==
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
) {
cb(new ServiceError('IMPORTED_FILE_EXTENSION_INVALID'));
return;
}
cb(null, true);
}
const storage = Multer.diskStorage({
destination: function (req, file, cb) {
const path = getImportsStoragePath();
cb(null, path);
},
filename: function (req, file, cb) {
// Add the creation timestamp to clean up temp files later.
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(null, uniqueSuffix);
},
});
export const uploadImportFileMulterOptions = {
storage,
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: allowSheetExtensions,
};

View File

@@ -1,5 +1,5 @@
import * as moment from 'moment';
import bluebird from 'bluebird';
import * as bluebird from 'bluebird';
import { deleteImportFile } from './_utils';
import { Inject, Injectable } from '@nestjs/common';
import { ImportModel } from './models/Import';

View File

@@ -1,28 +1,28 @@
import XLSX from 'xlsx';
import { ImportableResources } from './ImportableResources';
import * as XLSX from 'xlsx';
import { sanitizeResourceName } from './_utils';
import { Injectable } from '@nestjs/common';
import { getImportableService } from './decorators/Import.decorator';
import { ImportableRegistry } from './ImportableRegistry';
@Injectable()
export class ImportSampleService {
constructor(
private readonly importable: ImportableResources,
) {}
private readonly importableRegistry: ImportableRegistry,
) {
}
/**
* Retrieves the sample sheet of the given resource.
* @param {string} resource
* @param {string} format
* @returns {Buffer | string}
*/
public sample(
public async sample(
resource: string,
format: 'csv' | 'xlsx'
): Buffer | string {
): Promise<Buffer | string> {
const _resource = sanitizeResourceName(resource);
const ImportableRegistry = this.importable.registry;
const importable = ImportableRegistry.getImportable(_resource);
const importable = await this.importableRegistry.getImportable(_resource);
const data = importable.sampleData();
@@ -30,6 +30,7 @@ export class ImportSampleService {
const worksheet = XLSX.utils.json_to_sheet(data);
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
// Determine the output format
if (format === 'csv') {
const csvOutput = XLSX.utils.sheet_to_csv(worksheet);

View File

@@ -1,43 +1,26 @@
import { camelCase, upperFirst } from 'lodash';
import { Importable } from './Importable';
import { getImportableService } from './decorators/Import.decorator';
import { Injectable } from '@nestjs/common';
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
@Injectable()
export class ImportableRegistry {
private static instance: ImportableRegistry;
private importables: Record<string, Importable>;
constructor() {
this.importables = {};
}
/**
* Gets singleton instance of registry.
* @returns {ImportableRegistry}
*/
public static getInstance(): ImportableRegistry {
if (!ImportableRegistry.instance) {
ImportableRegistry.instance = new ImportableRegistry();
}
return ImportableRegistry.instance;
}
/**
* Registers the given importable service.
* @param {string} resource
* @param {Importable} importable
*/
public registerImportable(resource: string, importable: Importable): void {
const _resource = this.sanitizeResourceName(resource);
this.importables[_resource] = importable;
}
constructor(private readonly moduleRef: ModuleRef) {}
/**
* Retrieves the importable service instance of the given resource name.
* @param {string} name
* @param {string} name
* @returns {Importable}
*/
public getImportable(name: string): Importable {
public async getImportable(name: string) {
const _name = this.sanitizeResourceName(name);
return this.importables[_name];
const importable = getImportableService(_name);
const contextId = ContextIdFactory.create();
const importableInstance = await this.moduleRef.resolve(importable, contextId, {
strict: false,
});
return importableInstance;
}
private sanitizeResourceName(resource: string) {

View File

@@ -1,73 +0,0 @@
// import { AccountsImportable } from '../Accounts/AccountsImportable';
import { Injectable } from '@nestjs/common';
import { ImportableRegistry } from './ImportableRegistry';
// import { UncategorizedTransactionsImportable } from '../BankingCategorize/commands/UncategorizedTransactionsImportable';
// import { CustomersImportable } from '../Contacts/Customers/CustomersImportable';
// import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable';
// import { ItemsImportable } from '../Items/ItemsImportable';
// import { ItemCategoriesImportable } from '../ItemCategories/ItemCategoriesImportable';
// import { ManualJournalImportable } from '../ManualJournals/commands/ManualJournalsImport';
// import { BillsImportable } from '../Purchases/Bills/BillsImportable';
// import { ExpensesImportable } from '../Expenses/ExpensesImportable';
// import { SaleInvoicesImportable } from '../Sales/Invoices/SaleInvoicesImportable';
// import { SaleEstimatesImportable } from '../Sales/Estimates/SaleEstimatesImportable';
// import { BillPaymentsImportable } from '../Purchases/BillPayments/BillPaymentsImportable';
// import { VendorCreditsImportable } from '../Purchases/VendorCredits/VendorCreditsImportable';
// import { PaymentsReceivedImportable } from '../Sales/PaymentReceived/PaymentsReceivedImportable';
// import { CreditNotesImportable } from '../CreditNotes/commands/CreditNotesImportable';
// import { SaleReceiptsImportable } from '../Sales/Receipts/SaleReceiptsImportable';
// import { TaxRatesImportable } from '../TaxRates/TaxRatesImportable';
@Injectable()
export class ImportableResources {
private static registry: ImportableRegistry;
constructor() {
this.boot();
}
/**
* Importable instances.
*/
private importables = [
// { resource: 'Account', importable: AccountsImportable },
// {
// resource: 'UncategorizedCashflowTransaction',
// importable: UncategorizedTransactionsImportable,
// },
// { resource: 'Customer', importable: CustomersImportable },
// { resource: 'Vendor', importable: VendorsImportable },
// { resource: 'Item', importable: ItemsImportable },
// { resource: 'ItemCategory', importable: ItemCategoriesImportable },
// { resource: 'ManualJournal', importable: ManualJournalImportable },
// { resource: 'Bill', importable: BillsImportable },
// { resource: 'Expense', importable: ExpensesImportable },
// { resource: 'SaleInvoice', importable: SaleInvoicesImportable },
// { resource: 'SaleEstimate', importable: SaleEstimatesImportable },
// { resource: 'BillPayment', importable: BillPaymentsImportable },
// { resource: 'PaymentReceive', importable: PaymentsReceivedImportable },
// { resource: 'VendorCredit', importable: VendorCreditsImportable },
// { resource: 'CreditNote', importable: CreditNotesImportable },
// { resource: 'SaleReceipt', importable: SaleReceiptsImportable },
// { resource: 'TaxRate', importable: TaxRatesImportable },
];
public get registry() {
return ImportableResources.registry;
}
/**
* Boots all the registered importables.
*/
public boot() {
if (!ImportableResources.registry) {
const instance = ImportableRegistry.getInstance();
this.importables.forEach((importable) => {
// const importableInstance = Container.get(importable.importable);
// instance.registerImportable(importable.resource, importableInstance);
});
ImportableResources.registry = instance;
}
}
}

View File

@@ -0,0 +1,37 @@
import { Global } from "@nestjs/common";
const importableModels = new Map<string, boolean>();
const importableService = new Map<string, any>()
/**
* Decorator that marks a model as exportable and registers its metadata.
* @param metadata Model metadata configuration for export
*/
export function ImportableModel() {
return function (target: any) {
const modelName = target.name;
importableModels.set(modelName, true);
};
}
export function ImportableService({ name }: { name: string }) {
return function (target: any) {
importableService.set(name, target);
// Apply the @Global() decorator to make the service globally available
Global()(target);
};
}
/**
* Gets the registered exportable model metadata
* @param modelName Name of the model class
*/
export function getImportableModelMeta(modelName: string): boolean | undefined {
return importableModels.get(modelName);
}
export function getImportableService(modelName: string) {
return importableService.get(modelName);
}