floriday/utils.js

638 lines
18 KiB
JavaScript
Raw Normal View History

2023-05-09 12:13:19 +00:00
import { models } from './models/sequelize.js';
2023-02-08 11:31:54 +00:00
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;
2023-01-11 13:37:46 +00:00
/**
2023-05-15 12:19:43 +00:00
* Gets the Access Token
*
2023-05-15 12:19:43 +00:00
* @param {Boolean} isForce Force to request new token
*/
2023-05-15 12:19:43 +00:00
export async function requestToken(isForce = false) {
let spinner = ora(`Requesting new token...`).start();
2023-05-05 09:48:44 +00:00
try {
const clientConfigData = await models.clientConfig.findOne();
2023-05-11 07:43:27 +00:00
let tokenExpirationDate, token;
if (clientConfigData) {
token = clientConfigData.currentToken;
2023-05-05 09:48:44 +00:00
tokenExpirationDate = clientConfigData.tokenExpiration;
2023-05-11 07:43:27 +00:00
}
2023-05-15 12:19:43 +00:00
if (isForce || !token || !tokenExpirationDate || moment().isAfter(tokenExpirationDate)) {
2023-05-05 09:48:44 +00:00
let clientId, clientSecret
if (JSON.parse(env.USE_SECRETS_DB)) {
clientId = clientConfigData.clientId;
clientSecret = clientConfigData.clientSecret;
} else {
clientId = env.CLIENT_ID
clientSecret = env.CLIENT_SECRET
};
let data = {
2023-05-05 09:48:44 +00:00
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
scope: 'role:app catalog:read supply:read organization:read network:write network:read'
};
data = Object.keys(data)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`)
.join('&')
2023-05-15 12:19:43 +00:00
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
2023-05-15 12:19:43 +00:00
const response = (await vnRequest('POST', env.API_ENDPOINT, data, headers)).data;
if (response.statusText = 'OK')
2023-05-05 09:48:44 +00:00
spinner.succeed();
else {
spinner.fail();
criticalError(new Error(`Token request failed with status: ${response.status} - ${response.statusText}`));
}
let tokenExpirationDate = moment()
.add(response.expires_in, 's')
2023-05-05 09:48:44 +00:00
.format('YYYY-MM-DD HH:mm:ss');
await updateClientConfig(
clientId,
clientSecret,
response.access_token,
2023-05-05 09:48:44 +00:00
tokenExpirationDate
);
2023-05-15 12:19:43 +00:00
} else
spinner.succeed('Using stored token...');
2023-05-05 09:48:44 +00:00
} catch (err) {
spinner.fail();
throw err;
2023-04-24 10:46:06 +00:00
}
}
2023-04-06 08:56:52 +00:00
/**
* Returns the current token
*
* @returns {string}
*/
export async function getCurrentToken() {
2023-05-15 12:19:43 +00:00
return (await models.clientConfig.findOne()).currentToken;
}
2023-05-08 10:18:14 +00:00
/**
* 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`);
}
}
2023-05-08 11:02:31 +00:00
const clientConfigData = await models.clientConfig.findOne();
if (!clientConfigData)
await updateClientConfig(env.CLIENT_ID, env.CLIENT_SECRET);
2023-05-08 10:18:14 +00:00
spinner.succeed();
}
2023-04-24 10:46:06 +00:00
2023-04-06 08:56:52 +00:00
/**
* Returns the expiration of current token
2023-04-06 08:56:52 +00:00
*
* @returns {string}
*/
export async function getCurrentTokenExpiration() {
return (await models.clientConfig.findOne()).tokenExpiration;
}
/**
* Updates the access token in the client config table
*
* @param {String} clientId
* @param {String} clientSecret
* @param {String} accessToken
* @param {String} tokenExpirationDate
*/
export async function updateClientConfig(clientId, clientSecret, currentToken, tokenExpiration) {
2023-05-05 09:48:44 +00:00
try {
const spinner = ora(`Updating token...`).start();
if (!JSON.parse(process.env.USE_SECRETS_DB))
clientId = clientSecret = null
await models.clientConfig.upsert({
id: 1,
clientId,
clientSecret,
currentToken,
tokenExpiration,
2023-05-05 09:48:44 +00:00
});
spinner.succeed();
} catch (err) {
2023-05-05 09:48:44 +00:00
spinner.fail();
throw(err);
2023-05-05 09:48:44 +00:00
}
}
2023-01-11 12:13:22 +00:00
/**
* pauses the execution of the script for the given amount of milliseconds
*
* @param {integer} ms
* @returns A promise that resolves after ms milliseconds.
*/
export async function sleep(ms) {
2023-04-24 10:46:06 +00:00
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
2023-01-11 12:13:22 +00:00
}
2023-01-16 13:52:08 +00:00
/**
* Recieves an array of functions and executes them in a queue with the given concurrency
*
* @param {Array} fnArray
* @param {Number} concurrency
* @returns
*/
export async function asyncQueue(fnArray, concurrency = 1) {
2023-01-16 13:52:08 +00:00
2023-04-24 10:46:06 +00:00
const results = []; // 1
2023-01-16 13:52:08 +00:00
2023-04-24 10:46:06 +00:00
const queue = fnArray.map((fn, index) => () => fn().then((result) => results[index] = result));
2023-01-16 13:52:08 +00:00
2023-04-24 10:46:06 +00:00
const run = async () => { // 2
const fn = queue.shift();
if (fn) {
await fn();
await run();
}
};
2023-01-16 13:52:08 +00:00
2023-04-24 10:46:06 +00:00
const promises = []; // 3
while (concurrency--) { // 4
promises.push(run());
}
2023-01-16 13:52:08 +00:00
2023-04-24 10:46:06 +00:00
await Promise.all(promises); // 5
2023-01-24 12:51:44 +00:00
2023-04-24 10:46:06 +00:00
return results;
2023-01-16 13:52:08 +00:00
}
2023-05-17 05:36:58 +00:00
/**
* Sync the suppliers
*/
export async function syncSuppliers(){
2023-04-24 10:46:06 +00:00
let spinner = ora('Preparing to load suppliers...').start();
try {
2023-05-17 08:59:27 +00:00
const maxSequenceNumber = (await vnRequest('GET', `${env.API_URL}/organizations/current-max-sequence`)).data;
2023-04-24 10:46:06 +00:00
let timeFinish, timeToGoSec, timeToGoMin, timeLeft;
for (let curSequenceNumber = 0; curSequenceNumber <= maxSequenceNumber; curSequenceNumber++) {
let timeStart = new moment();
let data = (await vnRequest('GET', `${env.API_URL}/organizations/sync/${curSequenceNumber}?organizationType=SUPPLIER`)).data;
2023-04-24 10:46:06 +00:00
let suppliers = data.results;
for (let supplier of suppliers) {
curSequenceNumber = supplier.sequenceNumber;
spinner.text = `Syncing suppliers, ${maxSequenceNumber - curSequenceNumber} are missing`
if (timeFinish)
spinner.text = spinner.text + ` (${timeLeft})`
await models.organization.upsert({
2023-05-15 12:19:43 +00:00
...supplier,
isConnected: JSON.parse(env.SUPPLIERS_ALWAYS_CONN),
lastSync: moment(),
2023-04-24 10:46:06 +00:00
});
};
2023-04-24 10:46:06 +00:00
timeFinish = new moment();
timeToGoSec = (timeFinish.diff(timeStart, 'seconds') * (maxSequenceNumber - curSequenceNumber) / 1000)
timeToGoMin = Math.trunc(timeToGoSec / 60)
if (!timeToGoMin)
timeLeft = `${Math.trunc(timeToGoSec)} sec`
else
timeLeft = `${timeToGoMin} min`
}
spinner.text = `Syncing suppliers...`;
2023-04-24 10:46:06 +00:00
spinner.succeed()
}
catch (err) {
spinner.fail();
throw new Error(err);
}
2023-01-24 12:51:44 +00:00
}
/**
2023-05-17 05:36:58 +00:00
* Create the connections in Floriday
*/
export async function syncConnections(){
await deleteConnections();
let spinner;
2023-04-24 10:46:06 +00:00
try {
let connectionsInDb = await models.organization.findAll({
attributes: ['organizationId'],
where: { isConnected: true }
});
2023-04-24 10:46:06 +00:00
const connectionsInFloriday = (await vnRequest('GET', `${env.API_URL}/connections`)).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}`);
2023-04-24 10:46:06 +00:00
}
if (spinner) spinner.succeed();
2023-04-24 10:46:06 +00:00
} catch (err) {
if (spinner) spinner.fail();
2023-04-24 10:46:06 +00:00
throw new Error(err);
}
2023-02-10 09:42:54 +00:00
}
2023-05-17 05:36:58 +00:00
/**
* Sync the trade items for organizations that are connected
2023-05-17 05:36:58 +00:00
*/
export async function syncTradeItems(){
2023-04-24 10:46:06 +00:00
const spinner = ora(`Syncing trade items...`).start();
const suppliers = await models.organization.findAll({
attributes: ['organizationId'],
2023-05-09 12:13:19 +00:00
where: { isConnected: true }
});
let i = 0, x = 0;
2023-04-24 10:46:06 +00:00
for (let supplier of suppliers) {
try {
const params = new URLSearchParams({
supplierOrganizationId: supplier.organizationId,
}).toString();
2023-05-17 08:59:27 +00:00
let tradeItems = (await vnRequest('GET', `${env.API_URL}/trade-items?${params}`)).data
spinner.text = `Syncing ${i} trade items of [${x++}|${suppliers.length}] suppliers...`
if (!tradeItems.length) continue;
2023-04-24 10:46:06 +00:00
for (let tradeItem of tradeItems) {
await insertItem(tradeItem);
2023-05-15 12:19:43 +00:00
spinner.text = `Syncing ${i++} trade items of [${x}|${suppliers.length}] suppliers...`
2023-04-24 10:46:06 +00:00
};
2023-05-08 12:55:18 +00:00
} catch (err) {
2023-04-24 10:46:06 +00:00
spinner.fail();
2023-05-08 12:55:18 +00:00
throw err;
2023-04-24 10:46:06 +00:00
}
}
spinner.succeed()
2023-01-16 13:52:08 +00:00
}
2023-02-14 13:36:05 +00:00
/**
* Sync the supply lines for organizations that are connected
*
* If necessary, create the item or the warehouse
2023-02-14 13:36:05 +00:00
*/
export async function syncSupplyLines() {
const spinner = ora(`Syncing supply lines...`).start();
try {
let connectedSuppliers = await models.organization.findAll({
attributes: ['organizationId'],
where: { isConnected: true }
});
2023-05-15 12:19:43 +00:00
let i = 0, x = 1;
for (let supplier of connectedSuppliers) {
spinner.text = `Syncing ${i} supply lines of [${x++}|${connectedSuppliers.length}] suppliers...`
const params = new URLSearchParams({
supplierOrganizationId: supplier.organizationId,
}).toString();
2023-05-17 08:59:27 +00:00
let supplyLines = (await vnRequest('GET',`${env.API_URL}/supply-lines?${params}`)).data;
if (!supplyLines.length) continue
2023-05-15 12:19:43 +00:00
for (let supplyLine of supplyLines) {
// Check if the trade item exists, and if it doesn't, create it
2023-05-15 12:19:43 +00:00
let tradeItem = await models.tradeItem.findOne({
where: { tradeItemId: supplyLine.tradeItemId }
});
if (!tradeItem) {
let tradeItem = (await vnRequest('GET', `${env.API_URL}/trade-items/${supplyLine.tradeItemId}`)).data;
await insertItem(tradeItem);
}
// Check if the warehouse exists, and if it doesn't, create it
let warehouse = await models.warehouses.findOne({
where: { warehouseId: supplyLine.warehouseId }
});
if (!warehouse) {
let warehouse = (await vnRequest('GET', `${env.API_URL}/warehouses/${supplyLine.warehouseId}`)).data;
await insertWarehouse(warehouse);
2023-05-15 12:19:43 +00:00
}
spinner.text = `Syncing ${i++} supply lines of [${x}|${connectedSuppliers.length}] suppliers...`
2023-05-15 12:19:43 +00:00
await models.supplyLine.upsert({
...supplyLine,
organizationId: supplyLine.supplierOrganizationId,
2023-05-15 12:19:43 +00:00
pricePerPiece_currency: supplyLine.pricePerPiece ? supplyLine.pricePerPiece.currency : null,
pricePerPiece_value: supplyLine.pricePerPiece ? supplyLine.pricePerPiece.value : null,
deliveryPeriod_startDateTime: supplyLine.deliveryPeriod ? supplyLine.deliveryPeriod.startDateTime : null,
deliveryPeriod_endDateTime: supplyLine.deliveryPeriod ? supplyLine.deliveryPeriod.endDateTime : null,
orderPeriod_startDateTime: supplyLine.orderPeriod ? supplyLine.orderPeriod.startDateTime : null,
orderPeriod_endDateTime: supplyLine.orderPeriod ? supplyLine.orderPeriod.endDateTime : null,
agreementReference_code: supplyLine.agreementReference ? supplyLine.agreementReference.code : null,
agreementReference_description: supplyLine.agreementReference ? supplyLine.agreementReference.description : null,
lastSync: moment(),
});
for (let volumePrice of supplyLine.volumePrices)
await models.volumePrices.upsert({
...volumePrice,
supplyLineId: supplyLine.supplyLineId,
});
}
}
spinner.succeed();
}
catch (err) {
spinner.fail();
throw err;
}
}
/**
* Insert trade item and dependences in db
*
* @param {array} tradeItem
*/
export async function insertItem(tradeItem) {
let tx;
2023-04-24 10:46:06 +00:00
try {
tx = await models.sequelize.transaction();
2023-04-24 10:46:06 +00:00
2023-05-09 12:13:19 +00:00
// Upsert trade item
2023-04-24 10:46:06 +00:00
await models.tradeItem.upsert({
2023-05-09 12:13:19 +00:00
...tradeItem,
organizationId: tradeItem.supplierOrganizationId,
2023-05-15 12:19:43 +00:00
lastSync: moment(),
2023-05-09 12:13:19 +00:00
}, { transaction: tx });
2023-04-24 10:46:06 +00:00
2023-05-09 12:13:19 +00:00
// Upsert characteristics
if (tradeItem.characteristics)
2023-05-15 12:19:43 +00:00
if (tradeItem.characteristics.length)
for (const characteristic of tradeItem.characteristics) {
await models.characteristic.upsert({
...characteristic,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
}
2023-05-09 12:13:19 +00:00
// Upsert seasonal periods
if (tradeItem.seasonalPeriods)
2023-05-15 12:19:43 +00:00
if (tradeItem.seasonalPeriods.length)
for (const seasonalPeriod of tradeItem.seasonalPeriods) {
await models.seasonalPeriod.upsert({
...seasonalPeriod,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
}
2023-04-24 10:46:06 +00:00
2023-05-09 12:13:19 +00:00
// Upsert photos
if (tradeItem.photos)
2023-05-15 12:19:43 +00:00
if (tradeItem.photos.length)
for (const photo of tradeItem.photos) {
await models.photo.upsert({
...photo,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
}
2023-04-24 10:46:06 +00:00
2023-05-09 12:13:19 +00:00
// Upsert packing configurations
if (tradeItem.packingConfigurations)
2023-05-15 12:19:43 +00:00
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,
2023-05-15 12:19:43 +00:00
}, { transaction: tx });
}
2023-04-24 10:46:06 +00:00
2023-05-09 12:13:19 +00:00
// Upsert country of origin ISO codes
if (tradeItem.countryOfOriginIsoCodes)
2023-05-15 12:19:43 +00:00
if (tradeItem.countryOfOriginIsoCodes.length)
for (const isoCode of tradeItem.countryOfOriginIsoCodes || []) {
await models.countryOfOriginIsoCode.upsert({
isoCode,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
}
2023-04-24 10:46:06 +00:00
2023-05-09 12:13:19 +00:00
// Upsert botanical names
if (tradeItem.botanicalNames)
2023-05-15 12:19:43 +00:00
if (tradeItem.botanicalNames.length)
for (const botanicalName of tradeItem.botanicalNames) {
await models.botanicalName.upsert({
botanicalNameId: uuidv4(),
2023-05-15 12:19:43 +00:00
name: botanicalName,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
}
2023-04-24 10:46:06 +00:00
await tx.commit();
2023-05-09 12:13:19 +00:00
} catch (err) {
2023-04-24 10:46:06 +00:00
await tx.rollback();
throw err;
2023-04-24 10:46:06 +00:00
}
2023-02-14 12:34:54 +00:00
}
/**
* Insert warehouse in db
*
* @param {array} warehouse
*/
export async function insertWarehouse(warehouse) {
let tx;
try {
tx = await models.sequelize.transaction();
await models.warehouses.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(),
});
await tx.commit();
} catch (err) {
await tx.rollback();
throw err;
}
}
2023-05-16 08:24:04 +00:00
/**
* Sync the warehouses for organizations that are connected
2023-05-16 08:24:04 +00:00
**/
export async function syncWarehouses(){
let spinner = ora('Syncing warehouses...').start();
try {
const suppliers = await models.organization.findAll({
where: { isConnected: true }
});
let x = 0, i = 0;
2023-05-16 08:24:04 +00:00
for (let supplier of suppliers) {
2023-05-17 08:59:27 +00:00
spinner.text = `Syncing ${i} warehouses of [${x++}|${suppliers.length}] suppliers...`
const warehouses = (await vnRequest('GET', `${env.API_URL}/organizations/supplier/${supplier.organizationId}/warehouses`)).data;
2023-05-16 08:24:04 +00:00
for (let warehouse of warehouses) {
spinner.text = `Syncing ${i++} warehouses of [${x}|${suppliers.length}] suppliers...`
await insertWarehouse(warehouse);
2023-05-16 08:24:04 +00:00
}
}
spinner.succeed();
2023-05-16 08:24:04 +00:00
}
catch (err) {
spinner.fail();
throw new Error(err);
}
}
/**
* Removes Floriday connections that we don't have in the database
**/
export async function deleteConnections() {
let spinner;
try {
let i = 1;
2023-05-17 08:59:27 +00:00
const connectionsInFloriday = (await vnRequest('GET', `${env.API_URL}/connections`)).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}`);
spinner.text = `Deleting ${i++} of ${ghostConnections.length} 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
*
* @return {array}
**/
2023-05-15 12:19:43 +00:00
export async function vnRequest(method, url, data, headers) {
2023-05-17 08:59:27 +00:00
if (!headers)
headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await getCurrentToken()}`,
'X-Api-Key': process.env.API_KEY,
};
for(let i = 0; i < env.MAX_REQUEST_RETRIES; i++) {
try {
if (['GET', 'DELETE'].includes(method))
2023-05-15 12:19:43 +00:00
return await axios({method, url, headers});
else
2023-05-15 12:19:43 +00:00
return await axios({method, url, data, headers});
} catch (err) {
2023-05-15 12:19:43 +00:00
switch (err.code) {
case 'ECONNRESET': // Client network socket TLS
2023-05-16 08:24:04 +00:00
case 'EAI_AGAIN': // getaddrinfo
2023-05-15 12:19:43 +00:00
warning(err);
await sleep(1000);
break;
case 'ECONNABORTED':
case 'ECONNREFUSED':
case 'ERR_BAD_REQUEST':
switch (err.response.status) {
2023-05-16 08:24:04 +00:00
case 504:
case 502:
warning(err);
await sleep(1000);
break;
2023-05-15 12:19:43 +00:00
case 429: // Too Many Requests
warning(err);
await sleep(3400); // Stipulated by floryday
2023-05-16 08:24:04 +00:00
break;
2023-05-15 12:19:43 +00:00
case 401: // Unauthorized
warning(err);
await requestToken(true);
headers.Authorization ?
headers.Authorization = `Bearer ${await getCurrentToken()}` :
criticalError(err);
break;
default:
criticalError(err);
}
break;
default:
criticalError(err);
}
}
}
}
/**
* Critical error
*
* @param {err}
**/
export async function criticalError(err) {
2023-05-05 09:48:44 +00:00
console.log(chalk.red.bold(`[CRITICAL]`), chalk.red(`${err.message}`));
2023-04-24 10:46:06 +00:00
process.exit();
}
/**
* Warning
*
* @param {err}
**/
export async function warning(err) {
console.log(chalk.yellow.bold(`[WARNING]`), chalk.yellow(`${err.message}`));
}