feat: linking relation with id in importing

This commit is contained in:
Ahmed Bouhuolia
2024-04-01 01:13:31 +02:00
parent 22a016b56e
commit 74da28b464
21 changed files with 394 additions and 84 deletions

View File

@@ -1,21 +1,32 @@
import { Service } from 'typedi';
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import { isUndefined, get, chain } from 'lodash';
import bluebird from 'bluebird';
import { isUndefined, get, chain, toArray, pickBy, castArray } from 'lodash';
import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
import { parseBoolean } from '@/utils';
import { trimObject } from './_utils';
import { Account, Item } from '@/models';
import ResourceService from '../Resource/ResourceService';
import { Knex } from 'knex';
const CurrencyParsingDTOs = 10;
@Service()
export class ImportFileDataTransformer {
@Inject()
private resource: ResourceService;
/**
*
* @param {number} tenantId -
* @param {}
*/
public parseSheetData(
public async parseSheetData(
tenantId: number,
importFile: any,
importableFields: any,
data: Record<string, unknown>[]
data: Record<string, unknown>[],
trx?: Knex.Transaction
) {
// Sanitize the sheet data.
const sanitizedData = this.sanitizeSheetData(data);
@@ -25,10 +36,17 @@ export class ImportFileDataTransformer {
sanitizedData,
importFile.mappingParsed
);
const resourceModel = this.resource.getResourceModel(
tenantId,
importFile.resource
);
// Parse the mapped sheet values.
const parsedValues = this.parseExcelValues(importableFields, mappedDTOs);
return parsedValues;
return this.parseExcelValues(
importableFields,
mappedDTOs,
resourceModel,
trx
);
}
/**
@@ -67,35 +85,86 @@ export class ImportFileDataTransformer {
* @param {Record<string, any>} valueDTOS -
* @returns {Record<string, any>}
*/
public parseExcelValues(
public async parseExcelValues(
fields: ResourceMetaFieldsMap,
valueDTOs: Record<string, any>[]
): Record<string, any> {
const parser = (value, key) => {
valueDTOs: Record<string, any>[],
resourceModel: any,
trx?: Knex.Transaction
): Promise<Record<string, any>> {
// Prases the given object value based on the field key type.
const parser = async (value, key) => {
let _value = value;
const field = fields[key];
// Parses the boolean value.
if (fields[key].fieldType === 'boolean') {
_value = parseBoolean(value, false);
// Parses the enumeration value.
} else if (fields[key].fieldType === 'enumeration') {
} else if (field.fieldType === 'enumeration') {
const field = fields[key];
const option = get(field, 'options', []).find(
(option) => option.label === value
);
_value = get(option, 'key');
// Prases the numeric value.
// Parses the numeric value.
} else if (fields[key].fieldType === 'number') {
_value = parseFloat(value);
// Parses the relation value.
} else if (field.fieldType === 'relation') {
const relationModel = resourceModel.relationMappings[field.relationKey];
const RelationModel = relationModel?.modelClass;
if (!relationModel || !RelationModel) {
throw new Error(`The relation model of ${key} field is not exist.`);
}
const relationQuery = RelationModel.query(trx);
const relationKeys = field?.importableRelationLabel
? castArray(field?.importableRelationLabel)
: castArray(field?.relationEntityLabel);
relationQuery.where(function () {
relationKeys.forEach((relationKey: string) => {
this.orWhereRaw('LOWER(??) = LOWER(?)', [relationKey, value]);
});
});
const result = await relationQuery.first();
_value = get(result, 'id');
}
return _value;
};
return valueDTOs.map((DTO) => {
return chain(DTO)
.pickBy((value, key) => !isUndefined(fields[key]))
.mapValues(parser)
.value();
const parseKey = (key: string) => {
const field = fields[key];
let _objectTransferObjectKey = key;
if (field.fieldType === 'relation') {
_objectTransferObjectKey = `${key}Id`;
}
return _objectTransferObjectKey;
};
const parseAsync = async (valueDTO) => {
// Remove the undefined fields.
const _valueDTO = pickBy(
valueDTO,
(value, key) => !isUndefined(fields[key])
);
const keys = Object.keys(_valueDTO);
// Map the object values.
return bluebird.reduce(
keys,
async (acc, key) => {
const parsedValue = await parser(_valueDTO[key], key);
const parsedKey = await parseKey(key);
acc[parsedKey] = parsedValue;
return acc;
},
{}
);
};
return bluebird.map(valueDTOs, parseAsync, {
concurrency: CurrencyParsingDTOs,
});
}
}

View File

@@ -57,19 +57,31 @@ export class ImportFileProcess {
tenantId,
importFile.resource
);
// Prases the sheet json data.
const parsedData = this.importParser.parseSheetData(
importFile,
importableFields,
sheetData
);
// Runs the importing operation with ability to return errors that will happen.
const [successedImport, failedImport] = await this.uow.withTransaction(
tenantId,
(trx: Knex.Transaction) =>
this.importCommon.import(tenantId, importFile, parsedData, trx),
trx
);
const [successedImport, failedImport, allData] =
await this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Prases the sheet json data.
const parsedData = await this.importParser.parseSheetData(
tenantId,
importFile,
importableFields,
sheetData,
trx
);
const [successedImport, failedImport] =
await this.importCommon.import(
tenantId,
importFile,
parsedData,
trx
);
return [successedImport, failedImport, parsedData];
},
trx
);
const mapping = importFile.mappingParsed;
const errors = chain(failedImport)
.map((oper) => oper.error)
@@ -77,7 +89,7 @@ export class ImportFileProcess {
.value();
const unmappedColumns = getUnmappedSheetColumns(header, mapping);
const totalCount = parsedData.length;
const totalCount = allData.length;
const createdCount = successedImport.length;
const errorsCount = failedImport.length;

View File

@@ -4,6 +4,8 @@ import { ImportableRegistry } from './ImportableRegistry';
import { UncategorizedTransactionsImportable } from '../Cashflow/UncategorizedTransactionsImportable';
import { CustomersImportable } from '../Contacts/Customers/CustomersImportable';
import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable';
import { ItemsImportable } from '../Items/ItemsImportable';
import { ItemCategoriesImportable } from '../ItemCategories/ItemCategoriesImportable';
@Service()
export class ImportableResources {
@@ -24,6 +26,8 @@ export class ImportableResources {
},
{ resource: 'Customer', importable: CustomersImportable },
{ resource: 'Vendor', importable: VendorsImportable },
{ resource: 'Item', importable: ItemsImportable },
{ resource: 'ItemCategory', importable: ItemCategoriesImportable },
];
public get registry() {

View File

@@ -1,5 +1,12 @@
import * as Yup from 'yup';
import { defaultTo, upperFirst, camelCase, first, isUndefined, pickBy } from 'lodash';
import {
defaultTo,
upperFirst,
camelCase,
first,
isUndefined,
pickBy,
} from 'lodash';
import pluralize from 'pluralize';
import { ResourceMetaFieldsMap } from './interfaces';
import { IModelMetaField } from '@/interfaces';
@@ -83,11 +90,25 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
if (field.required) {
fieldSchema = fieldSchema.required();
}
yupSchema[fieldName] = fieldSchema;
const _fieldName = parseFieldName(fieldName, field);
yupSchema[_fieldName] = fieldSchema;
});
return Yup.object().shape(yupSchema);
};
const parseFieldName = (fieldName: string, field: IModelMetaField) => {
let _key = fieldName;
if (field.fieldType === 'relation') {
_key = `${fieldName}Id`;
}
if (field.dataTransferObjectKey) {
_key = field.dataTransferObjectKey;
}
return _key;
};
export const getUnmappedSheetColumns = (columns, mapping) => {
return columns.filter(
(column) => !mapping.some((map) => map.from === column)