fix: import resource imporements

This commit is contained in:
Ahmed Bouhuolia
2024-03-27 04:01:01 +02:00
parent 973d1832bd
commit ad4e51d81d
59 changed files with 1508 additions and 211 deletions

View File

@@ -34,4 +34,12 @@ export class AccountsImportable extends Importable {
public get concurrency() {
return 1;
}
public public sampleData(): any[] {
return [
{
}
]
}
}

View File

@@ -1,7 +1,7 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import UnitOfWork, { IsolationLevel } from '../UnitOfWork';
import { Knex } from 'knex';
import UnitOfWork from '../UnitOfWork';
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
@Service()
@@ -19,10 +19,10 @@ export class CreateUncategorizedTransaction {
*/
public create(
tenantId: number,
createDTO: CreateUncategorizedTransactionDTO
createDTO: CreateUncategorizedTransactionDTO,
trx?: Knex.Transaction
) {
const { UncategorizedCashflowTransaction, Account } =
this.tenancy.models(tenantId);
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
return this.uow.withTransaction(
tenantId,
@@ -32,9 +32,9 @@ export class CreateUncategorizedTransaction {
).insertAndFetch({
...createDTO,
});
return transaction;
},
trx
);
}
}

View File

@@ -0,0 +1,84 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import * as yup from 'yup';
import { Importable } from '../Import/Importable';
import { CreateUncategorizedTransaction } from './CreateUncategorizedTransaction';
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
import { ImportableContext } from '../Import/interfaces';
import HasTenancyService from '../Tenancy/TenancyService';
import { ImportSampleData } from './constants';
@Service()
export class UncategorizedTransactionsImportable extends Importable {
@Inject()
private createUncategorizedTransaction: CreateUncategorizedTransaction;
@Inject()
private tenancy: HasTenancyService;
/**
* Passing the sheet DTO to create uncategorized transaction.
* @param {number} tenantId
* @param {number} tenantId
* @param {any} createDTO
* @param {Knex.Transaction} trx
*/
public async importable(
tenantId: number,
createDTO: CreateUncategorizedTransactionDTO,
trx?: Knex.Transaction
) {
return this.createUncategorizedTransaction.create(tenantId, createDTO, trx);
}
/**
* Transformes the DTO before validating and importing.
* @param {CreateUncategorizedTransactionDTO} createDTO
* @param {ImportableContext} context
* @returns {CreateUncategorizedTransactionDTO}
*/
public transform(
createDTO: CreateUncategorizedTransactionDTO,
context?: ImportableContext
): CreateUncategorizedTransactionDTO {
return {
...createDTO,
accountId: context.import.paramsParsed.accountId,
};
}
/**
* Sample data used to download sample sheet.
* @returns {Record<string, any>[]}
*/
public sampleData(): Record<string, any>[] {
return ImportSampleData;
}
/**
* Params validation schema.
* @returns {ValidationSchema[]}
*/
public paramsValidationSchema() {
return yup.object().shape({
accountId: yup.number().required(),
});
}
/**
* Validates the params existance asyncly.
* @param {number} tenantId -
* @param {Record<string, any>} params -
*/
public async validateParams(
tenantId: number,
params: Record<string, any>
): Promise<void> {
const { Account } = this.tenancy.models(tenantId);
if (params.accountId) {
await Account.query()
.findById(params.accountId)
.throwIfNotFound({});
}
}
}

View File

@@ -75,3 +75,25 @@ export interface ICashflowTransactionTypeMeta {
direction: CASHFLOW_DIRECTION;
creditType: string[];
}
export const ImportSampleData = [
{
Amount: 5000,
Date: '2024-01-01',
Payee: 'John Roberts',
Description: 'Cheque deposit',
},
{
Amount: 5000,
Date: '2024-01-01',
Payee: 'John Roberts',
Description: 'Cheque deposit',
},
{
Amount: 5000,
Date: '2024-01-01',
Payee: 'John Roberts',
Description: 'Cheque deposit',
},
]

