Compare commits

...

9 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
7e6f1efe30 feat: Add date format controll to import 2024-08-29 19:48:58 +02:00
Ahmed Bouhuolia
c43123db76 Merge pull request #636 from bigcapitalhq/expand-export-page-size
fix: Expand the resources export page size limitation
2024-08-29 14:23:48 +02:00
Ahmed Bouhuolia
ebbcab3926 fix: Expand the resources export page size limitation 2024-08-29 14:22:45 +02:00
Ahmed Bouhuolia
a235f573c0 Merge pull request #635 from bigcapitalhq/fix-avoid-cost-job-import-preview
fix: Avoid running the cost job in import preview
2024-08-29 10:09:13 +02:00
Ahmed Bouhuolia
84a0b8f495 fix: re-schedule the jobs have date from the current moment 2024-08-29 10:05:38 +02:00
Ahmed Bouhuolia
b87321c897 fix: Avoid running the cost job in import preview 2024-08-28 22:15:15 +02:00
Ahmed Bouhuolia
fc6ebfea5c Debounce scheduling calculating items cost 2024-08-28 21:25:47 +02:00
Ahmed Bouhuolia
161d60393a Merge pull request #629 from bigcapitalhq/details-subscription
fix: Add subscription plans offer text
2024-08-25 19:44:34 +02:00
Ahmed Bouhuolia
79413fa85e fix: Add subscription plans offer text 2024-08-25 19:43:54 +02:00
39 changed files with 449 additions and 107 deletions

View File

@@ -37,6 +37,7 @@
"agendash": "^3.1.0", "agendash": "^3.1.0",
"app-root-path": "^3.0.0", "app-root-path": "^3.0.0",
"async": "^3.2.0", "async": "^3.2.0",
"async-mutex": "^0.5.0",
"axios": "^1.6.0", "axios": "^1.6.0",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",

View File

@@ -40,6 +40,7 @@ export class ImportController extends BaseController {
body('mapping.*.group').optional(), body('mapping.*.group').optional(),
body('mapping.*.from').exists(), body('mapping.*.from').exists(),
body('mapping.*.to').exists(), body('mapping.*.to').exists(),
body('mapping.*.dateFormat').optional({ nullable: true }),
], ],
this.validationResult, this.validationResult,
this.asyncMiddleware(this.mapping.bind(this)), this.asyncMiddleware(this.mapping.bind(this)),

View File

