mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 06:10:31 +00:00
feat: import resource
This commit is contained in:
@@ -62,16 +62,16 @@ export class ImportController extends BaseController {
|
|||||||
private get importValidationSchema() {
|
private get importValidationSchema() {
|
||||||
return [
|
return [
|
||||||
body('resource').exists(),
|
body('resource').exists(),
|
||||||
// body('file').custom((value, { req }) => {
|
// body('file').custom((value, { req }) => {
|
||||||
// if (!value) {
|
// if (!value) {
|
||||||
// throw new Error('File is required');
|
// throw new Error('File is required');
|
||||||
// }
|
// }
|
||||||
// if (!['xlsx', 'csv'].includes(value.split('.').pop())) {
|
// if (!['xlsx', 'csv'].includes(value.split('.').pop())) {
|
||||||
// throw new Error('File must be in xlsx or csv format');
|
// throw new Error('File must be in xlsx or csv format');
|
||||||
// }
|
// }
|
||||||
// return true;
|
// return true;
|
||||||
// }),
|
// }),
|
||||||
// ];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,7 +92,6 @@ export class ImportController extends BaseController {
|
|||||||
const data = await this.importResourceApp.import(
|
const data = await this.importResourceApp.import(
|
||||||
tenantId,
|
tenantId,
|
||||||
req.body.resource,
|
req.body.resource,
|
||||||
req.file.path,
|
|
||||||
req.file.filename
|
req.file.filename
|
||||||
);
|
);
|
||||||
return res.status(200).send(data);
|
return res.status(200).send(data);
|
||||||
@@ -107,18 +106,19 @@ export class ImportController extends BaseController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
private async mapping(req: Request, res: Response, next: NextFunction) {
|
private async mapping(req: Request, res: Response, next: NextFunction) {
|
||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
const { import_id: importId } = req.params;
|
const { import_id: importId } = req.params;
|
||||||
const body = this.matchedBodyData(req);
|
const body = this.matchedBodyData(req);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.importResourceApp.mapping(tenantId, importId, body?.mapping);
|
const mapping = await this.importResourceApp.mapping(
|
||||||
|
tenantId,
|
||||||
|
importId,
|
||||||
|
body?.mapping
|
||||||
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send(mapping);
|
||||||
id: importId,
|
|
||||||
message: 'The given import sheet has mapped successfully.'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,7 @@ export class ImportController extends BaseController {
|
|||||||
const { import_id: importId } = req.params;
|
const { import_id: importId } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const preview = await this.importResourceApp.preview(tenantId, importId);
|
const preview = await this.importResourceApp.preview(tenantId, importId);
|
||||||
|
|
||||||
return res.status(200).send(preview);
|
return res.status(200).send(preview);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -158,7 +158,7 @@ export class ImportController extends BaseController {
|
|||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
id: importId,
|
id: importId,
|
||||||
message: 'Importing the uploaded file is importing.'
|
message: 'Importing the uploaded file is importing.',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -181,18 +181,18 @@ export class ImportController extends BaseController {
|
|||||||
if (error instanceof ServiceError) {
|
if (error instanceof ServiceError) {
|
||||||
if (error.errorType === 'INVALID_MAP_ATTRS') {
|
if (error.errorType === 'INVALID_MAP_ATTRS') {
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
errors: [{ type: 'INVALID_MAP_ATTRS' }]
|
errors: [{ type: 'INVALID_MAP_ATTRS' }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (error.errorType === 'DUPLICATED_FROM_MAP_ATTR') {
|
if (error.errorType === 'DUPLICATED_FROM_MAP_ATTR') {
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
errors: [{ type: 'DUPLICATED_FROM_MAP_ATTR' }],
|
errors: [{ type: 'DUPLICATED_FROM_MAP_ATTR' }],
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
if (error.errorType === 'DUPLICATED_TO_MAP_ATTR') {
|
if (error.errorType === 'DUPLICATED_TO_MAP_ATTR') {
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
errors: [{ type: 'DUPLICATED_TO_MAP_ATTR' }],
|
errors: [{ type: 'DUPLICATED_TO_MAP_ATTR' }],
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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 { AccountsApplication } from '../Accounts/AccountsApplication';
|
|
||||||
import { CreateAccount } from '../Accounts/CreateAccount';
|
import { CreateAccount } from '../Accounts/CreateAccount';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
|
|||||||
@@ -1,20 +1,36 @@
|
|||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import XLSX from 'xlsx';
|
import XLSX from 'xlsx';
|
||||||
import bluebird from 'bluebird';
|
import bluebird from 'bluebird';
|
||||||
|
import * as R from 'ramda';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { first } from 'lodash';
|
||||||
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { ImportInsertError } from './interfaces';
|
import {
|
||||||
|
ImportInsertError,
|
||||||
|
ImportOperError,
|
||||||
|
ImportOperSuccess,
|
||||||
|
} from './interfaces';
|
||||||
import { AccountsImportable } from './AccountsImportable';
|
import { AccountsImportable } from './AccountsImportable';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { trimObject } from './_utils';
|
||||||
|
import { ImportableResources } from './ImportableResources';
|
||||||
|
import ResourceService from '../Resource/ResourceService';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileCommon {
|
export class ImportFileCommon {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private importFileValidator: ImportFileDataValidator;
|
private importFileValidator: ImportFileDataValidator;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private importable: AccountsImportable;
|
private importable: ImportableResources;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private resource: ResourceService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps the columns of the imported data based on the provided mapping attributes.
|
* Maps the columns of the imported data based on the provided mapping attributes.
|
||||||
@@ -22,7 +38,7 @@ export class ImportFileCommon {
|
|||||||
* @param {ImportMappingAttr[]} map - The mapping attributes.
|
* @param {ImportMappingAttr[]} map - The mapping attributes.
|
||||||
* @returns {Record<string, any>[]} - The mapped data objects.
|
* @returns {Record<string, any>[]} - The mapped data objects.
|
||||||
*/
|
*/
|
||||||
public parseXlsxSheet(buffer) {
|
public parseXlsxSheet(buffer: Buffer): Record<string, unknown>[] {
|
||||||
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
||||||
|
|
||||||
const firstSheetName = workbook.SheetNames[0];
|
const firstSheetName = workbook.SheetNames[0];
|
||||||
@@ -43,45 +59,81 @@ export class ImportFileCommon {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {Record<string, any>} importableFields
|
* @param {string} resourceName - Resource name.
|
||||||
* @param {Record<string, any>} parsedData
|
* @param {Record<string, any>} parsedData -
|
||||||
* @param {Knex.Transaction} trx
|
* @param {Knex.Transaction} trx
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public import(
|
public async import(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
importableFields,
|
resourceName: string,
|
||||||
parsedData: Record<string, any>,
|
parsedData: Record<string, any>[],
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
): Promise<(void | ImportInsertError[])[]> {
|
): Promise<[ImportOperSuccess[], ImportOperError[]]> {
|
||||||
return bluebird.map(
|
const importableFields = this.resource.getResourceImportableFields(
|
||||||
parsedData,
|
tenantId,
|
||||||
async (objectDTO, index: number): Promise<true | ImportInsertError[]> => {
|
resourceName
|
||||||
try {
|
|
||||||
// Validate the DTO object before passing it to the service layer.
|
|
||||||
await this.importFileValidator.validateData(
|
|
||||||
importableFields,
|
|
||||||
objectDTO
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
// Run the importable function and listen to the errors.
|
|
||||||
await this.importable.importable(tenantId, objectDTO, trx);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ServiceError) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
errorCode: 'ValidationError',
|
|
||||||
errorMessage: error.message || error.errorType,
|
|
||||||
rowNumber: index + 1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (errors) {
|
|
||||||
return errors.map((er) => ({ ...er, rowNumber: index + 1 }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ concurrency: 2 }
|
|
||||||
);
|
);
|
||||||
|
const ImportableRegistry = this.importable.registry;
|
||||||
|
const importable = ImportableRegistry.getImportable(resourceName);
|
||||||
|
|
||||||
|
const success: ImportOperSuccess[] = [];
|
||||||
|
const failed: ImportOperError[] = [];
|
||||||
|
|
||||||
|
const importAsync = async (objectDTO, index: number): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// Validate the DTO object before passing it to the service layer.
|
||||||
|
await this.importFileValidator.validateData(
|
||||||
|
importableFields,
|
||||||
|
objectDTO
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
// Run the importable function and listen to the errors.
|
||||||
|
const data = await importable.importable(tenantId, objectDTO, trx);
|
||||||
|
success.push({ index, data });
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ServiceError) {
|
||||||
|
const error = [
|
||||||
|
{
|
||||||
|
errorCode: 'ValidationError',
|
||||||
|
errorMessage: err.message || err.errorType,
|
||||||
|
rowNumber: index + 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
failed.push({ index, error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (errors) {
|
||||||
|
const error = errors.map((er) => ({ ...er, rowNumber: index + 1 }));
|
||||||
|
failed.push({ index, error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await bluebird.map(parsedData, importAsync, { concurrency: 2 });
|
||||||
|
|
||||||
|
return [success, failed];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the sheet columns from the given sheet data.
|
||||||
|
* @param {unknown[]} json
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public parseSheetColumns(json: unknown[]): string[] {
|
||||||
|
return R.compose(Object.keys, trimObject, first)(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the imported file from the storage and database.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {} importFile
|
||||||
|
*/
|
||||||
|
private async deleteImportFile(tenantId: number, importFile: any) {
|
||||||
|
const { Import } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// Deletes the import row.
|
||||||
|
await Import.query().findById(importFile.id).delete();
|
||||||
|
|
||||||
|
// Deletes the imported file.
|
||||||
|
await fs.unlink(`public/imports/${importFile.filename}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,12 @@ import ResourceService from '../Resource/ResourceService';
|
|||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileDataTransformer {
|
export class ImportFileDataTransformer {
|
||||||
@Inject()
|
|
||||||
private resource: ResourceService;
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {}
|
* @param {}
|
||||||
*/
|
*/
|
||||||
public transformSheetData(
|
public parseSheetData(
|
||||||
importFile: any,
|
importFile: any,
|
||||||
importableFields: any,
|
importableFields: any,
|
||||||
data: Record<string, unknown>[]
|
data: Record<string, unknown>[]
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import { ImportInsertError, ResourceMetaFieldsMap } from './interfaces';
|
import { ImportInsertError, ResourceMetaFieldsMap } from './interfaces';
|
||||||
import { convertFieldsToYupValidation } from './_utils';
|
import { ERRORS, convertFieldsToYupValidation } from './_utils';
|
||||||
|
import { IModelMeta } from '@/interfaces';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileDataValidator {
|
export class ImportFileDataValidator {
|
||||||
|
/**
|
||||||
|
* Validates the given resource is importable.
|
||||||
|
* @param {IModelMeta} resourceMeta
|
||||||
|
*/
|
||||||
|
public validateResourceImportable(resourceMeta: IModelMeta) {
|
||||||
|
// Throw service error if the resource does not support importing.
|
||||||
|
if (!resourceMeta.importable) {
|
||||||
|
throw new ServiceError(ERRORS.RESOURCE_NOT_IMPORTABLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the given mapped DTOs and returns errors with their index.
|
* Validates the given mapped DTOs and returns errors with their index.
|
||||||
* @param {Record<string, any>} mappedDTOs
|
* @param {Record<string, any>} mappedDTOs
|
||||||
* @returns {Promise<ImportValidationError[][]>}
|
* @returns {Promise<void | ImportInsertError[]>}
|
||||||
*/
|
*/
|
||||||
public async validateData(
|
public async validateData(
|
||||||
importableFields: ResourceMetaFieldsMap,
|
importableFields: ResourceMetaFieldsMap,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import { fromPairs } from 'lodash';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
import { ImportMappingAttr } from './interfaces';
|
import { ImportFileMapPOJO, ImportMappingAttr } from './interfaces';
|
||||||
import ResourceService from '../Resource/ResourceService';
|
import ResourceService from '../Resource/ResourceService';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { ERRORS } from './_utils';
|
import { ERRORS } from './_utils';
|
||||||
import { fromPairs } from 'lodash';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileMapping {
|
export class ImportFileMapping {
|
||||||
@@ -24,7 +24,7 @@ export class ImportFileMapping {
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
importId: number,
|
importId: number,
|
||||||
maps: ImportMappingAttr[]
|
maps: ImportMappingAttr[]
|
||||||
) {
|
): Promise<ImportFileMapPOJO> {
|
||||||
const { Import } = this.tenancy.models(tenantId);
|
const { Import } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const importFile = await Import.query()
|
const importFile = await Import.query()
|
||||||
@@ -42,6 +42,13 @@ export class ImportFileMapping {
|
|||||||
await Import.query().findById(importFile.id).patch({
|
await Import.query().findById(importFile.id).patch({
|
||||||
mapping: mappingStringified,
|
mapping: mappingStringified,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
import: {
|
||||||
|
importId: importFile.importId,
|
||||||
|
resource: importFile.resource,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { first, omit } from 'lodash';
|
|
||||||
import { ServiceError } from '@/exceptions';
|
|
||||||
import { ERRORS, getUnmappedSheetColumns } from './_utils';
|
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
import { ImportFileCommon } from './ImportFileCommon';
|
import { ImportFilePreviewPOJO } from './interfaces';
|
||||||
import { ImportFileDataTransformer } from './ImportFileDataTransformer';
|
import { ImportFileProcess } from './ImportFileProcess';
|
||||||
import ResourceService from '../Resource/ResourceService';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFilePreview {
|
export class ImportFilePreview {
|
||||||
@@ -13,86 +9,26 @@ export class ImportFilePreview {
|
|||||||
private tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private resource: ResourceService;
|
private importFile: ImportFileProcess;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private importFileCommon: ImportFileCommon;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private importFileParser: ImportFileDataTransformer;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Preview the imported file results before commiting the transactions.
|
||||||
* - Returns the passed rows and will be in inserted.
|
|
||||||
* - Returns the passed rows will be overwritten.
|
|
||||||
* - Returns the rows errors from the validation.
|
|
||||||
* - Returns the unmapped fields.
|
|
||||||
*
|
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number} importId
|
* @param {number} importId
|
||||||
|
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||||
*/
|
*/
|
||||||
public async preview(tenantId: number, importId: number) {
|
public async preview(
|
||||||
const { Import } = this.tenancy.models(tenantId);
|
tenantId: number,
|
||||||
|
importId: number
|
||||||
const importFile = await Import.query()
|
): Promise<ImportFilePreviewPOJO> {
|
||||||
.findOne('importId', importId)
|
|
||||||
.throwIfNotFound();
|
|
||||||
|
|
||||||
// Throw error if the import file is not mapped yet.
|
|
||||||
if (!importFile.isMapped) {
|
|
||||||
throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED);
|
|
||||||
}
|
|
||||||
const buffer = await this.importFileCommon.readImportFile(
|
|
||||||
importFile.filename
|
|
||||||
);
|
|
||||||
const jsonData = this.importFileCommon.parseXlsxSheet(buffer);
|
|
||||||
|
|
||||||
const importableFields = this.resource.getResourceImportableFields(
|
|
||||||
tenantId,
|
|
||||||
importFile.resource
|
|
||||||
);
|
|
||||||
// Prases the sheet json data.
|
|
||||||
const parsedData = this.importFileParser.transformSheetData(
|
|
||||||
importFile,
|
|
||||||
importableFields,
|
|
||||||
jsonData
|
|
||||||
);
|
|
||||||
const knex = this.tenancy.knex(tenantId);
|
const knex = this.tenancy.knex(tenantId);
|
||||||
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });
|
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });
|
||||||
|
|
||||||
// Runs the importing operation with ability to return errors that will happen.
|
const meta = await this.importFile.import(tenantId, importId, trx);
|
||||||
const asyncOpers = await this.importFileCommon.import(
|
|
||||||
tenantId,
|
|
||||||
importableFields,
|
|
||||||
parsedData,
|
|
||||||
trx
|
|
||||||
);
|
|
||||||
// Filter out the operations that have successed.
|
|
||||||
const successAsyncOpers = asyncOpers.filter((oper) => !oper);
|
|
||||||
const errors = asyncOpers.filter((oper) => oper);
|
|
||||||
|
|
||||||
// Rollback all the successed transactions.
|
// Rollback the successed transaction.
|
||||||
await trx.rollback();
|
await trx.rollback();
|
||||||
|
|
||||||
const header = Object.keys(first(jsonData));
|
return meta;
|
||||||
const mapping = importFile.mappingParsed;
|
|
||||||
|
|
||||||
const unmappedColumns = getUnmappedSheetColumns(header, mapping);
|
|
||||||
const totalCount = parsedData.length;
|
|
||||||
|
|
||||||
const createdCount = successAsyncOpers.length;
|
|
||||||
const errorsCount = errors.length;
|
|
||||||
const skippedCount = errorsCount;
|
|
||||||
|
|
||||||
return {
|
|
||||||
createdCount,
|
|
||||||
skippedCount,
|
|
||||||
totalCount,
|
|
||||||
errorsCount,
|
|
||||||
errors,
|
|
||||||
unmappedColumns: unmappedColumns,
|
|
||||||
unmappedColumnsCount: unmappedColumns.length,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import * as R from 'ramda';
|
import { chain } from 'lodash';
|
||||||
import XLSX from 'xlsx';
|
|
||||||
import { first, isUndefined } from 'lodash';
|
|
||||||
import bluebird from 'bluebird';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
|
||||||
import { ERRORS, convertFieldsToYupValidation, trimObject } from './_utils';
|
|
||||||
import { ImportMappingAttr, ImportValidationError } from './interfaces';
|
|
||||||
import { AccountsImportable } from './AccountsImportable';
|
|
||||||
import UnitOfWork from '../UnitOfWork';
|
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { ERRORS, getSheetColumns, getUnmappedSheetColumns } from './_utils';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import { ImportFileCommon } from './ImportFileCommon';
|
||||||
|
import { ImportFileDataTransformer } from './ImportFileDataTransformer';
|
||||||
import ResourceService from '../Resource/ResourceService';
|
import ResourceService from '../Resource/ResourceService';
|
||||||
|
import UnitOfWork from '../UnitOfWork';
|
||||||
|
import { ImportFilePreviewPOJO } from './interfaces';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileProcess {
|
export class ImportFileProcess {
|
||||||
@@ -20,121 +16,28 @@ export class ImportFileProcess {
|
|||||||
private tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private importable: AccountsImportable;
|
private resource: ResourceService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private importCommon: ImportFileCommon;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private importParser: ImportFileDataTransformer;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private uow: UnitOfWork;
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private resourceService: ResourceService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the import file.
|
* Preview the imported file results before commiting the transactions.
|
||||||
* @param {string} filename
|
|
||||||
* @returns {Promise<Buffer>}
|
|
||||||
*/
|
|
||||||
public readImportFile(filename: string) {
|
|
||||||
return fs.readFile(`public/imports/${filename}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps the columns of the imported data based on the provided mapping attributes.
|
|
||||||
* @param {Record<string, any>[]} body - The array of data objects to map.
|
|
||||||
* @param {ImportMappingAttr[]} map - The mapping attributes.
|
|
||||||
* @returns {Record<string, any>[]} - The mapped data objects.
|
|
||||||
*/
|
|
||||||
public parseXlsxSheet(buffer) {
|
|
||||||
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
|
||||||
|
|
||||||
const firstSheetName = workbook.SheetNames[0];
|
|
||||||
const worksheet = workbook.Sheets[firstSheetName];
|
|
||||||
|
|
||||||
return XLSX.utils.sheet_to_json(worksheet);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitizes the data in the imported sheet by trimming object keys.
|
|
||||||
* @param json - The JSON data representing the imported sheet.
|
|
||||||
* @returns {string[][]} - The sanitized data with trimmed object keys.
|
|
||||||
*/
|
|
||||||
public sanitizeSheetData(json) {
|
|
||||||
return R.compose(R.map(Object.keys), R.map(trimObject))(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps the columns of the imported data based on the provided mapping attributes.
|
|
||||||
* @param {Record<string, any>[]} body - The array of data objects to map.
|
|
||||||
* @param {ImportMappingAttr[]} map - The mapping attributes.
|
|
||||||
* @returns {Record<string, any>[]} - The mapped data objects.
|
|
||||||
*/
|
|
||||||
private mapSheetColumns(
|
|
||||||
body: Record<string, any>[],
|
|
||||||
map: ImportMappingAttr[]
|
|
||||||
): Record<string, any>[] {
|
|
||||||
return body.map((item) => {
|
|
||||||
const newItem = {};
|
|
||||||
map
|
|
||||||
.filter((mapping) => !isUndefined(item[mapping.from]))
|
|
||||||
.forEach((mapping) => {
|
|
||||||
newItem[mapping.to] = item[mapping.from];
|
|
||||||
});
|
|
||||||
return newItem;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the given mapped DTOs and returns errors with their index.
|
|
||||||
* @param {Record<string, any>} mappedDTOs
|
|
||||||
* @returns {Promise<ImportValidationError[][]>}
|
|
||||||
*/
|
|
||||||
private async validateData(
|
|
||||||
tenantId: number,
|
|
||||||
resource: string,
|
|
||||||
mappedDTOs: Record<string, any>
|
|
||||||
): Promise<ImportValidationError[][]> {
|
|
||||||
const importableFields = this.resourceService.getResourceImportableFields(
|
|
||||||
tenantId,
|
|
||||||
resource
|
|
||||||
);
|
|
||||||
const YupSchema = convertFieldsToYupValidation(importableFields);
|
|
||||||
|
|
||||||
const validateData = async (data, index: number) => {
|
|
||||||
const _data = { ...data };
|
|
||||||
|
|
||||||
try {
|
|
||||||
await YupSchema.validate(_data, { abortEarly: false });
|
|
||||||
return { index, data: _data, errors: [] };
|
|
||||||
} catch (validationError) {
|
|
||||||
const errors = validationError.inner.map((error) => ({
|
|
||||||
path: error.params.path,
|
|
||||||
label: error.params.label,
|
|
||||||
message: error.errors,
|
|
||||||
}));
|
|
||||||
return { index, data: _data, errors };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const errors = await bluebird.map(mappedDTOs, validateData, {
|
|
||||||
concurrency: 20,
|
|
||||||
});
|
|
||||||
return errors.filter((error) => error !== false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transformes the mapped DTOs.
|
|
||||||
* @param DTOs
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
private transformDTOs(DTOs) {
|
|
||||||
return DTOs.map((DTO) => this.importable.transform(DTO));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes the import file sheet through the resource service.
|
|
||||||
* @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 import(
|
||||||
|
tenantId: number,
|
||||||
|
importId: number,
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
): Promise<ImportFilePreviewPOJO> {
|
||||||
const { Import } = this.tenancy.models(tenantId);
|
const { Import } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const importFile = await Import.query()
|
const importFile = await Import.query()
|
||||||
@@ -145,55 +48,54 @@ export class ImportFileProcess {
|
|||||||
if (!importFile.isMapped) {
|
if (!importFile.isMapped) {
|
||||||
throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED);
|
throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED);
|
||||||
}
|
}
|
||||||
const buffer = await this.readImportFile(importFile.filename);
|
// Read the imported file.
|
||||||
const jsonData = this.parseXlsxSheet(buffer);
|
const buffer = await this.importCommon.readImportFile(importFile.filename);
|
||||||
|
const sheetData = this.importCommon.parseXlsxSheet(buffer);
|
||||||
|
const header = getSheetColumns(sheetData);
|
||||||
|
|
||||||
const data = this.sanitizeSheetData(jsonData);
|
const importableFields = this.resource.getResourceImportableFields(
|
||||||
|
|
||||||
const header = first(data);
|
|
||||||
const body = jsonData;
|
|
||||||
|
|
||||||
const mappedDTOs = this.mapSheetColumns(body, importFile.mappingParsed);
|
|
||||||
const transformedDTOs = this.transformDTOs(mappedDTOs);
|
|
||||||
|
|
||||||
// Validate the mapped DTOs.
|
|
||||||
const rowsWithErrors = await this.validateData(
|
|
||||||
tenantId,
|
tenantId,
|
||||||
importFile.resource,
|
importFile.resource
|
||||||
transformedDTOs
|
|
||||||
);
|
);
|
||||||
// Runs the importing under UOW envirement.
|
// Prases the sheet json data.
|
||||||
await this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
const parsedData = this.importParser.parseSheetData(
|
||||||
await bluebird.map(
|
importFile,
|
||||||
rowsWithErrors,
|
importableFields,
|
||||||
(rowWithErrors) => {
|
sheetData
|
||||||
if (rowWithErrors.errors.length === 0) {
|
);
|
||||||
return this.importable.importable(
|
// Runs the importing operation with ability to return errors that will happen.
|
||||||
tenantId,
|
const [successedImport, failedImport] = await this.uow.withTransaction(
|
||||||
rowWithErrors.data,
|
tenantId,
|
||||||
trx
|
(trx: Knex.Transaction) =>
|
||||||
);
|
this.importCommon.import(
|
||||||
}
|
tenantId,
|
||||||
},
|
importFile.resource,
|
||||||
{ concurrency: 10 }
|
parsedData,
|
||||||
);
|
trx
|
||||||
});
|
),
|
||||||
// Deletes the imported file after importing success./
|
trx
|
||||||
await this.deleteImportFile(tenantId, importFile)
|
);
|
||||||
}
|
const mapping = importFile.mappingParsed;
|
||||||
|
const errors = chain(failedImport)
|
||||||
|
.map((oper) => oper.error)
|
||||||
|
.flatten()
|
||||||
|
.value();
|
||||||
|
|
||||||
/**
|
const unmappedColumns = getUnmappedSheetColumns(header, mapping);
|
||||||
* Deletes the imported file from the storage and database.
|
const totalCount = parsedData.length;
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {} importFile
|
|
||||||
*/
|
|
||||||
private async deleteImportFile(tenantId: number, importFile: any) {
|
|
||||||
const { Import } = this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
// Deletes the import row.
|
const createdCount = successedImport.length;
|
||||||
await Import.query().findById(importFile.id).delete();
|
const errorsCount = failedImport.length;
|
||||||
|
const skippedCount = errorsCount;
|
||||||
|
|
||||||
// Deletes the imported file.
|
return {
|
||||||
await fs.unlink(`public/imports/${importFile.filename}`);
|
createdCount,
|
||||||
|
skippedCount,
|
||||||
|
totalCount,
|
||||||
|
errorsCount,
|
||||||
|
errors,
|
||||||
|
unmappedColumns: unmappedColumns,
|
||||||
|
unmappedColumnsCount: unmappedColumns.length,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { first, values } from 'lodash';
|
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { ServiceError } from '@/exceptions';
|
|
||||||
import XLSX from 'xlsx';
|
|
||||||
import * as R from 'ramda';
|
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
import { ERRORS, trimObject } from './_utils';
|
import { sanitizeResourceName } from './_utils';
|
||||||
import ResourceService from '../Resource/ResourceService';
|
import ResourceService from '../Resource/ResourceService';
|
||||||
import fs from 'fs/promises';
|
|
||||||
import { IModelMetaField } from '@/interfaces';
|
import { IModelMetaField } from '@/interfaces';
|
||||||
import { ImportFileCommon } from './ImportFileCommon';
|
import { ImportFileCommon } from './ImportFileCommon';
|
||||||
|
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
||||||
|
import { ImportFileUploadPOJO } from './interfaces';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileUploadService {
|
export class ImportFileUploadService {
|
||||||
@Inject()
|
@Inject()
|
||||||
@@ -20,6 +18,9 @@ export class ImportFileUploadService {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private importFileCommon: ImportFileCommon;
|
private importFileCommon: ImportFileCommon;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private importValidator: ImportFileDataValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the imported file and stores the import file meta under unqiue id.
|
* Reads the imported file and stores the import file meta under unqiue id.
|
||||||
* @param {number} tenantId - Tenant id.
|
* @param {number} tenantId - Tenant id.
|
||||||
@@ -30,58 +31,50 @@ export class ImportFileUploadService {
|
|||||||
*/
|
*/
|
||||||
public async import(
|
public async import(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
resource: string,
|
resourceName: string,
|
||||||
filePath: string,
|
|
||||||
filename: string
|
filename: string
|
||||||
) {
|
): Promise<ImportFileUploadPOJO> {
|
||||||
const { Import } = this.tenancy.models(tenantId);
|
const { Import } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const resourceMeta = this.resourceService.getResourceMeta(
|
const resourceMeta = this.resourceService.getResourceMeta(
|
||||||
tenantId,
|
tenantId,
|
||||||
resource
|
resourceName
|
||||||
);
|
);
|
||||||
// Throw service error if the resource does not support importing.
|
// Throw service error if the resource does not support importing.
|
||||||
if (!resourceMeta.importable) {
|
this.importValidator.validateResourceImportable(resourceMeta);
|
||||||
throw new ServiceError(ERRORS.RESOURCE_NOT_IMPORTABLE);
|
|
||||||
}
|
|
||||||
// Reads the imported file into buffer.
|
// Reads the imported file into buffer.
|
||||||
const buffer = await this.importFileCommon.readImportFile(filename);
|
const buffer = await this.importFileCommon.readImportFile(filename);
|
||||||
|
|
||||||
// Parse the buffer file to array data.
|
// Parse the buffer file to array data.
|
||||||
const jsonData = this.importFileCommon.parseXlsxSheet(buffer);
|
const sheetData = this.importFileCommon.parseXlsxSheet(buffer);
|
||||||
|
|
||||||
const columns = this.getColumns(jsonData);
|
const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
|
||||||
const coumnsStringified = JSON.stringify(columns);
|
const coumnsStringified = JSON.stringify(sheetColumns);
|
||||||
|
|
||||||
// @todo validate the resource.
|
const _resourceName = sanitizeResourceName(resourceName);
|
||||||
const _resource = this.resourceService.resourceToModelName(resource);
|
|
||||||
|
|
||||||
const exportFile = await Import.query().insert({
|
// Store the import model with related metadata.
|
||||||
|
const importFile = await Import.query().insert({
|
||||||
filename,
|
filename,
|
||||||
importId: filename,
|
importId: filename,
|
||||||
resource: _resource,
|
resource: _resourceName,
|
||||||
columns: coumnsStringified,
|
columns: coumnsStringified,
|
||||||
});
|
});
|
||||||
const resourceColumns = this.resourceService.getResourceImportableFields(
|
const resourceColumns = this.resourceService.getResourceImportableFields(
|
||||||
tenantId,
|
tenantId,
|
||||||
resource
|
_resourceName
|
||||||
);
|
);
|
||||||
const resourceColumnsTransformeed = Object.entries(resourceColumns).map(
|
const resourceColumnsTransformeed = Object.entries(resourceColumns).map(
|
||||||
([key, { name }]: [string, IModelMetaField]) => ({ key, name })
|
([key, { name }]: [string, IModelMetaField]) => ({ key, name })
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
export: exportFile,
|
import: {
|
||||||
columns,
|
importId: importFile.importId,
|
||||||
|
resource: importFile.resource,
|
||||||
|
},
|
||||||
|
sheetColumns,
|
||||||
resourceColumns: resourceColumnsTransformeed,
|
resourceColumns: resourceColumnsTransformeed,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the sheet columns from the given sheet data.
|
|
||||||
* @param {unknown[]} json
|
|
||||||
* @returns {string[]}
|
|
||||||
*/
|
|
||||||
private getColumns(json: unknown[]): string[] {
|
|
||||||
return R.compose(Object.keys, trimObject, first)(json);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,22 +22,16 @@ export class ImportResourceApplication {
|
|||||||
/**
|
/**
|
||||||
* Reads the imported file and stores the import file meta under unqiue id.
|
* Reads the imported file and stores the import file meta under unqiue id.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {string} filePath -
|
* @param {string} resource -
|
||||||
* @param {string} fileName -
|
* @param {string} fileName -
|
||||||
* @returns
|
* @returns {Promise<ImportFileUploadPOJO>}
|
||||||
*/
|
*/
|
||||||
public async import(
|
public async import(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
resource: string,
|
resource: string,
|
||||||
filePath: string,
|
|
||||||
filename: string
|
filename: string
|
||||||
) {
|
) {
|
||||||
return this.importFileService.import(
|
return this.importFileService.import(tenantId, resource, filename);
|
||||||
tenantId,
|
|
||||||
resource,
|
|
||||||
filePath,
|
|
||||||
filename
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,6 +65,6 @@ export class ImportResourceApplication {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async process(tenantId: number, importId: number) {
|
public async process(tenantId: number, importId: number) {
|
||||||
return this.importProcessService.process(tenantId, importId);
|
return this.importProcessService.import(tenantId, importId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
packages/server/src/services/Import/ImportableRegistry.ts
Normal file
31
packages/server/src/services/Import/ImportableRegistry.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { camelCase, upperFirst } from 'lodash';
|
||||||
|
|
||||||
|
export class ImportableRegistry {
|
||||||
|
private static instance: ImportableRegistry;
|
||||||
|
private importables: Record<string, any>;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.importables = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): ImportableRegistry {
|
||||||
|
if (!ImportableRegistry.instance) {
|
||||||
|
ImportableRegistry.instance = new ImportableRegistry();
|
||||||
|
}
|
||||||
|
return ImportableRegistry.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerImportable(resource: string, importable: any): void {
|
||||||
|
const _resource = this.sanitizeResourceName(resource);
|
||||||
|
this.importables[_resource] = importable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getImportable(name: string): any {
|
||||||
|
const _name = this.sanitizeResourceName(name);
|
||||||
|
return this.importables[_name];
|
||||||
|
}
|
||||||
|
|
||||||
|
private sanitizeResourceName(resource: string) {
|
||||||
|
return upperFirst(camelCase(resource));
|
||||||
|
}
|
||||||
|
}
|
||||||
39
packages/server/src/services/Import/ImportableResources.ts
Normal file
39
packages/server/src/services/Import/ImportableResources.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import Container, { Service } from 'typedi';
|
||||||
|
import { AccountsImportable } from './AccountsImportable';
|
||||||
|
import { ImportableRegistry } from './ImportableRegistry';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class ImportableResources {
|
||||||
|
private static registry: ImportableRegistry;
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importable instances.
|
||||||
|
*/
|
||||||
|
private importables = [
|
||||||
|
{ resource: 'Account', importable: AccountsImportable },
|
||||||
|
];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
import { upperFirst, camelCase, first } from 'lodash';
|
||||||
|
import pluralize from 'pluralize';
|
||||||
import { ResourceMetaFieldsMap } from './interfaces';
|
import { ResourceMetaFieldsMap } from './interfaces';
|
||||||
import { IModelMetaField } from '@/interfaces';
|
import { IModelMetaField } from '@/interfaces';
|
||||||
|
|
||||||
@@ -62,11 +64,16 @@ export const ERRORS = {
|
|||||||
IMPORT_FILE_NOT_MAPPED: 'IMPORT_FILE_NOT_MAPPED',
|
IMPORT_FILE_NOT_MAPPED: 'IMPORT_FILE_NOT_MAPPED',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const getUnmappedSheetColumns = (columns, mapping) => {
|
export const getUnmappedSheetColumns = (columns, mapping) => {
|
||||||
return columns.filter(
|
return columns.filter(
|
||||||
(column) => !mapping.some((map) => map.from === column)
|
(column) => !mapping.some((map) => map.from === column)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sanitizeResourceName = (resourceName: string) => {
|
||||||
|
return upperFirst(camelCase(pluralize.singular(resourceName)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSheetColumns = (sheetData: unknown[]) => {
|
||||||
|
return Object.keys(first(sheetData));
|
||||||
|
};
|
||||||
|
|||||||
@@ -18,3 +18,40 @@ export interface ImportInsertError {
|
|||||||
errorCode: string;
|
errorCode: string;
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImportFileUploadPOJO {
|
||||||
|
import: {
|
||||||
|
importId: string;
|
||||||
|
resource: string;
|
||||||
|
};
|
||||||
|
sheetColumns: string[];
|
||||||
|
resourceColumns: { key: string; name: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImportFileMapPOJO {
|
||||||
|
import: {
|
||||||
|
importId: string;
|
||||||
|
resource: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImportFilePreviewPOJO {
|
||||||
|
createdCount: number;
|
||||||
|
skippedCount: number;
|
||||||
|
totalCount: number;
|
||||||
|
errorsCount: number;
|
||||||
|
errors: ImportInsertError[];
|
||||||
|
unmappedColumns: string[];
|
||||||
|
unmappedColumnsCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface ImportOperSuccess {
|
||||||
|
data: unknown;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImportOperError {
|
||||||
|
error: ImportInsertError;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user