import { GraphQLBoolean, GraphQLNonNull, } from 'graphql'; import { omit } from 'lodash'; import csvParser = require('csv-parser'); import * as uuid from 'node-uuid'; import { Material } from 'iworks-db-model'; import { IMaterial, IMaterialType, ISelectMaterial, ISelectMaterialType, IMaterialUploadStatus, MasterialUploadStatus, } from '../../model/material'; import { publish } from '../../pubsub'; import { authorized } from '../../auth'; import { mapUnit, mapGroup, mapStatus } from './mappers'; import { MATERIALS_UPLOAD_UPDATED } from './events'; import config from '../../config'; import { createMaterial, updateMaterial, createOrUpdateMaterial, restoreMaterial, archivingMaterial, deleteMaterial, getSelectorFromMaterialDTO, } from '../materialRepository'; import { materialType, selectMaterialType, createMaterialType, updateMaterialType, materialsUploadStatusType, uploadMetaInputType, } from './types'; import { getSelectorFromMateriaTypelDTO, isValidMaterialTypeSelector, createOrUpdateMaterialType, } from '../../materialType/materialTypeRepository'; import validationError from '../../errors/validationError'; interface InputMaterial { input: IMaterial; } interface IInputDeleteMaterial { input: ISelectMaterial; } interface IInputSelectMaterial { input: ISelectMaterial; } const validateMaterialTypeData = (data: any) => { const { materialTypeCode, materialTypeName, materialTypeUnit, group } = data; const errors = []; const emptyValue = 'Значение не задано'; if (!materialTypeCode) { errors.push({ key: 'materialTypeCode', message: emptyValue }); } if (!materialTypeName) { errors.push({ key: 'materialTypeName', message: emptyValue }); } if (!materialTypeUnit) { errors.push({ key: 'materialTypeUnit', message: emptyValue }); } if (!group) { errors.push({ key: 'group', message: emptyValue }); } if (errors.length > 0) { throw new validationError(errors); } }; const validateMaterialData = (data: any) => { const { code, name, description, price, unit, unitValue } = data; const errors = []; const emptyValue = 'Значение не задано'; if (!code) { errors.push({ key: 'code', message: emptyValue }); } if (!name) { errors.push({ key: 'name', message: emptyValue }); } if (!price) { errors.push({ key: 'price', message: emptyValue }); } if (unit && !unitValue) { errors.push({ key: 'unitValue', message: emptyValue }); } if (errors.length > 0) { throw new validationError(errors); } }; const timeout = (ms: number) => new Promise(res => setTimeout(res, ms)); async function uploadMaterials(stream: any, syncId: string): Promise { let totalCount = 0; let failedCount = 0; let successCount = 0; // Small hack to make sure that subscriptions are passed after http callback await timeout(1000); const startUploadStatus: IMaterialUploadStatus = { syncId, totalCount: 0, successCount: 0, failedCount: 0, currentMaterial: undefined, status: MasterialUploadStatus.started, }; publish(MATERIALS_UPLOAD_UPDATED, startUploadStatus); for await (const data of stream) { let currentMaterial = undefined; let status: MasterialUploadStatus = MasterialUploadStatus.inprogress; try { totalCount += 1; console.log(`Read row ${totalCount}`); // Material type validateMaterialTypeData(data); const materialTypeSelector: ISelectMaterialType = { id: undefined, code: data.materialTypeCode, }; const materialTypeUnit = mapUnit(data.materialTypeUnit); const materialTypeData: Partial = { code: data.materialTypeCode, name: data.materialTypeName, unit: materialTypeUnit, group: mapGroup(data.group.toUpperCase()), }; const typeResult: Partial = await createOrUpdateMaterialType(materialTypeSelector, materialTypeData); // Material validateMaterialData(data); const materialSelector: ISelectMaterial = { id: undefined, code: data.code, modificationCode: data.modificationСode, }; const materialUnit = data.unit ? mapUnit(data.unit) : materialTypeUnit; const materialUnitValue = materialUnit === materialTypeUnit ? 1 : parseFloat(data.unitValue); const materialData: Partial = { code: data.code, name: data.name, description: data.description, price: Number(parseFloat(data.price).toPrecision(3)), modificationCode: data.modificationСode, unit: materialUnit, unitValue: materialUnitValue, status: mapStatus(data.status.toUpperCase()), }; currentMaterial = await createOrUpdateMaterial(materialSelector, materialData, typeResult); successCount += 1; } catch (err) { console.error(err); if (!(err instanceof validationError)) { status = MasterialUploadStatus.failed; } failedCount += 1; } finally { const uploadStatus: IMaterialUploadStatus = { syncId, totalCount, successCount, failedCount, currentMaterial, status, }; publish(MATERIALS_UPLOAD_UPDATED, uploadStatus); } } const result: IMaterialUploadStatus = { syncId, totalCount, successCount, failedCount, status: MasterialUploadStatus.finished, currentMaterial: null, }; publish(MATERIALS_UPLOAD_UPDATED, result); return result; } export default { createMaterial: { description: 'Creates a new material', type: materialType, args: { input: { type: new GraphQLNonNull(createMaterialType) }, }, resolve: authorized( ['AM', 'RM', 'CE'], (_: any, inputMaterial: InputMaterial) => { const input = inputMaterial.input; if (!input) { throw new Error('Input is not specified for createMaterial'); } const materialType = input.materialType; if (!materialType) { throw new Error('materialType is not specified for createMaterial'); } const materialSelector = getSelectorFromMateriaTypelDTO(materialType); const cleanType = omit(input, ['materialType']); return createMaterial(cleanType, materialSelector); }, ), }, archivingMaterial: { description: 'Archiving a material', type: materialType, args: { input: { type: new GraphQLNonNull(selectMaterialType) }, }, resolve: authorized( ['AM', 'RM', 'CE'], (_: any, inputSelect: IInputSelectMaterial) => { const input = inputSelect.input; if (!input) { throw new Error('Input is not specified for createMaterial'); } return archivingMaterial(input); }, ), }, restoreMaterial: { description: 'Restore a material', type: materialType, args: { input: { type: new GraphQLNonNull(selectMaterialType) }, }, resolve: authorized( ['AM', 'RM', 'CE'], (_: any, inputSelect: IInputSelectMaterial) => { const input = inputSelect.input; if (!input) { throw new Error('Input is not specified for createMaterial'); } return restoreMaterial(input); }, ), }, updateMaterial: { description: 'Changes exiting material', type: materialType, args: { input: { type: new GraphQLNonNull(updateMaterialType) }, }, resolve: authorized( ['AM', 'RM', 'CE'], (_: any, inputMaterial: InputMaterial) => { const input = inputMaterial.input; if (!input) { throw new Error('Input is not specified for updateMaterial'); } if (input.materialType && !isValidMaterialTypeSelector(input.materialType as IMaterialType)) { throw new Error('MaterialType is existing, but selector is incorrect.'); } const selector = getSelectorFromMaterialDTO(input); let materialSelector = undefined; if (input.materialType) { materialSelector = getSelectorFromMateriaTypelDTO(input.materialType); } const cleanType = omit(input, ['materialType', 'status']); return updateMaterial(selector, cleanType, materialSelector); }, ), }, deleteMaterial: { description: 'Delete a material', type: GraphQLBoolean, args: { input: { type: new GraphQLNonNull(selectMaterialType) }, }, resolve: authorized( ['AM', 'RM', 'CE'], (_: any, data: IInputDeleteMaterial) => { const input = data.input; if (!input) { throw new Error('Input is not specified for delete material'); } return deleteMaterial(input); }, ), }, materialsUpload: { description: 'Upload materials from csv files', type: materialsUploadStatusType, args: { input: { type: uploadMetaInputType }, }, resolve: authorized( ['AM', 'RM', 'CE'], async (_: any, { input }: any): Promise => { const { stream } = await input.upload; const syncId = input.id; const csvStream = stream.pipe(csvParser({ separator: config.MATERIALS_CSV_SEPARATOR, headers: [ 'code', 'name', 'description', 'price', 'modificationСode', 'imageSrc', 'modificationParams', 'updated', 'status', 'unit', 'unitValue', 'materialTypeCode', 'materialTypeName', 'materialTypeUnit', 'group', ], })); return uploadMaterials(csvStream, syncId); }, ), }, };