import { models } from './models/sequelize.js'; import { Op } from 'sequelize'; import { v4 as uuidv4 } from 'uuid'; import axios from 'axios'; import moment from 'moment'; import chalk from 'chalk'; import ora from 'ora'; const env = process.env; /** * Gets the Access Token. * * @param {Boolean} isForce Force to request new token */ export async function requestToken(isForce = false) { let spinner = ora(`Requesting new token...`).start(); try { const clientConfigData = await models.config.findOne(); let tokenExpiration, token; if (clientConfigData) { token = clientConfigData.currentToken; tokenExpiration = clientConfigData.tokenExpiration; } if (isForce || !token || !tokenExpiration || moment().isAfter(tokenExpiration)) { const clientId = JSON.parse(env.USE_SECRETS_DB) ? clientConfigData.clientId || env.CLIENT_ID : env.CLIENT_ID; const clientSecret = JSON.parse(env.USE_SECRETS_DB) ? clientConfigData.clientSecret || env.CLIENT_SECRET : env.CLIENT_SECRET; const data = new URLSearchParams({ grant_type: 'client_credentials', client_id: clientId, client_secret: clientSecret, scope: 'role:app catalog:read supply:read organization:read network:write network:read' }).toString(); const headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; const response = (await vnRequest('POST', env.API_ENDPOINT, data, headers, spinner)).data; const tokenExpiration = moment() .add(response.expires_in, 's') .format('YYYY-MM-DD HH:mm:ss'); await updateClientConfig({ clientId, clientSecret, currentToken: response.access_token, tokenExpiration, }); spinner.succeed(); } else spinner.succeed('Using stored token...'); } catch (err) { spinner.fail(); throw err; } } /** * Returns the current token. * * @returns {string} */ export async function getCurrentToken() { return (await models.config.findOne()).currentToken; } /** * Check the floriday data config. */ export async function checkConfig() { const spinner = ora(`Checking config...`).start(); const excludedEnvVars = ['VSCODE_GIT_ASKPASS_EXTRA_ARGS']; const requiredEnvVars = Object.keys(env); const filteredEnvVars = requiredEnvVars.filter(reqEnvVar => !excludedEnvVars.includes(reqEnvVar)); for (const reqEnvVar of filteredEnvVars) { if (!process.env[reqEnvVar]) { spinner.fail(); throw new Error(`You haven't provided the ${reqEnvVar} environment variable`); } } const clientConfigData = await models.config.findOne(); if (!clientConfigData) await updateClientConfig(env.CLIENT_ID, env.CLIENT_SECRET); spinner.succeed(); } /** * Returns the expiration of current token. * * @returns {string} */ export async function getCurrentTokenExpiration() { return (await models.config.findOne()).tokenExpiration; } /** * Updates the access token in the client config table. * * @param {Array} clientConfig [clientId, clientSecret, currenToken, tokenExpiration] */ export async function updateClientConfig(clientConfig) { try { if (!JSON.parse(process.env.USE_SECRETS_DB)) clientId = clientSecret = null await models.config.upsert({ id: 1, ...clientConfig, }); } catch (err) { throw(err); } } /** * Pauses the execution of the script for the specified number of milliseconds. * * @param {Integer} ms */ export async function sleep(ms) { await new Promise(resolve => setTimeout(resolve, ms)); } /** * Sync a model. * * @param {String} model (organization | tradeItem | supplyLine | clockPresaleSupply) */ export async function syncModel(model) { let spinner = ora(`Syncing ${model}...`).start(); let i = 1; try { const dbSeqNum = await models.sequenceNumber.findOne({ where: { model } }) let curSeqNum = dbSeqNum?.maxSequenceNumber ?? 0; let maxSeqUrl, syncUrl; switch (model) { case 'organization': maxSeqUrl = `${env.API_URL}/organizations/current-max-sequence`; syncUrl = `${env.API_URL}/organizations/sync/`; break; case 'warehouse': maxSeqUrl = `${env.API_URL}/warehouses/current-max-sequence`; syncUrl = `${env.API_URL}/warehouses/sync/`; break; case 'tradeItem': maxSeqUrl = `${env.API_URL}/trade-items/current-max-sequence`; syncUrl = `${env.API_URL}/trade-items/sync/`; break; case 'supplyLine': maxSeqUrl = `${env.API_URL}/supply-lines/current-max-sequence`; syncUrl = `${env.API_URL}/supply-lines/sync/`; break; case 'clockPresaleSupply': maxSeqUrl = `${env.API_URL}/auction/clock-presales-supply/max-sequence-number`; syncUrl = `${env.API_URL}/auction/clock-presales-supply/sync/`; break; default: throw new Error('Unsupported model'); } const maxSeqNum = (await vnRequest('GET', maxSeqUrl, null, null, spinner)).data; for (curSeqNum; curSeqNum < maxSeqNum; curSeqNum++) { let params, misSeqNum; if (model === 'organization') params = new URLSearchParams({organizationType: 'SUPPLIER'}).toString(); else if (model === 'supplyLine') params = new URLSearchParams({postFilterSelectedTradeItems: false}).toString(); const res = (await vnRequest('GET', `${syncUrl}${curSeqNum}${params ? `?${params}` : ''}`, null, null, spinner)).data; curSeqNum = res.maximumSequenceNumber; const objects = res.results; misSeqNum = maxSeqNum - curSeqNum; spinner.text = `Syncing ${i - 1} ${model}, ${misSeqNum} missing...`; for (let object of objects) { switch (model) { case 'organization': await insertOrganization(object); break; case 'warehouse': await insertWarehouse(object); break; case 'tradeItem': await insertTradeItem(object); break; case 'supplyLine': await insertSupplyLine(object); break; case 'clockPresaleSupply': await insertClockPresalesSupply(object); break; default: throw new Error('Unsupported model'); } spinner.text = `Syncing ${i++} ${model}, ${misSeqNum} missing...` }; await insertSequenceNumber(model, curSeqNum); } if (curSeqNum < maxSeqNum) await insertSequenceNumber(model, maxSeqNum); spinner.text = (i != 1) ? `Syncing ${i} ${model}...` : `Syncing ${model}... ${chalk.gray('(Not found)')}`; spinner.succeed(); } catch (err) { spinner.fail(); throw err; } } /** * Create the connections in Floriday. */ export async function syncConnections(){ await deleteConnections(); let spinner; try { let connectionsInDb = await models.organization.findAll({ where: { isConnected: true, companyGln: { [Op.ne]: null }, rfhRelationId: { [Op.ne]: null }, } }); const connectionsInFloriday = (await vnRequest('GET', `${env.API_URL}/connections`, null, null, spinner)).data; let isExists = false, connectionsToPut = []; for (let connectionInDb of connectionsInDb) { for (let connectionInFloriday of connectionsInFloriday) if (connectionInFloriday == connectionInDb.organizationId) { isExists = true; break; } if (!isExists) connectionsToPut.push(connectionInDb.organizationId) isExists = false; } if (connectionsToPut.length) spinner = ora(`Creating connections in Floriday...`).start(); let i = 1; for (let connection of connectionsToPut) { spinner.text = `Creating ${i++} of ${connectionsToPut.length} connections in Floriday...` await vnRequest('PUT', `${env.API_URL}/connections/${connection}`, null, null, spinner); } if (spinner) spinner.succeed(); } catch (err) { if (spinner) spinner.fail(); throw new Error(err); } } /** * Insert sequence number in the database. * * @param {String} model * @param {Number} sequenceNumber */ export async function insertSequenceNumber(model, sequenceNumber) { const tx = await models.sequelize.transaction(); try { await models.sequenceNumber.upsert({ model: model, maxSequenceNumber: sequenceNumber, }, { transaction: tx }); await tx.commit(); } catch (err) { await tx.rollback(); throw err; } } /** * Insert trade item and dependences in the database. * * @param {Array} tradeItem */ export async function insertTradeItem(tradeItem) { const tx = await models.sequelize.transaction(); try { // Upsert trade item await models.tradeItem.upsert({ ...tradeItem, organizationId: tradeItem.supplierOrganizationId, lastSync: moment(), }, { transaction: tx }); // Upsert characteristics if (tradeItem.characteristics) if (tradeItem.characteristics.length) for (const characteristic of tradeItem.characteristics) { await models.characteristic.upsert({ tradeItemId: tradeItem.tradeItemId, ...characteristic, }, { transaction: tx }); } // Upsert seasonal periods if (tradeItem.seasonalPeriods) if (tradeItem.seasonalPeriods.length) for (const seasonalPeriod of tradeItem.seasonalPeriods) { await models.seasonalPeriod.upsert({ tradeItemId: tradeItem.tradeItemId, ...seasonalPeriod, }, { transaction: tx }); } // Upsert photos if (tradeItem.photos) if (tradeItem.photos.length) for (const photo of tradeItem.photos) { await models.photo.upsert({ ...photo, tradeItemId: tradeItem.tradeItemId, }, { transaction: tx }); } // Upsert packing configurations if (tradeItem.packingConfigurations) if (tradeItem.packingConfigurations.length) for (const packingConfiguration of tradeItem.packingConfigurations) { const uuid = uuidv4(); await models.packingConfiguration.upsert({ packingConfigurationId: uuid, ...packingConfiguration, additionalPricePerPiece_currency: packingConfiguration.additionalPricePerPiece.currency, additionalPricePerPiece_value: packingConfiguration.additionalPricePerPiece.value, tradeItemId: tradeItem.tradeItemId, }, { transaction: tx }); await models.package.upsert({ ...packingConfiguration.package, packingConfigurationId: uuid, }, { transaction: tx }); } // Upsert country of origin ISO codes if (tradeItem.countryOfOriginIsoCodes) if (tradeItem.countryOfOriginIsoCodes.length) for (const isoCode of tradeItem.countryOfOriginIsoCodes || []) { await models.countryOfOriginIsoCode.upsert({ isoCode, tradeItemId: tradeItem.tradeItemId, }, { transaction: tx }); } // Upsert botanical names if (tradeItem.botanicalNames) if (tradeItem.botanicalNames.length) for (const botanicalName of tradeItem.botanicalNames) { await models.botanicalName.upsert({ botanicalNameId: uuidv4(), name: botanicalName, tradeItemId: tradeItem.tradeItemId, }, { transaction: tx }); } await tx.commit(); } catch (err) { await tx.rollback(); throw err; } } /** * Insert clock presales supply in the database. * * @param {Array} clockPresaleSupply */ export async function insertClockPresalesSupply(clockPresaleSupply) { const tx = await models.sequelize.transaction(); try { await models.clockPresaleSupply.upsert({ ...clockPresaleSupply, pricePerPiece_currency: clockPresaleSupply.pricePerPiece.currency, pricePerPiece_value: clockPresaleSupply.pricePerPiece.value, organizationId: clockPresaleSupply.supplierOrganizationId, }, { transaction: tx }); await tx.commit(); } catch (err) { await tx.rollback(); throw err; } } /** * Insert warehouse in the database. * * @param {Array} warehouse */ export async function insertWarehouse(warehouse) { const tx = await models.sequelize.transaction(); try { await models.warehouse.upsert({ ...warehouse, location_gln: warehouse.location.gln, location_address_addressLine: warehouse.location.address.addressLine, location_address_city: warehouse.location.address.city, location_address_countryCode: warehouse.location.address.countryCode, location_address_postalCode: warehouse.location.address.postalCode, location_address_stateOrProvince: warehouse.location.address.stateOrProvince, lastSync: moment(), }, { transaction: tx }); await tx.commit(); } catch (err) { await tx.rollback(); throw err; } } /** * Insert organization in the database. * * @param {Array} organization */ export async function insertOrganization(organization) { const tx = await models.sequelize.transaction(); try { await models.organization.upsert({ ...organization, isConnected: JSON.parse(env.ORGS_ALWAYS_CONN), lastSync: moment(), }, { transaction: tx }); await tx.commit(); } catch (err) { await tx.rollback(); throw err; } } /** * Insert supply line and dependences in the database. * * @param {Array} supplyLine */ export async function insertSupplyLine(supplyLine) { const tx = await models.sequelize.transaction(); try { // Check if the warehouse exists, and if it doesn't, create it let warehouse = await models.warehouse.findOne({ where: { warehouseId: supplyLine.warehouseId } }); if (!warehouse) { let warehouse = (await vnRequest('GET', `${env.API_URL}/warehouses/${supplyLine.warehouseId}`, null, null, null)).data; // Check if the organization exists, and if it doesn't, create it let organization = await models.organization.findOne({ where: { organizationId: warehouse.organizationId } }, { transaction: tx }); if (!organization) { let organization = (await vnRequest('GET', `${env.API_URL}/organizations/${warehouse.organizationId}`, null, null, null)).data; await insertOrganization(organization); } await insertWarehouse(warehouse); } // Check if the trade item exists, and if it doesn't, create it let tradeItem = await models.tradeItem.findOne({ where: { tradeItemId: supplyLine.tradeItemId } }, { transaction: tx }); if (!tradeItem) { let tradeItem = (await vnRequest('GET', `${env.API_URL}/trade-items/${supplyLine.tradeItemId}`, null, null, null)).data; await insertTradeItem(tradeItem); } await models.supplyLine.upsert({ ...supplyLine, organizationId: supplyLine.supplierOrganizationId, deliveryPeriodStartDateTime: supplyLine.deliveryPeriod?.startDateTime ?? null, deliveryPeriodEndDateTime: supplyLine.deliveryPeriod?.endDateTime ?? null, orderPeriodStartDateTime: supplyLine.orderPeriod?.startDateTime ?? null, orderPeriodEndDateTime: supplyLine.orderPeriod?.endDateTime ?? null, agreementReference_code: supplyLine.agreementReference?.code ?? null, agreementReference_description: supplyLine.agreementReference?.description ?? null, lastSync: moment(), }, { transaction: tx }); // Upsert packing configurations if (supplyLine.packingConfigurations.length) for (const packingConfiguration of supplyLine.packingConfigurations) await models.supplyLinePackingConfiguration.upsert({ packingConfigurationId: uuidv4(), ...packingConfiguration, packageVbnPackageCode: packingConfiguration.package.vbnPackageCode, packageCustomPackageId: packingConfiguration.package.customPackageId, additionalPricePerPieceCurrency: packingConfiguration.additionalPricePerPiece.currency, additionalPricePerPieceValue: packingConfiguration.additionalPricePerPiece.value, supplyLineId: supplyLine.supplyLineId, }, { transaction: tx }); // Upsert volume price for (let volumePrice of supplyLine.volumePrices) await models.volumePrice.upsert({ supplyLineId: supplyLine.supplyLineId, ...volumePrice, }, { transaction: tx }); await tx.commit(); } catch (err) { await tx.rollback(); throw err; } } /** * Removes Floriday connections that we don't have in the database. **/ export async function deleteConnections() { let spinner; try { let i = 1; const connectionsInFloriday = (await vnRequest('GET', `${env.API_URL}/connections`, null, null, spinner)).data; const connectionsInDb = await models.organization.findAll({ attributes: ['organizationId'], where: { isConnected: true } }); let isExists = false, ghostConnections = []; for (let connectionInFloriday of connectionsInFloriday) { for (let connectionInDb of connectionsInDb) if (connectionInFloriday == connectionInDb.organizationId) { isExists = true; break; } if (!isExists) ghostConnections.push(connectionInFloriday) isExists = false; } if (ghostConnections.length) spinner = ora(`Deleting connections that aren't in the db...`).start(); for (let connection of ghostConnections) { await vnRequest('DELETE', `${env.API_URL}/connections/${connection}`, null, null, spinner); spinner.text = `Deleting ${i++} of ${ghostConnections.length} connections that aren't in the db...` } if (spinner) spinner.succeed(); } catch (err) { if (spinner) spinner.fail(); criticalError(err); } } /** * Perform a REST request. * * @param {String} url * @param {String} method * @param {Array} body * @param {Array} header * @param {Array} spinner * * @return {Array} **/ export async function vnRequest(method, url, data, headers, spinner) { if (!headers) headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${await getCurrentToken()}`, 'X-Api-Key': process.env.API_KEY, }; while(true) { try { return (['GET', 'DELETE'].includes(method)) ? await axios({method, url, headers}) : await axios({method, url, data, headers}); } catch (err) { switch (err.code) { case 'ECONNRESET': // Client network socket TLS case 'EAI_AGAIN': // getaddrinfo if (spinner) spinner.warn(); warning(err); await sleep(1000); if (spinner) spinner.start(); break; case 'ECONNABORTED': case 'ECONNREFUSED': case 'ERR_BAD_REQUEST': switch (err.response.status) { case 404: // Not found return err; case 504: case 502: if (spinner) spinner.warn(); warning(err); await sleep(1000); if (spinner) spinner.start(); break; case 429: // Too Many Requests if (spinner) spinner.warn(); warning(err); await sleep(60000); if (spinner) spinner.start(); break; case 401: // Unauthorized if (spinner) spinner.warn(); warning(err); await requestToken(true); headers.Authorization ? headers.Authorization = `Bearer ${await getCurrentToken()}` : criticalError(err); if (spinner) spinner.start(); break; default: if (spinner) spinner.warn(); warning(err); await sleep(env.MS_RETRY_UNHANDLED_ERROR); if (spinner) spinner.start(); break; } break; default: if (spinner) spinner.warn(); warning(err); await sleep(env.MS_RETRY_UNHANDLED_ERROR); if (spinner) spinner.start(); break; } } } } /** * Critical error. * * @param {err} **/ export async function criticalError(err) { console.log(chalk.red.bold(`[CRITICAL]`), chalk.red(err.message)); process.exit(); } /** * Warning. * * @param {err} **/ export async function warning(err) { (err.response?.status && err.response?.data?.message) ? (console.log(chalk.yellow.bold(`[WARNING]`), chalk.yellow(`${err.response.status} - ${err.response.data.message}`))) : (console.log(chalk.yellow.bold(`[WARNING]`), chalk.yellow(err.message))); }