feat: wip import resources

This commit is contained in:
Ahmed Bouhuolia
2024-03-15 00:18:41 +02:00
parent 084d9d3d10
commit ab4c0ab7a7
14 changed files with 96 additions and 125 deletions

View File

@@ -37,8 +37,6 @@
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"country-codes-list": "^1.6.8", "country-codes-list": "^1.6.8",
"cpy": "^8.1.2", "cpy": "^8.1.2",
@@ -56,7 +54,6 @@
"express": "^4.17.1", "express": "^4.17.1",
"express-basic-auth": "^1.2.0", "express-basic-auth": "^1.2.0",
"express-boom": "^3.0.0", "express-boom": "^3.0.0",
"express-fileupload": "^1.1.7-alpha.3",
"express-oauth-server": "^2.0.0", "express-oauth-server": "^2.0.0",
"express-validator": "^6.12.2", "express-validator": "^6.12.2",
"form-data": "^4.0.0", "form-data": "^4.0.0",

View File

@@ -1,15 +1,10 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import Multer from 'multer';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { ImportResourceApplication } from '@/services/Import/ImportResourceApplication'; import { ImportResourceApplication } from '@/services/Import/ImportResourceApplication';
import { uploadImportFile } from './_utils';
const upload = Multer({
dest: './public/imports',
limits: { fileSize: 5 * 1024 * 1024 },
});
@Service() @Service()
export class ImportController extends BaseController { export class ImportController extends BaseController {
@@ -24,7 +19,7 @@ export class ImportController extends BaseController {
router.post( router.post(
'/file', '/file',
upload.single('file'), uploadImportFile.single('file'),
this.importValidationSchema, this.importValidationSchema,
this.validationResult, this.validationResult,
this.asyncMiddleware(this.fileUpload.bind(this)), this.asyncMiddleware(this.fileUpload.bind(this)),
@@ -60,27 +55,11 @@ export class ImportController extends BaseController {
* @returns {ValidationSchema[]} * @returns {ValidationSchema[]}
*/ */
private get importValidationSchema() { private get importValidationSchema() {
return [ return [body('resource').exists()];
body('resource').exists(),
// body('file').custom((value, { req }) => {
// if (!value) {
// throw new Error('File is required');
// }
// if (!['xlsx', 'csv'].includes(value.split('.').pop())) {
// throw new Error('File must be in xlsx or csv format');
// }
// return true;
// }),
];
} }
/** /**
* Imports xlsx/csv to the given resource type. * Imports xlsx/csv to the given resource type.
*
* - Save the xlsx/csv file and give it a random name.
* - Save the file metadata on the DB storage.
* -
*
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
* @param {NextFunction} next - * @param {NextFunction} next -
@@ -117,7 +96,6 @@ export class ImportController extends BaseController {
importId, importId,
body?.mapping body?.mapping
); );
return res.status(200).send(mapping); return res.status(200).send(mapping);
} catch (error) { } catch (error) {
next(error); next(error);
@@ -144,22 +122,19 @@ export class ImportController extends BaseController {
} }
/** /**
* * Importing the imported file to the application storage.
* @param req * @param {Request} req
* @param res * @param {Response} res
* @param next * @param {NextFunction} next
*/ */
private async import(req: Request, res: Response, next: NextFunction) { private async import(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const { import_id: importId } = req.params; const { import_id: importId } = req.params;
try { try {
await this.importResourceApp.process(tenantId, importId); const result = await this.importResourceApp.process(tenantId, importId);
return res.status(200).send({ return res.status(200).send(result);
id: importId,
message: 'Importing the uploaded file is importing.',
});
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -194,6 +169,11 @@ export class ImportController extends BaseController {
errors: [{ type: 'DUPLICATED_TO_MAP_ATTR' }], errors: [{ type: 'DUPLICATED_TO_MAP_ATTR' }],
}); });
} }
if (error.errorType === 'IMPORTED_FILE_EXTENSION_INVALID') {
return res.status(400).send({
errors: [{ type: 'IMPORTED_FILE_EXTENSION_INVALID' }],
});
}
} }
next(error); next(error);
} }

View File

@@ -0,0 +1,20 @@
import Multer from 'multer';
import { ServiceError } from '@/exceptions';
export function allowSheetExtensions(req, file, cb) {
if (
file.mimetype !== 'text/csv' &&
file.mimetype !== 'application/vnd.ms-excel'
) {
cb(new ServiceError('IMPORTED_FILE_EXTENSION_INVALID'));
return;
}
cb(null, true);
}
export const uploadImportFile = Multer({
dest: './public/imports',
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: allowSheetExtensions,
});

View File

@@ -4,7 +4,6 @@ import helmet from 'helmet';
import boom from 'express-boom'; import boom from 'express-boom';
import errorHandler from 'errorhandler'; import errorHandler from 'errorhandler';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import fileUpload from 'express-fileupload';
import { Server } from 'socket.io'; import { Server } from 'socket.io';
import Container from 'typedi'; import Container from 'typedi';
import routes from 'api'; import routes from 'api';
@@ -47,13 +46,6 @@ export default ({ app }) => {
app.use('/public', express.static(path.join(global.__storage_dir))); app.use('/public', express.static(path.join(global.__storage_dir)));
// Handle multi-media requests.
// app.use(
// fileUpload({
// createParentPath: true,
// })
// );
// Logger middleware. // Logger middleware.
app.use(LoggerMiddleware); app.use(LoggerMiddleware);

View File

@@ -1,15 +1,16 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { IAccountCreateDTO } from '@/interfaces'; import { IAccountCreateDTO } from '@/interfaces';
import { CreateAccount } from '../Accounts/CreateAccount'; import { CreateAccount } from './CreateAccount';
import { Importable } from '../Import/Importable';
@Service() @Service()
export class AccountsImportable { export class AccountsImportable extends Importable {
@Inject() @Inject()
private createAccountService: CreateAccount; private createAccountService: CreateAccount;
/** /**
* * Importing to account service.
* @param {number} tenantId * @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO * @param {IAccountCreateDTO} createAccountDTO
* @returns * @returns
@@ -27,20 +28,10 @@ export class AccountsImportable {
} }
/** /**
* * Concurrrency controlling of the importing process.
* @param data * @returns {number}
* @returns
*/ */
public transform(data) { public get concurrency() {
return { ...data }; return 1;
}
/**
*
* @param data
* @returns
*/
public preTransform(data) {
return { ...data };
} }
} }

View File

@@ -1,33 +0,0 @@
import { DATATYPES_LENGTH } from '@/data/DataTypes';
import { IsInt, IsOptional, IsString, Length, Min, Max, IsNotEmpty } from 'class-validator';
export class AccountDTOSchema {
@IsString()
@Length(3, DATATYPES_LENGTH.STRING)
@IsNotEmpty()
name: string;
@IsString()
@IsOptional()
@Length(3, 6)
code?: string;
@IsOptional()
currencyCode?: string;
@IsString()
@Length(3, DATATYPES_LENGTH.STRING)
@IsNotEmpty()
accountType: string;
@IsString()
@IsOptional()
@Length(0, DATATYPES_LENGTH.TEXT)
description?: string;
@IsInt()
@IsOptional()
@Min(0)
@Max(DATATYPES_LENGTH.INT_10)
parentAccountId?: number;
}

View File

@@ -11,7 +11,7 @@ import {
ImportOperError, ImportOperError,
ImportOperSuccess, ImportOperSuccess,
} from './interfaces'; } from './interfaces';
import { AccountsImportable } from './AccountsImportable'; import { AccountsImportable } from '../Accounts/AccountsImportable';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { trimObject } from './_utils'; import { trimObject } from './_utils';
import { ImportableResources } from './ImportableResources'; import { ImportableResources } from './ImportableResources';
@@ -57,12 +57,12 @@ export class ImportFileCommon {
} }
/** /**
* * Imports the given parsed data to the resource storage through registered importable service.
* @param {number} tenantId - * @param {number} tenantId -
* @param {string} resourceName - Resource name. * @param {string} resourceName - Resource name.
* @param {Record<string, any>} parsedData - * @param {Record<string, any>} parsedData - Parsed data.
* @param {Knex.Transaction} trx * @param {Knex.Transaction} trx - Knex transaction.
* @returns * @returns {Promise<[ImportOperSuccess[], ImportOperError[]]>}
*/ */
public async import( public async import(
tenantId: number, tenantId: number,
@@ -77,6 +77,8 @@ export class ImportFileCommon {
const ImportableRegistry = this.importable.registry; const ImportableRegistry = this.importable.registry;
const importable = ImportableRegistry.getImportable(resourceName); const importable = ImportableRegistry.getImportable(resourceName);
const concurrency = importable.concurrency || 10;
const success: ImportOperSuccess[] = []; const success: ImportOperSuccess[] = [];
const failed: ImportOperError[] = []; const failed: ImportOperError[] = [];
@@ -108,7 +110,7 @@ export class ImportFileCommon {
failed.push({ index, error }); failed.push({ index, error });
} }
}; };
await bluebird.map(parsedData, importAsync, { concurrency: 2 }); await bluebird.map(parsedData, importAsync, { concurrency });
return [success, failed]; return [success, failed];
} }
@@ -127,7 +129,7 @@ export class ImportFileCommon {
* @param {number} tenantId * @param {number} tenantId
* @param {} importFile * @param {} importFile
*/ */
private async deleteImportFile(tenantId: number, importFile: any) { public async deleteImportFile(tenantId: number, importFile: any) {
const { Import } = this.tenancy.models(tenantId); const { Import } = this.tenancy.models(tenantId);
// Deletes the import row. // Deletes the import row.

View File

@@ -1,10 +1,9 @@
import { Inject, Service } from 'typedi'; import { Service } from 'typedi';
import * as R from 'ramda'; import * as R from 'ramda';
import { isUndefined, mapValues, get, pickBy, chain } from 'lodash'; import { isUndefined, get, chain } from 'lodash';
import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces'; import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
import { parseBoolean } from '@/utils'; import { parseBoolean } from '@/utils';
import { trimObject } from './_utils'; import { trimObject } from './_utils';
import ResourceService from '../Resource/ResourceService';
@Service() @Service()
export class ImportFileDataTransformer { export class ImportFileDataTransformer {

View File

@@ -27,7 +27,7 @@ export class ImportFileUploadService {
* @param {string} resource - Resource name. * @param {string} resource - Resource name.
* @param {string} filePath - File path. * @param {string} filePath - File path.
* @param {string} fileName - File name. * @param {string} fileName - File name.
* @returns * @returns {Promise<ImportFileUploadPOJO>}
*/ */
public async import( public async import(
tenantId: number, tenantId: number,

View File

@@ -52,7 +52,7 @@ export class ImportResourceApplication {
* Preview the mapped results before process importing. * Preview the mapped results before process importing.
* @param {number} tenantId * @param {number} tenantId
* @param {number} importId * @param {number} importId
* @returns {} * @returns {Promise<ImportFilePreviewPOJO>}
*/ */
public async preview(tenantId: number, importId: number) { public async preview(tenantId: number, importId: number) {
return this.ImportFilePreviewService.preview(tenantId, importId); return this.ImportFilePreviewService.preview(tenantId, importId);
@@ -62,7 +62,7 @@ export class ImportResourceApplication {
* Process the import file sheet through service for creating entities. * Process the import file sheet through service for creating entities.
* @param {number} tenantId * @param {number} tenantId
* @param {number} importId * @param {number} importId
* @returns {Promise<void>} * @returns {Promise<ImportFilePreviewPOJO>}
*/ */
public async process(tenantId: number, importId: number) { public async process(tenantId: number, importId: number) {
return this.importProcessService.import(tenantId, importId); return this.importProcessService.import(tenantId, importId);

View File

@@ -1,7 +0,0 @@
import { Service } from "typedi";
@Service()
export class ImportResourceRegistry {
}

View File

@@ -1,7 +1,23 @@
import { Knex } from 'knex';
export abstract class Importable {
/**
*
* @param {number} tenantId
* @param {any} createDTO
* @param {Knex.Transaction} trx
*/
public importable(tenantId: number, createDTO: any, trx?: Knex.Transaction) {
throw new Error(
'The `importable` function is not defined in service importable.'
);
}
abstract class importable { /**
* Concurrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 10;
}
} }

View File

@@ -1,13 +1,18 @@
import { camelCase, upperFirst } from 'lodash'; import { camelCase, upperFirst } from 'lodash';
import { Importable } from './Importable';
export class ImportableRegistry { export class ImportableRegistry {
private static instance: ImportableRegistry; private static instance: ImportableRegistry;
private importables: Record<string, any>; private importables: Record<string, Importable>;
private constructor() { private constructor() {
this.importables = {}; this.importables = {};
} }
/**
* Gets singleton instance of registry.
* @returns {ImportableRegistry}
*/
public static getInstance(): ImportableRegistry { public static getInstance(): ImportableRegistry {
if (!ImportableRegistry.instance) { if (!ImportableRegistry.instance) {
ImportableRegistry.instance = new ImportableRegistry(); ImportableRegistry.instance = new ImportableRegistry();
@@ -15,12 +20,22 @@ export class ImportableRegistry {
return ImportableRegistry.instance; return ImportableRegistry.instance;
} }
public registerImportable(resource: string, importable: any): void { /**
* Registers the given importable service.
* @param {string} resource
* @param {Importable} importable
*/
public registerImportable(resource: string, importable: Importable): void {
const _resource = this.sanitizeResourceName(resource); const _resource = this.sanitizeResourceName(resource);
this.importables[_resource] = importable; this.importables[_resource] = importable;
} }
public getImportable(name: string): any { /**
* Retrieves the importable service instance of the given resource name.
* @param {string} name
* @returns {Importable}
*/
public getImportable(name: string): Importable {
const _name = this.sanitizeResourceName(name); const _name = this.sanitizeResourceName(name);
return this.importables[_name]; return this.importables[_name];
} }

View File

@@ -1,12 +1,11 @@
import Container, { Service } from 'typedi'; import Container, { Service } from 'typedi';
import { AccountsImportable } from './AccountsImportable'; import { AccountsImportable } from '../Accounts/AccountsImportable';
import { ImportableRegistry } from './ImportableRegistry'; import { ImportableRegistry } from './ImportableRegistry';
@Service() @Service()
export class ImportableResources { export class ImportableResources {
private static registry: ImportableRegistry; private static registry: ImportableRegistry;
constructor() { constructor() {
this.boot(); this.boot();
} }