@@ -3,6 +3,6 @@ import { ImportFilePreviewPOJO } from "@/services/Import/interfaces";
export interface IImportFileCommitedEventPayload { export interface IImportFileCommitedEventPayload {
tenantId: number; tenantId: number;
importId: number; importId: string;
meta: ImportFilePreviewPOJO; meta: ImportFilePreviewPOJO;
} }

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { AccountsApplication } from './AccountsApplication'; import { AccountsApplication } from './AccountsApplication';
import { Exportable } from '../Export/Exportable'; import { Exportable } from '../Export/Exportable';
import { IAccountsFilter, IAccountsStructureType } from '@/interfaces'; import { IAccountsFilter, IAccountsStructureType } from '@/interfaces';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service() @Service()
export class AccountsExportable extends Exportable { export class AccountsExportable extends Exportable {
@@ -20,7 +21,7 @@ export class AccountsExportable extends Exportable {
inactiveMode: false, inactiveMode: false,
...query, ...query,
structure: IAccountsStructureType.Flat, structure: IAccountsStructureType.Flat,
pageSize: 12000, pageSize: EXPORT_SIZE_LIMIT,
page: 1, page: 1,
} as IAccountsFilter; } as IAccountsFilter;

View File

@@ -100,7 +100,6 @@ export class TriggerRecognizedTransactions {
private async triggerRecognizeTransactionsOnImportCommitted({ private async triggerRecognizeTransactionsOnImportCommitted({
tenantId, tenantId,
importId, importId,
meta,
}: IImportFileCommitedEventPayload) { }: IImportFileCommitedEventPayload) {
const importFile = await Import.query().findOne({ importId }); const importFile = await Import.query().findOne({ importId });
const batch = importFile.paramsParsed.batch; const batch = importFile.paramsParsed.batch;

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IItemsFilter } from '@/interfaces'; import { IItemsFilter } from '@/interfaces';
import { CustomersApplication } from './CustomersApplication'; import { CustomersApplication } from './CustomersApplication';
import { Exportable } from '@/services/Export/Exportable'; import { Exportable } from '@/services/Export/Exportable';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service() @Service()
export class CustomersExportable extends Exportable { export class CustomersExportable extends Exportable {
@@ -17,9 +18,9 @@ export class CustomersExportable extends Exportable {
const parsedQuery = { const parsedQuery = {
sortOrder: 'DESC', sortOrder: 'DESC',
columnSortBy: 'created_at', columnSortBy: 'created_at',
page: 1,
...query, ...query,
pageSize: 12, page: 1,
pageSize: EXPORT_SIZE_LIMIT,
} as IItemsFilter; } as IItemsFilter;
return this.customersApplication return this.customersApplication

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IItemsFilter } from '@/interfaces'; import { IItemsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable'; import { Exportable } from '@/services/Export/Exportable';
import { VendorsApplication } from './VendorsApplication'; import { VendorsApplication } from './VendorsApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service() @Service()
export class VendorsExportable extends Exportable { export class VendorsExportable extends Exportable {
@@ -17,9 +18,9 @@ export class VendorsExportable extends Exportable {
const parsedQuery = { const parsedQuery = {
sortOrder: 'DESC', sortOrder: 'DESC',
columnSortBy: 'created_at', columnSortBy: 'created_at',
page: 1,
...query, ...query,
pageSize: 12, page: 1,
pageSize: EXPORT_SIZE_LIMIT,
} as IItemsFilter; } as IItemsFilter;
return this.vendorsApplication return this.vendorsApplication

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { Exportable } from '../Export/Exportable'; import { Exportable } from '../Export/Exportable';
import { IExpensesFilter } from '@/interfaces'; import { IExpensesFilter } from '@/interfaces';
import { ExpensesApplication } from './ExpensesApplication'; import { ExpensesApplication } from './ExpensesApplication';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service() @Service()
export class ExpensesExportable extends Exportable { export class ExpensesExportable extends Exportable {
@@ -19,7 +20,7 @@ export class ExpensesExportable extends Exportable {
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: 12000, pageSize: EXPORT_SIZE_LIMIT,
} as IExpensesFilter; } as IExpensesFilter;
return this.expensesApplication return this.expensesApplication

View File

@@ -0,0 +1 @@
export const EXPORT_SIZE_LIMIT = 9999999;

View File

@@ -0,0 +1,104 @@
import { Service } from 'typedi';
import { AsyncLocalStorage } from 'async_hooks';
@Service()
export class ImportAls {
private als: AsyncLocalStorage<Map<string, any>>;
constructor() {
this.als = new AsyncLocalStorage();
}
/**
* Runs a callback function within the context of a new AsyncLocalStorage store.
* @param callback The function to be executed within the AsyncLocalStorage context.
* @returns The result of the callback function.
*/
public run<T>(callback: () => T): T {
return this.als.run<T>(new Map(), callback);
}
/**
* Runs a callback function in preview mode within the AsyncLocalStorage context.
* @param callback The function to be executed in preview mode.
* @returns The result of the callback function.
*/
public runPreview<T>(callback: () => T): T {
return this.run(() => {
this.markAsImport();
this.markAsImportPreview();
return callback();
});
}
/**
* Runs a callback function in commit mode within the AsyncLocalStorage context.
* @param {() => T} callback - The function to be executed in commit mode.
* @returns {T} The result of the callback function.
*/
public runCommit<T>(callback: () => T): T {
return this.run(() => {
this.markAsImport();
this.markAsImportCommit();
return callback();
});
}
/**
* Retrieves the current AsyncLocalStorage store.
* @returns The current store or undefined if not in a valid context.
*/
public getStore(): Map<string, any> | undefined {
return this.als.getStore();
}
/**
* Marks the current context as an import operation.
* @param flag Boolean flag to set or unset the import status. Defaults to true.
*/
public markAsImport(flag: boolean = true): void {
const store = this.getStore();
store?.set('isImport', flag);
}
/**
* Marks the current context as an import commit operation.
* @param flag Boolean flag to set or unset the import commit status. Defaults to true.
*/
public markAsImportCommit(flag: boolean = true): void {
const store = this.getStore();
store?.set('isImportCommit', flag);
}
/**
* Marks the current context as an import preview operation.
* @param {boolean} flag - Boolean flag to set or unset the import preview status. Defaults to true.
*/
public markAsImportPreview(flag: boolean = true): void {
const store = this.getStore();
store?.set('isImportPreview', flag);
}
/**
* Checks if the current context is an import operation.
* @returns {boolean} True if the context is an import operation, false otherwise.
*/
public isImport(): boolean {
return !!this.getStore()?.get('isImport');
}
/**
* Checks if the current context is an import commit operation.
* @returns {boolean} True if the context is an import commit operation, false otherwise.
*/
public isImportCommit(): boolean {
return !!this.getStore()?.get('isImportCommit');
}
/**
* Checks if the current context is an import preview operation.
* @returns {boolean} True if the context is an import preview operation, false otherwise.
*/
public isImportPreview(): boolean {
return !!this.getStore()?.get('isImportPreview');
}
}

View File

@@ -12,7 +12,11 @@ import {
ImportableContext, ImportableContext,
} from './interfaces'; } from './interfaces';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { getUniqueImportableValue, trimObject } from './_utils'; import {
convertMappingsToObject,
getUniqueImportableValue,
trimObject,
} from './_utils';
import { ImportableResources } from './ImportableResources'; import { ImportableResources } from './ImportableResources';
import ResourceService from '../Resource/ResourceService'; import ResourceService from '../Resource/ResourceService';
import { Import } from '@/system/models'; import { Import } from '@/system/models';
@@ -43,7 +47,6 @@ export class ImportFileCommon {
return XLSX.utils.sheet_to_json(worksheet, {}); return XLSX.utils.sheet_to_json(worksheet, {});
} }
/** /**
* 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 {number} tenantId -
@@ -82,11 +85,14 @@ export class ImportFileCommon {
rowNumber, rowNumber,
uniqueValue, uniqueValue,
}; };
const mappingSettings = convertMappingsToObject(importFile.columnsParsed);
try { try {
// Validate the DTO object before passing it to the service layer. // Validate the DTO object before passing it to the service layer.
await this.importFileValidator.validateData( await this.importFileValidator.validateData(
resourceFields, resourceFields,
transformedDTO transformedDTO,
mappingSettings
); );
try { try {
// Run the importable function and listen to the errors. // Run the importable function and listen to the errors.

View File

@@ -1,5 +1,9 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { ImportInsertError, ResourceMetaFieldsMap } from './interfaces'; import {
ImportInsertError,
ImportMappingAttr,
ResourceMetaFieldsMap,
} from './interfaces';
import { ERRORS, convertFieldsToYupValidation } from './_utils'; import { ERRORS, convertFieldsToYupValidation } from './_utils';
import { IModelMeta } from '@/interfaces'; import { IModelMeta } from '@/interfaces';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
@@ -24,9 +28,13 @@ export class ImportFileDataValidator {
*/ */
public async validateData( public async validateData(
importableFields: ResourceMetaFieldsMap, importableFields: ResourceMetaFieldsMap,
data: Record<string, any> data: Record<string, any>,
mappingSettings: Record<string, ImportMappingAttr> = {}
): Promise<void | ImportInsertError[]> { ): Promise<void | ImportInsertError[]> {
const YupSchema = convertFieldsToYupValidation(importableFields); const YupSchema = convertFieldsToYupValidation(
importableFields,
mappingSettings
);
const _data = { ...data }; const _data = { ...data };
try { try {

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService'; import HasTenancyService from '../Tenancy/TenancyService';
import { ImportFilePreviewPOJO } from './interfaces'; import { ImportFilePreviewPOJO } from './interfaces';
import { ImportFileProcess } from './ImportFileProcess'; import { ImportFileProcess } from './ImportFileProcess';
import { ImportAls } from './ImportALS';
@Service() @Service()
export class ImportFilePreview { export class ImportFilePreview {
@@ -11,13 +12,31 @@ export class ImportFilePreview {
@Inject() @Inject()
private importFile: ImportFileProcess; private importFile: ImportFileProcess;
@Inject()
private importAls: ImportAls;
/**
* Preview the imported file results before commiting the transactions.
* @param {number} tenantId -
* @param {string} importId -
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async preview(
tenantId: number,
importId: string
): Promise<ImportFilePreviewPOJO> {
return this.importAls.runPreview<Promise<ImportFilePreviewPOJO>>(() =>
this.previewAlsRun(tenantId, importId)
);
}
/** /**
* Preview the imported file results before commiting the transactions. * Preview the imported file results before commiting the transactions.
* @param {number} tenantId * @param {number} tenantId
* @param {number} importId * @param {number} importId
* @returns {Promise<ImportFilePreviewPOJO>} * @returns {Promise<ImportFilePreviewPOJO>}
*/ */
public async preview( public async previewAlsRun(
tenantId: number, tenantId: number,
importId: string importId: string
): Promise<ImportFilePreviewPOJO> { ): Promise<ImportFilePreviewPOJO> {

View File

@@ -5,6 +5,7 @@ import { ImportFileProcess } from './ImportFileProcess';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { IImportFileCommitedEventPayload } from '@/interfaces/Import'; import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
import { ImportAls } from './ImportALS';
@Service() @Service()
export class ImportFileProcessCommit { export class ImportFileProcessCommit {
@@ -14,16 +15,34 @@ export class ImportFileProcessCommit {
@Inject() @Inject()
private importFile: ImportFileProcess; private importFile: ImportFileProcess;
@Inject()
private importAls: ImportAls;
@Inject() @Inject()
private eventPublisher: EventPublisher; private eventPublisher: EventPublisher;
/**
* Commits the imported file under ALS.
* @param {number} tenantId
* @param {string} importId
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public commit(
tenantId: number,
importId: string
): Promise<ImportFilePreviewPOJO> {
return this.importAls.runCommit<Promise<ImportFilePreviewPOJO>>(() =>
this.commitAlsRun(tenantId, importId)
);
}
/** /**
* Commits the imported file. * Commits the imported file.
* @param {number} tenantId * @param {number} tenantId
* @param {number} importId * @param {number} importId
* @returns {Promise<ImportFilePreviewPOJO>} * @returns {Promise<ImportFilePreviewPOJO>}
*/ */
public async commit( public async commitAlsRun(
tenantId: number, tenantId: number,
importId: string importId: string
): Promise<ImportFilePreviewPOJO> { ): Promise<ImportFilePreviewPOJO> {

View File

@@ -17,9 +17,10 @@ import {
head, head,
split, split,
last, last,
set,
} from 'lodash'; } from 'lodash';
import pluralize from 'pluralize'; import pluralize from 'pluralize';
import { ResourceMetaFieldsMap } from './interfaces'; import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
import { IModelMetaField, IModelMetaField2 } from '@/interfaces'; import { IModelMetaField, IModelMetaField2 } from '@/interfaces';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { multiNumberParse } from '@/utils/multi-number-parse'; import { multiNumberParse } from '@/utils/multi-number-parse';
@@ -58,11 +59,19 @@ export function trimObject(obj: Record<string, string | number>) {
* @param {ResourceMetaFieldsMap} fields * @param {ResourceMetaFieldsMap} fields
* @returns {Yup} * @returns {Yup}
*/ */
export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => { export const convertFieldsToYupValidation = (
fields: ResourceMetaFieldsMap,
parentFieldName: string = '',
mappingSettings: Record<string, ImportMappingAttr> = {}
) => {
const yupSchema = {}; const yupSchema = {};
Object.keys(fields).forEach((fieldName: string) => { Object.keys(fields).forEach((fieldName: string) => {
const field = fields[fieldName] as IModelMetaField; const field = fields[fieldName] as IModelMetaField;
const fieldPath = parentFieldName
? `${parentFieldName}.${fieldName}`
: fieldName;
let fieldSchema; let fieldSchema;
fieldSchema = Yup.string().label(field.name); fieldSchema = Yup.string().label(field.name);
@@ -105,13 +114,23 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
if (!val) { if (!val) {
return true; return true;
} }
return moment(val, 'YYYY-MM-DD', true).isValid(); const fieldDateFormat =
(get(
mappingSettings,
`${fieldPath}.dateFormat`
) as unknown as string) || 'YYYY-MM-DD';
return moment(val, fieldDateFormat, true).isValid();
} }
); );
} else if (field.fieldType === 'url') { } else if (field.fieldType === 'url') {
fieldSchema = fieldSchema.url(); fieldSchema = fieldSchema.url();
} else if (field.fieldType === 'collection') { } else if (field.fieldType === 'collection') {
const nestedFieldShema = convertFieldsToYupValidation(field.fields); const nestedFieldShema = convertFieldsToYupValidation(
field.fields,
field.name,
mappingSettings
);
fieldSchema = Yup.array().label(field.name); fieldSchema = Yup.array().label(field.name);
if (!isUndefined(field.collectionMaxLength)) { if (!isUndefined(field.collectionMaxLength)) {
@@ -258,6 +277,7 @@ export const getResourceColumns = (resourceColumns: {
]) => { ]) => {
const extra: Record<string, any> = {}; const extra: Record<string, any> = {};
const key = fieldKey; const key = fieldKey;
const type = field.fieldType;
if (group) { if (group) {
extra.group = group; extra.group = group;
@@ -270,6 +290,7 @@ export const getResourceColumns = (resourceColumns: {
name, name,
required, required,
hint: importHint, hint: importHint,
type,
order, order,
...extra, ...extra,
}; };
@@ -322,6 +343,8 @@ export const valueParser =
}); });
const result = await relationQuery.first(); const result = await relationQuery.first();
_value = get(result, 'id'); _value = get(result, 'id');
} else if (field.fieldType === 'date') {
} else if (field.fieldType === 'collection') { } else if (field.fieldType === 'collection') {
const ObjectFieldKey = key.includes('.') ? key.split('.')[1] : key; const ObjectFieldKey = key.includes('.') ? key.split('.')[1] : key;
const _valueParser = valueParser(fields, tenantModels); const _valueParser = valueParser(fields, tenantModels);
@@ -433,8 +456,8 @@ export const getMapToPath = (to: string, group = '') =>
group ? `${group}.${to}` : to; group ? `${group}.${to}` : to;
export const getImportsStoragePath = () => { export const getImportsStoragePath = () => {
return path.join(global.__storage_dir, `/imports`); return path.join(global.__storage_dir, `/imports`);
} };
/** /**
* Deletes the imported file from the storage and database. * Deletes the imported file from the storage and database.
@@ -457,3 +480,19 @@ export const readImportFile = (filename: string) => {
return fs.readFile(`${filePath}/${filename}`); return fs.readFile(`${filePath}/${filename}`);
}; };
/**
* Converts an array of mapping objects to a structured object.
* @param {Array<Object>} mappings - Array of mapping objects.
* @returns {Object} - Structured object based on the mappings.
*/
export const convertMappingsToObject = (mappings) => {
return mappings.reduce((acc, mapping) => {
const { to, group } = mapping;
const key = group ? `['${group}.${to}']` : to;
set(acc, key, mapping);
return acc;
}, {});
};

View File

@@ -139,24 +139,25 @@ export default class InventoryService {
) { ) {
const agenda = Container.get('agenda'); const agenda = Container.get('agenda');
const commonJobsQuery = {
name: 'compute-item-cost',
lastRunAt: { $exists: false },
'data.tenantId': tenantId,
'data.itemId': itemId,
};
// Cancel any `compute-item-cost` in the queue has upper starting date // Cancel any `compute-item-cost` in the queue has upper starting date
// with the same given item. // with the same given item.
await agenda.cancel({ await agenda.cancel({
name: 'compute-item-cost', ...commonJobsQuery,
nextRunAt: { $ne: null }, 'data.startingDate': { $lte: startingDate },
'data.tenantId': tenantId,
'data.itemId': itemId,
'data.startingDate': { $gt: startingDate },
}); });
// Retrieve any `compute-item-cost` in the queue has lower starting date // Retrieve any `compute-item-cost` in the queue has lower starting date
// with the same given item. // with the same given item.
const dependsJobs = await agenda.jobs({ const dependsJobs = await agenda.jobs({
name: 'compute-item-cost', ...commonJobsQuery,
nextRunAt: { $ne: null }, 'data.startingDate': { $gte: startingDate },
'data.tenantId': tenantId,
'data.itemId': itemId,
'data.startingDate': { $lte: startingDate },
}); });
// If the depends jobs cleared.
if (dependsJobs.length === 0) { if (dependsJobs.length === 0) {
await agenda.schedule( await agenda.schedule(
config.scheduleComputeItemCost, config.scheduleComputeItemCost,
@@ -172,6 +173,13 @@ export default class InventoryService {
events.inventory.onComputeItemCostJobScheduled, events.inventory.onComputeItemCostJobScheduled,
{ startingDate, itemId, tenantId } as IInventoryItemCostScheduledPayload { startingDate, itemId, tenantId } as IInventoryItemCostScheduledPayload
); );
} else {
// Re-schedule the jobs that have higher date from current moment.
await Promise.all(
dependsJobs.map((job) =>
job.schedule(config.scheduleComputeItemCost).save()
)
);
} }
} }

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { Exportable } from '../Export/Exportable'; import { Exportable } from '../Export/Exportable';
import { IItemsFilter } from '@/interfaces'; import { IItemsFilter } from '@/interfaces';
import { ItemsApplication } from './ItemsApplication'; import { ItemsApplication } from './ItemsApplication';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service() @Service()
export class ItemsExportable extends Exportable { export class ItemsExportable extends Exportable {
@@ -19,7 +20,7 @@ export class ItemsExportable extends Exportable {
columnSortBy: 'created_at', columnSortBy: 'created_at',
page: 1, page: 1,
...query, ...query,
pageSize: 12, pageSize: EXPORT_SIZE_LIMIT,
} as IItemsFilter; } as IItemsFilter;
return this.itemsApplication return this.itemsApplication

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IManualJournalsFilter } from '@/interfaces'; import { IManualJournalsFilter } from '@/interfaces';
import { Exportable } from '../Export/Exportable'; import { Exportable } from '../Export/Exportable';
import { ManualJournalsApplication } from './ManualJournalsApplication'; import { ManualJournalsApplication } from './ManualJournalsApplication';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service() @Service()
export class ManualJournalsExportable extends Exportable { export class ManualJournalsExportable extends Exportable {
@@ -19,7 +20,7 @@ export class ManualJournalsExportable extends Exportable {
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: 12000, pageSize: EXPORT_SIZE_LIMIT,
} as IManualJournalsFilter; } as IManualJournalsFilter;
return this.manualJournalsApplication return this.manualJournalsApplication

View File

@@ -1,6 +1,7 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { Exportable } from '@/services/Export/Exportable'; import { Exportable } from '@/services/Export/Exportable';
import { BillPaymentsApplication } from './BillPaymentsApplication'; import { BillPaymentsApplication } from './BillPaymentsApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service() @Service()
export class BillPaymentExportable extends Exportable { export class BillPaymentExportable extends Exportable {
@@ -14,11 +15,11 @@ export class BillPaymentExportable extends Exportable {
*/ */
public exportable(tenantId: number, query: any) { public exportable(tenantId: number, query: any) {
const parsedQuery = { const parsedQuery = {
page: 1,
pageSize: 12,
...query,
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
} as any; } as any;
return this.billPaymentsApplication return this.billPaymentsApplication

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IBillsFilter } from '@/interfaces'; import { IBillsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable'; import { Exportable } from '@/services/Export/Exportable';
import { BillsApplication } from './BillsApplication'; import { BillsApplication } from './BillsApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service() @Service()
export class BillsExportable extends Exportable { export class BillsExportable extends Exportable {
@@ -19,7 +20,7 @@ export class BillsExportable extends Exportable {
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: 12000, pageSize: EXPORT_SIZE_LIMIT,
} as IBillsFilter; } as IBillsFilter;
return this.billsApplication return this.billsApplication

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { ISalesInvoicesFilter } from '@/interfaces'; import { ISalesInvoicesFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable'; import { Exportable } from '@/services/Export/Exportable';
import { SaleEstimatesApplication } from './SaleEstimatesApplication'; import { SaleEstimatesApplication } from './SaleEstimatesApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service() @Service()
export class SaleEstimatesExportable extends Exportable { export class SaleEstimatesExportable extends Exportable {
@@ -19,7 +20,7 @@ export class SaleEstimatesExportable extends Exportable {
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: 12000, pageSize: EXPORT_SIZE_LIMIT,
} as ISalesInvoicesFilter; } as ISalesInvoicesFilter;
return this.saleEstimatesApplication return this.saleEstimatesApplication

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { ISalesInvoicesFilter } from '@/interfaces'; import { ISalesInvoicesFilter } from '@/interfaces';
import { SaleInvoiceApplication } from './SaleInvoicesApplication'; import { SaleInvoiceApplication } from './SaleInvoicesApplication';
import { Exportable } from '@/services/Export/Exportable'; import { Exportable } from '@/services/Export/Exportable';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service() @Service()
export class SaleInvoicesExportable extends Exportable { export class SaleInvoicesExportable extends Exportable {
@@ -19,7 +20,7 @@ export class SaleInvoicesExportable extends Exportable {
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: 120000, pageSize: EXPORT_SIZE_LIMIT,
} as ISalesInvoicesFilter; } as ISalesInvoicesFilter;
return this.saleInvoicesApplication return this.saleInvoicesApplication

View File

@@ -1,3 +1,4 @@
import { Mutex } from 'async-mutex';
import { Container, Service, Inject } from 'typedi'; import { Container, Service, Inject } from 'typedi';
import { chain } from 'lodash'; import { chain } from 'lodash';
import moment from 'moment'; import moment from 'moment';
@@ -34,17 +35,26 @@ export class SaleInvoicesCost {
inventoryItemsIds: number[], inventoryItemsIds: number[],
startingDate: Date startingDate: Date
): Promise<void> { ): Promise<void> {
const asyncOpers: Promise<[]>[] = []; const mutex = new Mutex();
inventoryItemsIds.forEach((inventoryItemId: number) => { const asyncOpers = inventoryItemsIds.map(
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost( async (inventoryItemId: number) => {
tenantId, // @todo refactor the lock acquire to be distrbuted using Redis
inventoryItemId, // and run the cost schedule job after running invoice transaction.
startingDate const release = await mutex.acquire();
);
asyncOpers.push(oper); try {
}); await this.inventoryService.scheduleComputeItemCost(
await Promise.all([...asyncOpers]); tenantId,
inventoryItemId,
startingDate
);
} finally {
release();
}
}
);
await Promise.all(asyncOpers);
} }
/** /**
@@ -86,17 +96,22 @@ export class SaleInvoicesCost {
tenantId: number, tenantId: number,
inventoryTransactions: IInventoryTransaction[] inventoryTransactions: IInventoryTransaction[]
) { ) {
const asyncOpers: Promise<[]>[] = []; const mutex = new Mutex();
const reducedTransactions = this.getMaxDateInventoryTransactions( const reducedTransactions = this.getMaxDateInventoryTransactions(
inventoryTransactions inventoryTransactions
); );
reducedTransactions.forEach((transaction) => { const asyncOpers = reducedTransactions.map(async (transaction) => {
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost( const release = await mutex.acquire();
tenantId,
transaction.itemId, try {
transaction.date await this.inventoryService.scheduleComputeItemCost(
); tenantId,
asyncOpers.push(oper); transaction.itemId,
transaction.date
);
} finally {
release();
}
}); });
await Promise.all([...asyncOpers]); await Promise.all([...asyncOpers]);
} }

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IAccountsStructureType, IPaymentsReceivedFilter } from '@/interfaces'; import { IAccountsStructureType, IPaymentsReceivedFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable'; import { Exportable } from '@/services/Export/Exportable';
import { PaymentReceivesApplication } from './PaymentReceivedApplication'; import { PaymentReceivesApplication } from './PaymentReceivedApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service() @Service()
export class PaymentsReceivedExportable extends Exportable { export class PaymentsReceivedExportable extends Exportable {
@@ -21,6 +22,8 @@ export class PaymentsReceivedExportable extends Exportable {
inactiveMode: false, inactiveMode: false,
...query, ...query,
structure: IAccountsStructureType.Flat, structure: IAccountsStructureType.Flat,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
} as IPaymentsReceivedFilter; } as IPaymentsReceivedFilter;
return this.paymentReceivedApp return this.paymentReceivedApp

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { ISalesReceiptsFilter } from '@/interfaces'; import { ISalesReceiptsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable'; import { Exportable } from '@/services/Export/Exportable';
import { SaleReceiptApplication } from './SaleReceiptApplication'; import { SaleReceiptApplication } from './SaleReceiptApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service() @Service()
export class SaleReceiptsExportable extends Exportable { export class SaleReceiptsExportable extends Exportable {
@@ -19,7 +20,7 @@ export class SaleReceiptsExportable extends Exportable {
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: 12, pageSize: EXPORT_SIZE_LIMIT,
} as ISalesReceiptsFilter; } as ISalesReceiptsFilter;
return this.saleReceiptsApp return this.saleReceiptsApp

View File

@@ -10,6 +10,7 @@ import {
} from '@/interfaces'; } from '@/interfaces';
import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks'; import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks';
import { SaleInvoicesCost } from '@/services/Sales/Invoices/SalesInvoicesCost'; import { SaleInvoicesCost } from '@/services/Sales/Invoices/SalesInvoicesCost';
import { ImportAls } from '@/services/Import/ImportALS';
@Service() @Service()
export default class InventorySubscriber { export default class InventorySubscriber {
@@ -25,6 +26,9 @@ export default class InventorySubscriber {
@Inject('agenda') @Inject('agenda')
private agenda: any; private agenda: any;
@Inject()
private importAls: ImportAls;
/** /**
* Attaches events with handlers. * Attaches events with handlers.
*/ */
@@ -86,20 +90,17 @@ export default class InventorySubscriber {
private handleScheduleItemsCostOnInventoryTransactionsCreated = async ({ private handleScheduleItemsCostOnInventoryTransactionsCreated = async ({
tenantId, tenantId,
inventoryTransactions, inventoryTransactions,
trx trx,
}: IInventoryTransactionsCreatedPayload) => { }: IInventoryTransactionsCreatedPayload) => {
const inventoryItemsIds = map(inventoryTransactions, 'itemId'); const inImportPreviewScope = this.importAls.isImportPreview();
runAfterTransaction(trx, async () => { // Avoid running the cost items job if the async process is in import preview.
try { if (inImportPreviewScope) return;
await this.saleInvoicesCost.computeItemsCostByInventoryTransactions(
tenantId, await this.saleInvoicesCost.computeItemsCostByInventoryTransactions(
inventoryTransactions tenantId,
); inventoryTransactions
} catch (error) { );
console.error(error);
}
});
}; };
/** /**

View File

@@ -14,7 +14,7 @@
th.label, th.label,
td.label{ td.label{
width: 32% !important; width: 30% !important;
} }
thead{ thead{
@@ -31,16 +31,14 @@
tr td { tr td {
vertical-align: middle; vertical-align: middle;
} }
tr td{
:global(.bp4-popover-target .bp4-button),
:global(.bp4-popover-wrapper){
max-width: 250px;
}
}
} }
} }
.requiredSign{ .requiredSign{
color: rgb(250, 82, 82); color: rgb(250, 82, 82);
}
.columnSelectButton{
max-width: 250px;
min-width: 250px;
} }

View File

@@ -8,9 +8,12 @@ import { EntityColumnField, useImportFileContext } from './ImportFileProvider';
import { CLASSES } from '@/constants'; import { CLASSES } from '@/constants';
import { ImportFileContainer } from './ImportFileContainer'; import { ImportFileContainer } from './ImportFileContainer';
import { ImportStepperStep } from './_types'; import { ImportStepperStep } from './_types';
import { ImportFileMapBootProvider } from './ImportFileMappingBoot'; import {
ImportFileMapBootProvider,
useImportFileMapBootContext,
} from './ImportFileMappingBoot';
import styles from './ImportFileMapping.module.scss'; import styles from './ImportFileMapping.module.scss';
import { getFieldKey } from './_utils'; import { getDateFieldKey, getFieldKey } from './_utils';
export function ImportFileMapping() { export function ImportFileMapping() {
const { importId, entityColumns } = useImportFileContext(); const { importId, entityColumns } = useImportFileContext();
@@ -82,6 +85,7 @@ interface ImportFileMappingFieldsProps {
*/ */
function ImportFileMappingFields({ fields }: ImportFileMappingFieldsProps) { function ImportFileMappingFields({ fields }: ImportFileMappingFieldsProps) {
const { sheetColumns } = useImportFileContext(); const { sheetColumns } = useImportFileContext();
const { dateFormats } = useImportFileMapBootContext();
const items = useMemo( const items = useMemo(
() => sheetColumns.map((column) => ({ value: column, text: column })), () => sheetColumns.map((column) => ({ value: column, text: column })),
@@ -95,22 +99,35 @@ function ImportFileMappingFields({ fields }: ImportFileMappingFieldsProps) {
{column.required && <span className={styles.requiredSign}>*</span>} {column.required && <span className={styles.requiredSign}>*</span>}
</td> </td>
<td className={styles.field}> <td className={styles.field}>
<Group spacing={4}> <Group spacing={12} noWrap>
<FSelect <FSelect
name={getFieldKey(column.key, column.group)} name={`['${getFieldKey(column.key, column.group)}'].from`}
items={items} items={items}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
minimal={true} minimal={true}
fill={true} fill={true}
className={styles.columnSelectButton}
/> />
{column.hint && ( {column.hint && (
<Hint content={column.hint} position={Position.BOTTOM} /> <Hint content={column.hint} position={Position.BOTTOM} />
)} )}
{column.type === 'date' && (
<FSelect
name={getDateFieldKey(column.key, column.group)}
items={dateFormats}
placeholder={'Select date format'}
minimal={true}
fill={true}
valueAccessor={'key'}
textAccessor={'label'}
labelAccessor={''}
/>
)}
</Group> </Group>
</td> </td>
</tr> </tr>
), ),
[items], [items, dateFormats],
); );
const columns = useMemo( const columns = useMemo(
() => fields.map(columnMapper), () => fields.map(columnMapper),

View File

@@ -2,8 +2,11 @@ import { Spinner } from '@blueprintjs/core';
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import { Box } from '@/components'; import { Box } from '@/components';
import { useImportFileMeta } from '@/hooks/query/import'; import { useImportFileMeta } from '@/hooks/query/import';
import { useDateFormats } from '@/hooks/query';
interface ImportFileMapBootContextValue {} interface ImportFileMapBootContextValue {
dateFormats: Array<any>;
}
const ImportFileMapBootContext = createContext<ImportFileMapBootContextValue>( const ImportFileMapBootContext = createContext<ImportFileMapBootContextValue>(
{} as ImportFileMapBootContextValue, {} as ImportFileMapBootContextValue,
@@ -39,14 +42,22 @@ export const ImportFileMapBootProvider = ({
enabled: Boolean(importId), enabled: Boolean(importId),
}); });
// Fetch date format options.
const { data: dateFormats, isLoading: isDateFormatsLoading } =
useDateFormats();
const value = { const value = {
importFile, importFile,
isImportFileLoading, isImportFileLoading,
isImportFileFetching, isImportFileFetching,
dateFormats,
isDateFormatsLoading,
}; };
const isLoading = isDateFormatsLoading || isImportFileLoading;
return ( return (
<ImportFileMapBootContext.Provider value={value}> <ImportFileMapBootContext.Provider value={value}>
{isImportFileLoading ? ( {isLoading ? (
<Box style={{ padding: '2rem', textAlign: 'center' }}> <Box style={{ padding: '2rem', textAlign: 'center' }}>
<Spinner size={26} /> <Spinner size={26} />
</Box> </Box>

View File

@@ -13,6 +13,7 @@ export type EntityColumnField = {
required?: boolean; required?: boolean;
hint?: string; hint?: string;
group?: string; group?: string;
type: string;
}; };
export interface EntityColumn { export interface EntityColumn {

View File

@@ -6,8 +6,8 @@
flex: 1; flex: 1;
padding: 32px 20px; padding: 32px 20px;
padding-bottom: 80px; padding-bottom: 80px;
min-width: 660px; min-width: 800px;
max-width: 760px; max-width: 800px;
width: 75%; width: 75%;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;

View File

@@ -12,4 +12,13 @@ export interface ImportFileMappingFormProps {
children: React.ReactNode; children: React.ReactNode;
} }
export type ImportFileMappingFormValues = Record<string, string | null>; export type ImportFileMappingFormValues = Record<
string,
{ from: string | null; dateFormat?: string }
>;
export type ImportFileMappingRes = {
from: string;
to: string;
group: string;
}[];

View File

@@ -17,13 +17,15 @@ import {
} from './ImportFileProvider'; } from './ImportFileProvider';
import { useImportFileMapBootContext } from './ImportFileMappingBoot'; import { useImportFileMapBootContext } from './ImportFileMappingBoot';
import { deepdash, transformToForm } from '@/utils'; import { deepdash, transformToForm } from '@/utils';
import { ImportFileMappingFormValues } from './_types'; import { ImportFileMappingFormValues, ImportFileMappingRes } from './_types';
export const getFieldKey = (key: string, group = '') => { export const getFieldKey = (key: string, group = '') => {
return group ? `${group}.${key}` : key; return group ? `${group}.${key}` : key;
}; };
type ImportFileMappingRes = { from: string; to: string; group: string }[]; export const getDateFieldKey = (key: string, group: string = '') => {
return `${getFieldKey(key, group)}.dateFormat`;
};
/** /**
* Transformes the mapping form values to request. * Transformes the mapping form values to request.
@@ -34,10 +36,10 @@ export const transformValueToReq = (
value: ImportFileMappingFormValues, value: ImportFileMappingFormValues,
): { mapping: ImportFileMappingRes[] } => { ): { mapping: ImportFileMappingRes[] } => {
const mapping = chain(value) const mapping = chain(value)
.thru(deepdash.index) .pickBy((_value, key) => !isEmpty(_value) && _value?.from)
.pickBy((_value, key) => !isEmpty(get(value, key))) .map((_value, key) => ({
.map((from, key) => ({ from: _value.from,
from, dateFormat: _value.dateFormat,
to: key.includes('.') ? last(key.split('.')) : key, to: key.includes('.') ? last(key.split('.')) : key,
group: key.includes('.') ? head(key.split('.')) : '', group: key.includes('.') ? head(key.split('.')) : '',
})) }))
@@ -52,19 +54,23 @@ export const transformValueToReq = (
* @returns {Record<string, object | string>} * @returns {Record<string, object | string>}
*/ */
export const transformResToFormValues = ( export const transformResToFormValues = (
value: { from: string; to: string , group: string }[], value: { from: string; to: string; group: string }[],
): Record<string, object | string> => { ): Record<string, object | string> => {
return value?.reduce((acc, map) => { return value?.reduce((acc, map) => {
const path = map?.group ? `${map.group}.${map.to}` : map.to; const path = map?.group ? `['${map.group}.${map.to}']` : map.to;
set(acc, path, map.from); const dateFormatObj = map?.dateFormat
? { dateFormat: map?.dateFormat }
: {};
set(acc, path, { from: map?.from, ...dateFormatObj });
return acc; return acc;
}, {}); }, {});
}; };
/** /**
* Retrieves the initial values of mapping form. * Retrieves the initial values of mapping form.
* @param {EntityColumn[]} entityColumns * @param {EntityColumn[]} entityColumns
* @param {SheetColumn[]} sheetColumns * @param {SheetColumn[]} sheetColumns
*/ */
const getInitialDefaultValues = ( const getInitialDefaultValues = (
entityColumns: EntityColumn[], entityColumns: EntityColumn[],
@@ -76,10 +82,10 @@ const getInitialDefaultValues = (
const _matched = sheetColumns.find( const _matched = sheetColumns.find(
(column) => lowerCase(column) === _name, (column) => lowerCase(column) === _name,
); );
const _key = groupKey ? `${groupKey}.${key}` : key; const path = groupKey ? `['${groupKey}.${key}']` : key;
const _value = _matched ? _matched : ''; const from = _matched ? _matched : '';
set(acc, _key, _value); set(acc, path, { from });
}); });
return acc; return acc;
}, {}); }, {});
@@ -102,7 +108,6 @@ export const useImportFileMappingInitialValues = () => {
() => getInitialDefaultValues(entityColumns, sheetColumns), () => getInitialDefaultValues(entityColumns, sheetColumns),
[entityColumns, sheetColumns], [entityColumns, sheetColumns],
); );
return useMemo<Record<string, any>>( return useMemo<Record<string, any>>(
() => () =>
assign( assign(

View File

@@ -0,0 +1,20 @@
.container {
text-align: center;
margin-bottom: 1.15rem;
}
.iconText {
display: inline-flex;
font-size: 14px;
margin-right: 16px;
color: #00824d;
&:last-child {
margin-right: 0; /* Remove the margin on the last item */
}
}
.icon {
margin-right: 2px;
}

View File

@@ -0,0 +1,35 @@
import styles from './SubscriptionPlansOfferChecks.module.scss';
export function SubscriptionPlansOfferChecks() {
return (
<div className={styles.container}>
<span className={styles.iconText}>
<svg
xmlns="http://www.w3.org/2000/svg"
height="18px"
width="18px"
viewBox="0 -960 960 960"
fill="rgb(0, 130, 77)"
className={styles.icon}
>
<path d="M378-225 133-470l66-66 179 180 382-382 66 65-448 448Z"></path>
</svg>
14-day free trial
</span>
<span className={styles.iconText}>
<svg
xmlns="http://www.w3.org/2000/svg"
height="18px"
width="18px"
viewBox="0 -960 960 960"
fill="rgb(0, 130, 77)"
className={styles.icon}
>
<path d="M378-225 133-470l66-66 179 180 382-382 66 65-448 448Z"></path>
</svg>
24/7 online support
</span>
</div>
);
}

View File

@@ -24,7 +24,7 @@ function SubscriptionPlansPeriodSwitcherRoot({
); );
}; };
return ( return (
<Group position={'center'} spacing={10} style={{ marginBottom: '1.2rem' }}> <Group position={'center'} spacing={10} style={{ marginBottom: '1.6rem' }}>
<Text>Pay Monthly</Text> <Text>Pay Monthly</Text>
<Switch <Switch
large large

View File

@@ -1,6 +1,7 @@
import { Callout } from '@blueprintjs/core'; import { Callout } from '@blueprintjs/core';
import { SubscriptionPlans } from './SubscriptionPlans'; import { SubscriptionPlans } from './SubscriptionPlans';
import { SubscriptionPlansPeriodSwitcher } from './SubscriptionPlansPeriodSwitcher'; import { SubscriptionPlansPeriodSwitcher } from './SubscriptionPlansPeriodSwitcher';
import { SubscriptionPlansOfferChecks } from './SubscriptionPlansOfferChecks';
/** /**
* Billing plans. * Billing plans.
@@ -14,6 +15,7 @@ export function SubscriptionPlansSection() {
include applicable taxes. include applicable taxes.
</Callout> </Callout>
<SubscriptionPlansOfferChecks />
<SubscriptionPlansPeriodSwitcher /> <SubscriptionPlansPeriodSwitcher />
<SubscriptionPlans /> <SubscriptionPlans />
</section> </section>

View File

@@ -29,7 +29,7 @@
&__content { &__content {
width: 100%; width: 100%;
padding-bottom: 40px; padding-bottom: 80px;
} }
&__left-section { &__left-section {

9
pnpm-lock.yaml generated
View File

@@ -86,6 +86,9 @@ importers:
async: async:
specifier: ^3.2.0 specifier: ^3.2.0
version: 3.2.5 version: 3.2.5
async-mutex:
specifier: ^0.5.0
version: 0.5.0
axios: axios:
specifier: ^1.6.0 specifier: ^1.6.0
version: 1.7.2 version: 1.7.2
@@ -8009,6 +8012,12 @@ packages:
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
dev: false dev: false
/async-mutex@0.5.0:
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
dependencies:
tslib: 2.6.2
dev: false
/async-settle@1.0.0: /async-settle@1.0.0:
resolution: {integrity: sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==} resolution: {integrity: sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}