View File

@@ -36,7 +36,7 @@ export class CreateCustomer {
public async createCustomer(
tenantId: number,
customerDTO: ICustomerNewDTO,
authorizedUser: ISystemUser
trx?: Knex.Transaction
): Promise<ICustomer> {
const { Contact } = this.tenancy.models(tenantId);
@@ -46,28 +46,31 @@ export class CreateCustomer {
customerDTO
);
// Creates a new customer under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onCustomerCreating` event.
await this.eventPublisher.emitAsync(events.customers.onCreating, {
tenantId,
customerDTO,
trx,
} as ICustomerEventCreatingPayload);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onCustomerCreating` event.
await this.eventPublisher.emitAsync(events.customers.onCreating, {
tenantId,
customerDTO,
trx,
} as ICustomerEventCreatingPayload);
// Creates a new contact as customer.
const customer = await Contact.query(trx).insertAndFetch({
...customerObj,
});
// Triggers `onCustomerCreated` event.
await this.eventPublisher.emitAsync(events.customers.onCreated, {
customer,
tenantId,
customerId: customer.id,
authorizedUser,
trx,
} as ICustomerEventCreatedPayload);
// Creates a new contact as customer.
const customer = await Contact.query(trx).insertAndFetch({
...customerObj,
});
// Triggers `onCustomerCreated` event.
await this.eventPublisher.emitAsync(events.customers.onCreated, {
customer,
tenantId,
customerId: customer.id,
trx,
} as ICustomerEventCreatedPayload);
return customer;
});
return customer;
},
trx
);
}
}

View File

@@ -1,6 +1,6 @@
import moment from 'moment';
import { defaultTo, omit, isEmpty } from 'lodash';
import { Service, Inject } from 'typedi';
import { Service } from 'typedi';
import {
ContactService,
ICustomer,
@@ -51,6 +51,10 @@ export class CreateEditCustomerDTO {
).toMySqlDateTime(),
}
: {}),
openingBalanceExchangeRate: defaultTo(
customerDTO.openingBalanceExchangeRate,
1
),
};
};

View File

@@ -4,7 +4,6 @@ import {
ICustomerEditDTO,
ICustomerEventEditedPayload,
ICustomerEventEditingPayload,
ISystemUser,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';

View File

@@ -53,13 +53,8 @@ export class CustomersApplication {
public createCustomer = (
tenantId: number,
customerDTO: ICustomerNewDTO,
authorizedUser: ISystemUser
) => {
return this.createCustomerService.createCustomer(
tenantId,
customerDTO,
authorizedUser
);
return this.createCustomerService.createCustomer(tenantId, customerDTO);
};
/**

View File

@@ -0,0 +1,26 @@
import { Inject, Service } from 'typedi';
import { Importable } from '@/services/Import/Importable';
import { CreateCustomer } from './CRUD/CreateCustomer';
import { Knex } from 'knex';
import { ICustomer, ICustomerNewDTO } from '@/interfaces';
@Service()
export class CustomersImportable extends Importable {
@Inject()
private createCustomerService: CreateCustomer;
/**
* Mapps the imported data to create a new customer service.
* @param {number} tenantId
* @param {ICustomerNewDTO} createDTO
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public async importable(
tenantId: number,
createDTO: ICustomerNewDTO,
trx?: Knex.Transaction<any, any[]>
): Promise<void> {
await this.createCustomerService.createCustomer(tenantId, createDTO, trx);
}
}

View File

@@ -49,6 +49,10 @@ export class CreateEditVendorDTO {
).toMySqlDateTime(),
}
: {}),
openingBalanceExchangeRate: defaultTo(
vendorDTO.openingBalanceExchangeRate,
1
),
};
};

View File

@@ -35,7 +35,7 @@ export class CreateVendor {
public async createVendor(
tenantId: number,
vendorDTO: IVendorNewDTO,
authorizedUser: ISystemUser
trx?: Knex.Transaction
) {
const { Contact } = this.tenancy.models(tenantId);
@@ -45,28 +45,31 @@ export class CreateVendor {
vendorDTO
);
// Creates vendor contact under unit-of-work evnirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onVendorCreating` event.
await this.eventPublisher.emitAsync(events.vendors.onCreating, {
tenantId,
vendorDTO,
trx,
} as IVendorEventCreatingPayload);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onVendorCreating` event.
await this.eventPublisher.emitAsync(events.vendors.onCreating, {
tenantId,
vendorDTO,
trx,
} as IVendorEventCreatingPayload);
// Creates a new contact as vendor.
const vendor = await Contact.query(trx).insertAndFetch({
...vendorObject,
});
// Triggers `onVendorCreated` event.
await this.eventPublisher.emitAsync(events.vendors.onCreated, {
tenantId,
vendorId: vendor.id,
vendor,
authorizedUser,
trx,
} as IVendorEventCreatedPayload);
// Creates a new contact as vendor.
const vendor = await Contact.query(trx).insertAndFetch({
...vendorObject,
});
// Triggers `onVendorCreated` event.
await this.eventPublisher.emitAsync(events.vendors.onCreated, {
tenantId,
vendorId: vendor.id,
vendor,
trx,
} as IVendorEventCreatedPayload);
return vendor;
});
return vendor;
},
trx
);
}
}

View File

@@ -0,0 +1,24 @@
import { Importable } from '@/services/Import/Importable';
import { CreateVendor } from './CRUD/CreateVendor';
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
@Service()
export class VendorsImportable extends Importable {
@Inject()
private createVendorService: CreateVendor;
/**
* Maps the imported data to create a new vendor service.
* @param {number} tenantId
* @param {} createDTO
* @param {Knex.Transaction} trx
*/
public async importable(
tenantId: number,
createDTO: any,
trx?: Knex.Transaction<any, any[]>
): Promise<void> {
await this.createVendorService.createVendor(tenantId, createDTO, trx);
}
}

View File

@@ -7,16 +7,16 @@ import { first } from 'lodash';
import { ImportFileDataValidator } from './ImportFileDataValidator';
import { Knex } from 'knex';
import {
ImportInsertError,
ImportOperError,
ImportOperSuccess,
ImportableContext,
} from './interfaces';
import { AccountsImportable } from '../Accounts/AccountsImportable';
import { ServiceError } from '@/exceptions';
import { trimObject } from './_utils';
import { ImportableResources } from './ImportableResources';
import ResourceService from '../Resource/ResourceService';
import HasTenancyService from '../Tenancy/TenancyService';
import Import from '@/models/Import';
@Service()
export class ImportFileCommon {
@@ -39,12 +39,12 @@ export class ImportFileCommon {
* @returns {Record<string, any>[]} - The mapped data objects.
*/
public parseXlsxSheet(buffer: Buffer): Record<string, unknown>[] {
const workbook = XLSX.read(buffer, { type: 'buffer' });
const workbook = XLSX.read(buffer, { type: 'buffer', raw: true });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
return XLSX.utils.sheet_to_json(worksheet);
return XLSX.utils.sheet_to_json(worksheet, {});
}
/**
@@ -57,7 +57,7 @@ export class ImportFileCommon {
}
/**
* Imports the given parsed data to the resource storage through registered importable service.
* Imports the given parsed data to the resource storage through registered importable service.
* @param {number} tenantId -
* @param {string} resourceName - Resource name.
* @param {Record<string, any>} parsedData - Parsed data.
@@ -66,16 +66,16 @@ export class ImportFileCommon {
*/
public async import(
tenantId: number,
resourceName: string,
importFile: Import,
parsedData: Record<string, any>[],
trx?: Knex.Transaction
): Promise<[ImportOperSuccess[], ImportOperError[]]> {
const importableFields = this.resource.getResourceImportableFields(
tenantId,
resourceName
importFile.resource
);
const ImportableRegistry = this.importable.registry;
const importable = ImportableRegistry.getImportable(resourceName);
const importable = ImportableRegistry.getImportable(importFile.resource);
const concurrency = importable.concurrency || 10;
@@ -83,15 +83,25 @@ export class ImportFileCommon {
const failed: ImportOperError[] = [];
const importAsync = async (objectDTO, index: number): Promise<void> => {
const context: ImportableContext = {
rowIndex: index,
import: importFile,
};
const transformedDTO = importable.transform(objectDTO, context);
try {
// Validate the DTO object before passing it to the service layer.
await this.importFileValidator.validateData(
importableFields,
objectDTO
transformedDTO
);
try {
// Run the importable function and listen to the errors.
const data = await importable.importable(tenantId, objectDTO, trx);
const data = await importable.importable(
tenantId,
transformedDTO,
trx
);
success.push({ index, data });
} catch (err) {
if (err instanceof ServiceError) {
@@ -115,6 +125,60 @@ export class ImportFileCommon {
return [success, failed];
}
/**
*
* @param {string} resourceName
* @param {Record<string, any>} params
*/
public async validateParamsSchema(
resourceName: string,
params: Record<string, any>
) {
const ImportableRegistry = this.importable.registry;
const importable = ImportableRegistry.getImportable(resourceName);
const yupSchema = importable.paramsValidationSchema();
try {
await yupSchema.validate(params, { abortEarly: false });
} catch (validationError) {
const errors = validationError.inner.map((error) => ({
errorCode: 'ParamsValidationError',
errorMessage: error.errors,
}));
throw errors;
}
}
/**
*
* @param {string} resourceName
* @param {Record<string, any>} params
*/
public async validateParams(
tenantId: number,
resourceName: string,
params: Record<string, any>
) {
const ImportableRegistry = this.importable.registry;
const importable = ImportableRegistry.getImportable(resourceName);
await importable.validateParams(tenantId, params);
}
/**
*
* @param {string} resourceName
* @param {Record<string, any>} params
* @returns
*/
public transformParams(resourceName: string, params: Record<string, any>) {
const ImportableRegistry = this.importable.registry;
const importable = ImportableRegistry.getImportable(resourceName);
return importable.transformParams(params);
}
/**
* Retrieves the sheet columns from the given sheet data.
* @param {unknown[]} json

View File

@@ -1,7 +1,11 @@
import { fromPairs } from 'lodash';
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { ImportFileMapPOJO, ImportMappingAttr } from './interfaces';
import {
ImportDateFormats,
ImportFileMapPOJO,
ImportMappingAttr,
} from './interfaces';
import ResourceService from '../Resource/ResourceService';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './_utils';
@@ -37,12 +41,14 @@ export class ImportFileMapping {
// Validate the diplicated relations of map attrs.
this.validateDuplicatedMapAttrs(maps);
// Validate the date format mapping.
this.validateDateFormatMapping(tenantId, importFile.resource, maps);
const mappingStringified = JSON.stringify(maps);
await Import.query().findById(importFile.id).patch({
mapping: mappingStringified,
});
return {
import: {
importId: importFile.importId,
@@ -106,4 +112,34 @@ export class ImportFileMapping {
}
});
}
/**
* Validates the date format mapping.
* @param {number} tenantId
* @param {string} resource
* @param {ImportMappingAttr[]} maps
*/
private validateDateFormatMapping(
tenantId: number,
resource: string,
maps: ImportMappingAttr[]
) {
const fields = this.resource.getResourceImportableFields(
tenantId,
resource
);
maps.forEach((map) => {
if (
typeof fields[map.to] !== 'undefined' &&
fields[map.to].fieldType === 'date'
) {
if (
typeof map.dateFormat !== 'undefined' &&
ImportDateFormats.indexOf(map.dateFormat) === -1
) {
throw new ServiceError(ERRORS.INVALID_MAP_DATE_FORMAT);
}
}
});
}
}

View File

@@ -0,0 +1,32 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { ImportFileMetaTransformer } from './ImportFileMetaTransformer';
@Service()
export class ImportFileMeta {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
*
* @param {number} tenantId
* @param {number} importId
* @returns {}
*/
async getImportMeta(tenantId: number, importId: string) {
const { Import } = this.tenancy.models(tenantId);
const importFile = await Import.query().findOne('importId', importId);
// Retrieves the transformed accounts collection.
return this.transformer.transform(
tenantId,
importFile,
new ImportFileMetaTransformer()
);
}
}

View File

@@ -0,0 +1,19 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class ImportFileMetaTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['map'];
};
public excludeAttributes = (): string[] => {
return ['id', 'filename', 'columns', 'mappingParsed', 'mapping'];
}
map(importFile) {
return importFile.mappingParsed;
}
}

View File

@@ -67,12 +67,7 @@ export class ImportFileProcess {
const [successedImport, failedImport] = await this.uow.withTransaction(
tenantId,
(trx: Knex.Transaction) =>
this.importCommon.import(
tenantId,
importFile.resource,
parsedData,
trx
),
this.importCommon.import(tenantId, importFile, parsedData, trx),
trx
);
const mapping = importFile.mappingParsed;

View File

@@ -6,6 +6,7 @@ import { IModelMetaField } from '@/interfaces';
import { ImportFileCommon } from './ImportFileCommon';
import { ImportFileDataValidator } from './ImportFileDataValidator';
import { ImportFileUploadPOJO } from './interfaces';
import { ServiceError } from '@/exceptions';
@Service()
export class ImportFileUploadService {
@@ -32,13 +33,15 @@ export class ImportFileUploadService {
public async import(
tenantId: number,
resourceName: string,
filename: string
filename: string,
params: Record<string, number | string>
): Promise<ImportFileUploadPOJO> {
const { Import } = this.tenancy.models(tenantId);
const resource = sanitizeResourceName(resourceName);
const resourceMeta = this.resourceService.getResourceMeta(
tenantId,
resourceName
resource
);
// Throw service error if the resource does not support importing.
this.importValidator.validateResourceImportable(resourceMeta);
@@ -48,22 +51,32 @@ export class ImportFileUploadService {
// Parse the buffer file to array data.
const sheetData = this.importFileCommon.parseXlsxSheet(buffer);
const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
const coumnsStringified = JSON.stringify(sheetColumns);
const _resourceName = sanitizeResourceName(resourceName);
try {
// Validates the params Yup schema.
await this.importFileCommon.validateParamsSchema(resource, params);
// Validates importable params asyncly.
await this.importFileCommon.validateParams(tenantId, resource, params);
} catch (error) {
throw error;
}
const _params = this.importFileCommon.transformParams(resource, params);
const paramsStringified = JSON.stringify(_params);
// Store the import model with related metadata.
const importFile = await Import.query().insert({
filename,
resource,
importId: filename,
resource: _resourceName,
columns: coumnsStringified,
params: paramsStringified,
});
const resourceColumnsMap = this.resourceService.getResourceImportableFields(
tenantId,
_resourceName
resource
);
const resourceColumns = this.getResourceColumns(resourceColumnsMap);

View File

@@ -4,6 +4,8 @@ import { ImportFileMapping } from './ImportFileMapping';
import { ImportMappingAttr } from './interfaces';
import { ImportFileProcess } from './ImportFileProcess';
import { ImportFilePreview } from './ImportFilePreview';
import { ImportSampleService } from './ImportSample';
import { ImportFileMeta } from './ImportFileMeta';
@Inject()
export class ImportResourceApplication {
@@ -19,25 +21,32 @@ export class ImportResourceApplication {
@Inject()
private ImportFilePreviewService: ImportFilePreview;
@Inject()
private importSampleService: ImportSampleService;
@Inject()
private importMetaService: ImportFileMeta;
/**
* Reads the imported file and stores the import file meta under unqiue id.
* @param {number} tenantId -
* @param {string} resource -
* @param {string} fileName -
* @param {string} resource - Resource name.
* @param {string} fileName - File name.
* @returns {Promise<ImportFileUploadPOJO>}
*/
public async import(
tenantId: number,
resource: string,
filename: string
filename: string,
params: Record<string, any>
) {
return this.importFileService.import(tenantId, resource, filename);
return this.importFileService.import(tenantId, resource, filename, params);
}
/**
* Mapping the excel sheet columns with resource columns.
* @param {number} tenantId
* @param {number} importId
* @param {number} importId - Import id.
* @param {ImportMappingAttr} maps
*/
public async mapping(
@@ -51,7 +60,7 @@ export class ImportResourceApplication {
/**
* Preview the mapped results before process importing.
* @param {number} tenantId
* @param {number} importId
* @param {number} importId - Import id.
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async preview(tenantId: number, importId: number) {
@@ -67,4 +76,27 @@ export class ImportResourceApplication {
public async process(tenantId: number, importId: number) {
return this.importProcessService.import(tenantId, importId);
}
/**
* Retrieves the import meta of the given import id.
* @param {number} tenantId -
* @param {string} importId - Import id.
* @returns {}
*/
public importMeta(tenantId: number, importId: string) {
return this.importMetaService.getImportMeta(tenantId, importId);
}
/**
* Retrieves the csv/xlsx sample sheet of the given
* @param {number} tenantId
* @param {number} resource - Resource name.
*/
public sample(
tenantId: number,
resource: string,
format: 'csv' | 'xlsx' = 'csv'
) {
return this.importSampleService.sample(tenantId, resource, format);
}
}

View File

@@ -0,0 +1,46 @@
import XLSX from 'xlsx';
import { Inject, Service } from 'typedi';
import { ImportableResources } from './ImportableResources';
import { sanitizeResourceName } from './_utils';
@Service()
export class ImportSampleService {
@Inject()
private importable: ImportableResources;
/**
* Retrieves the sample sheet of the given resource.
* @param {number} tenantId
* @param {string} resource
* @param {string} format
* @returns {Buffer | string}
*/
public sample(
tenantId: number,
resource: string,
format: 'csv' | 'xlsx'
): Buffer | string {
const _resource = sanitizeResourceName(resource);
const ImportableRegistry = this.importable.registry;
const importable = ImportableRegistry.getImportable(_resource);
const data = importable.sampleData();
const workbook = XLSX.utils.book_new();
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);
return csvOutput;
} else {
const xlsxOutput = XLSX.write(workbook, {
bookType: 'xlsx',
type: 'buffer',
});
return xlsxOutput;
}
}
}

View File

@@ -1,4 +1,6 @@
import { Knex } from 'knex';
import * as Yup from 'yup';
import { ImportableContext } from './interfaces';
export abstract class Importable {
/**
@@ -13,6 +15,16 @@ export abstract class Importable {
);
}
/**
* Transformes the DTO before passing it to importable and validation.
* @param {Record<string, any>} createDTO
* @param {ImportableContext} context
* @returns {Record<string, any>}
*/
public transform(createDTO: Record<string, any>, context: ImportableContext) {
return createDTO;
}
/**
* Concurrency controlling of the importing process.
* @returns {number}
@@ -20,4 +32,41 @@ export abstract class Importable {
public get concurrency() {
return 10;
}
/**
* Retrieves the sample data of importable.
* @returns {Array<any>}
*/
public sampleData(): Array<any> {
return [];
}
// ------------------
// # Params
// ------------------
/**
* Params Yup validation schema.
* @returns {Yup.ObjectSchema<object, object>}
*/
public paramsValidationSchema(): Yup.ObjectSchema<object, object> {
return Yup.object().nullable();
}
/**
* Validates the params of the importable service.
* @param {Record<string, any>}
* @returns {Promise<boolean>} - True means passed and false failed.
*/
public async validateParams(
tenantId: number,
params: Record<string, any>
): Promise<void> {}
/**
* Transformes the import params before storing them.
* @param {Record<string, any>} parmas
*/
public transformParams(parmas: Record<string, any>) {
return parmas;
}
}

View File

@@ -1,6 +1,9 @@
import Container, { Service } from 'typedi';
import { AccountsImportable } from '../Accounts/AccountsImportable';
import { ImportableRegistry } from './ImportableRegistry';
import { UncategorizedTransactionsImportable } from '../Cashflow/UncategorizedTransactionsImportable';
import { CustomersImportable } from '../Contacts/Customers/CustomersImportable';
import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable';
@Service()
export class ImportableResources {
@@ -15,6 +18,12 @@ export class ImportableResources {
*/
private importables = [
{ resource: 'Account', importable: AccountsImportable },
{
resource: 'UncategorizedCashflowTransaction',
importable: UncategorizedTransactionsImportable,
},
{ resource: 'Customer', importable: CustomersImportable },
{ resource: 'Vendor', importable: VendorsImportable },
];
public get registry() {

View File

@@ -3,6 +3,17 @@ import { upperFirst, camelCase, first } from 'lodash';
import pluralize from 'pluralize';
import { ResourceMetaFieldsMap } from './interfaces';
import { IModelMetaField } from '@/interfaces';
import moment from 'moment';
export const ERRORS = {
RESOURCE_NOT_IMPORTABLE: 'RESOURCE_NOT_IMPORTABLE',
INVALID_MAP_ATTRS: 'INVALID_MAP_ATTRS',
DUPLICATED_FROM_MAP_ATTR: 'DUPLICATED_FROM_MAP_ATTR',
DUPLICATED_TO_MAP_ATTR: 'DUPLICATED_TO_MAP_ATTR',
IMPORT_FILE_NOT_MAPPED: 'IMPORT_FILE_NOT_MAPPED',
INVALID_MAP_DATE_FORMAT: 'INVALID_MAP_DATE_FORMAT',
MAP_DATE_FORMAT_NOT_DEFINED: 'MAP_DATE_FORMAT_NOT_DEFINED',
};
export function trimObject(obj) {
return Object.entries(obj).reduce((acc, [key, value]) => {
@@ -47,6 +58,18 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
return acc;
}, {});
fieldSchema = Yup.string().oneOf(Object.keys(options)).label(field.name);
// Validate date field type.
} else if (field.fieldType === 'date') {
fieldSchema = fieldSchema.test(
'date validation',
'Invalid date or format. The string should be a valid YYYY-MM-DD format.',
(val) => {
if (!val) {
return true;
}
return moment(val, 'YYYY-MM-DD', true).isValid();
}
);
}
if (field.required) {
fieldSchema = fieldSchema.required();
@@ -56,14 +79,6 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
return Yup.object().shape(yupSchema);
};
export const ERRORS = {
RESOURCE_NOT_IMPORTABLE: 'RESOURCE_NOT_IMPORTABLE',
INVALID_MAP_ATTRS: 'INVALID_MAP_ATTRS',
DUPLICATED_FROM_MAP_ATTR: 'DUPLICATED_FROM_MAP_ATTR',
DUPLICATED_TO_MAP_ATTR: 'DUPLICATED_TO_MAP_ATTR',
IMPORT_FILE_NOT_MAPPED: 'IMPORT_FILE_NOT_MAPPED',
};
export const getUnmappedSheetColumns = (columns, mapping) => {
return columns.filter(
(column) => !mapping.some((map) => map.from === column)

View File

@@ -1,8 +1,10 @@
import { IModelMetaField } from '@/interfaces';
import Import from '@/models/Import';
export interface ImportMappingAttr {
from: string;
to: string;
dateFormat?: string;
}
export interface ImportValidationError {
@@ -59,3 +61,16 @@ export interface ImportOperError {
error: ImportInsertError;
index: number;
}
export interface ImportableContext {
import: Import,
rowIndex: number;
}
export const ImportDateFormats = [
'yyyy-MM-dd',
'dd.MM.yy',
'MM/dd/yy',
'dd/MMM/yyyy'
]