diff --git a/package.json b/package.json index 2ce50f3..bd20d16 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "main.js", "scripts": { "start": "electron .", - "test": "jest", + "test": "jest --runInBand", "rebuild": "electron-rebuild -f -w sqlite3", "lint": "./node_modules/.bin/eslint" }, diff --git a/src/repository/DbContext.js b/src/repository/DbContext.js index 06b2358..995811f 100644 --- a/src/repository/DbContext.js +++ b/src/repository/DbContext.js @@ -1,27 +1,42 @@ -const config = require('../services/config.json'); -let dbConfig = config.knexConfig; const fs = require('fs'); const knex = require('knex'); -const getKnexObject = () => { - if (dbConfig.client === 'sqlite3') { - return getSqliteKnexObject(); - } -} +const config = require('../services/config.json'); -module.exports = { - getKnexObject -} +const dbConfig = config.knexConfig; /** * Get knex object with sqlite client */ -function getSqliteKnexObject() { - const filepaths = config.sqlite_config.filepath; - for (let filepath of filepaths) { - if (fs.existsSync(filepath)) { - dbConfig.connection.filename = filepath; - return knex(dbConfig); - } +function getSqliteKnexObject(ENV) { + const filename = (ENV === config.environment.name.Test) + ? config.sqlite_config.test_db + : config.sqlite_config.production_db; + const filepaths = config.sqlite_config.filepath; + + filepaths.forEach((filepath) => { + const fullFilePath = filepath + filename; + + if (fs.existsSync(fullFilePath)) { + dbConfig.connection.filename = fullFilePath; + + return knex(dbConfig); } -} \ No newline at end of file + + return null; + }); + + return new Error('Cannot get SQLite Knex object.'); +} + +const getKnexObject = (ENV = config.environment.name.Production) => { + if (dbConfig.client === 'sqlite3') { + return getSqliteKnexObject(ENV); + } + + return new Error('Database is not SQLite 3.'); +}; + +module.exports = { + getKnexObject, +}; diff --git a/src/services/componentServices.js b/src/services/componentServices.js index 8ebd793..ce33af1 100644 --- a/src/services/componentServices.js +++ b/src/services/componentServices.js @@ -1,741 +1,746 @@ -const config = require('./config.json'); -const constants = config.constants; -const resourceServices = require(config.paths.resourceServices); -const dbContext = require('../repository/DbContext'); -const knex = dbContext.getKnexObject(); - -const Component = require(config.paths.componentModel); -const ComponentType = require(config.paths.componentTypeModel); - -/** - * Gets all components of a given region. - * @param {Number} id must be an integer. - * @returns {Array} array of component objects if successful, null otherwise. - */ -const getComponentByRegionId = async (id) => { - const rawComponents = await knex - .select('*') - .from(constants.TABLE_COMPONENT) - .where(constants.COLUMN_REGION_ID, id) - .catch(e => { - console.error(e); - }); - - const componentTypes = await getComponentTypeAll(); - - const resources = await resourceServices.getResourceAll(); - - if (rawComponents.length === 0 || componentTypes === null || resources === null) return null; - - let components = []; - - for (let rawComponent of rawComponents) { - let componentValue = rawComponent.value; - - if (rawComponent.componentTypeId === 3) { - componentValue = resources[parseInt(rawComponent.value.split(';')[1]) - 1]; - } - - let component = new Component( - rawComponent.componentId, - rawComponent.name, - componentTypes[rawComponent.componentTypeId - 1], - rawComponent.regionId, - rawComponent.facilityId, - componentValue, - rawComponent.activationTime, - rawComponent.isChild, - rawComponent.parentId - ); - - components.push(component); - } - - for (let component of components) { - if (component.isChild) { - for (let parentComponent of components) { - if (component.parentId === parentComponent.componentId) { - component.parent = parentComponent; - break; - } - } - } - } - - return components; -}; - -/** - * Gets all components of a given facility. - * @param {Number} id must be an integer. - * @returns {Array} array of component objects if successful, null otherwise. - */ -const getComponentByFacilityId = async (id) => { - const rawComponents = await knex - .select('*') - .from(constants.TABLE_COMPONENT) - .where(constants.COLUMN_FACILITY_ID, id) - .catch(e => { - console.error(e); - }); - - const componentTypes = await getComponentTypeAll(); - - const resources = await resourceServices.getResourceAll(); - - if (rawComponents.length === 0 || componentTypes === null || resources === null) return null; - - let components = []; - - for (let rawComponent of rawComponents) { - let componentValue = rawComponent.value; - - if (rawComponent.componentTypeId === 3) { - componentValue = resources[parseInt(rawComponent.value.split(';')[1]) - 1]; - } - - let component = new Component( - rawComponent.componentId, - rawComponent.name, - componentTypes[rawComponent.componentTypeId - 1], - rawComponent.regionId, - rawComponent.facilityId, - componentValue, - rawComponent.activationTime, - rawComponent.isChild, - rawComponent.parentId - ); - - components.push(component); - } - - for (let component of components) { - if (component.isChild) { - for (let parentComponent of components) { - if (component.parentId === parentComponent.componentId) { - component.parent = parentComponent; - break; - } - } - } - } - - return components; -} - -/** - * Gets all functional components of a given region. - * @param {Number} id must be an integer. - * @returns {Array} array of component objects if successful, null otherwise. - */ -const getComponentFunctionalByRegionId = async (id) => { - const rawComponents = await knex - .select(constants.TABLE_COMPONENT + '.' + '*') - .from(constants.TABLE_COMPONENT) - .innerJoin( - constants.TABLE_FACILITY, - constants.TABLE_COMPONENT + '.' + constants.COLUMN_FACILITY_ID, - constants.TABLE_FACILITY + '.' + constants.COLUMN_FACILITY_ID - ) - .where(constants.TABLE_COMPONENT + '.' + constants.COLUMN_REGION_ID, id) - .andWhere(constants.TABLE_FACILITY + '.' + constants.COLUMN_IS_FUNCTIONAL, 1) - .catch(e => { - console.error(e); - }); - - const componentTypes = await getComponentTypeAll(); - - const resources = await resourceServices.getResourceAll(); - - if (rawComponents.length === 0 || componentTypes === null || resources === null) return null; - - let components = []; - - for (let rawComponent of rawComponents) { - let componentValue = rawComponent.value; - - if (rawComponent.componentTypeId === 3) { - componentValue = resources[parseInt(rawComponent.value.split(';')[1]) - 1]; - } - - let component = new Component( - rawComponent.componentId, - rawComponent.name, - componentTypes[rawComponent.componentTypeId - 1], - rawComponent.regionId, - rawComponent.facilityId, - componentValue, - rawComponent.activationTime, - rawComponent.isChild, - rawComponent.parentId - ); - - components.push(component); - } - - for (let component of components) { - if (component.isChild) { - for (let parentComponent of components) { - if (component.parentId === parentComponent.componentId) { - component.parent = parentComponent; - break; - } - } - } - } - - return components; -} - -/** - * Gets all functional resource components of a given state. - * @param {Number} id must be an integer. - * @returns {Array} array of component objects if successful, null otherwise. - */ -const getComponentResourceFunctionalByStateId = async (id) => { - const rawComponents = await knex - .select(constants.TABLE_COMPONENT + '.' + '*') - .from(constants.TABLE_COMPONENT) - .innerJoin( - constants.TABLE_FACILITY, - constants.TABLE_COMPONENT + '.' + constants.COLUMN_FACILITY_ID, - constants.TABLE_FACILITY + '.' + constants.COLUMN_FACILITY_ID - ) - .leftJoin( - constants.TABLE_REGION, - constants.TABLE_COMPONENT + '.' + constants.COLUMN_REGION_ID, - constants.TABLE_REGION + '.' + constants.COLUMN_REGION_ID - ) - .where(constants.TABLE_REGION + '.' + constants.COLUMN_STATE_ID, id) - .andWhere(constants.TABLE_FACILITY + '.' + constants.COLUMN_IS_FUNCTIONAL, 1) - .andWhere(constants.COLUMN_COMPONENT_TYPE_ID, 3) - .catch(e => { - console.error(e); - }); - - const componentTypes = await getComponentTypeAll(); - - const resources = await resourceServices.getResourceAll(); - - if (rawComponents.length === 0 || componentTypes === null || resources === null) return null; - - let components = []; - - for (let rawComponent of rawComponents) { - let componentValue = rawComponent.value; - - if (rawComponent.componentTypeId === 3) { - componentValue = resources[parseInt(rawComponent.value.split(';')[1]) - 1]; - } - - let component = new Component( - rawComponent.componentId, - rawComponent.name, - componentTypes[rawComponent.componentTypeId - 1], - rawComponent.regionId, - rawComponent.facilityId, - componentValue, - rawComponent.activationTime, - rawComponent.isChild, - rawComponent.parentId - ); - - components.push(component); - } - - for (let component of components) { - if (component.isChild) { - for (let parentComponent of components) { - if (component.parentId === parentComponent.componentId) { - component.parent = parentComponent; - break; - } - } - } - } - - return components; -} - -/** - * Gets all functional components of the given IDs. - * @param {Array} ids must be an array of integers. - * @returns {Array} array of component objects if successful, null otherwise. - */ -const getComponentFunctionalByIds = async (ids) => { - const rawComponents = await knex - .select(constants.TABLE_COMPONENT + '.' + '*') - .from(constants.TABLE_COMPONENT) - .innerJoin( - constants.TABLE_FACILITY, - constants.TABLE_COMPONENT + '.' + constants.COLUMN_FACILITY_ID, - constants.TABLE_FACILITY + '.' + constants.COLUMN_FACILITY_ID - ) - .whereIn(constants.COLUMN_COMPONENT_ID, ids) - .andWhere(constants.TABLE_FACILITY + '.' + constants.COLUMN_IS_FUNCTIONAL, 1) - .catch(e => { - console.error(e); - }); - - const componentTypes = await getComponentTypeAll(); - - const resources = await resourceServices.getResourceAll(); - - if (rawComponents.length === 0 || componentTypes === null || resources === null) return null; - - let components = []; - - for (let rawComponent of rawComponents) { - let componentValue = rawComponent.value; - - if (rawComponent.componentTypeId === 3) { - componentValue = resources[parseInt(rawComponent.value.split(';')[1]) - 1]; - } - - let component = new Component( - rawComponent.componentId, - rawComponent.name, - componentTypes[rawComponent.componentTypeId - 1], - rawComponent.regionId, - rawComponent.facilityId, - componentValue, - rawComponent.activationTime, - rawComponent.isChild, - rawComponent.parentId - ); - - components.push(component); - } - - for (let component of components) { - if (component.isChild) { - for (let parentComponent of components) { - if (component.parentId === parentComponent.componentId) { - component.parent = parentComponent; - break; - } - } - } - } - - return components; -} - -/** - * Gets all components that don't belong to a facility in a given region. - * @param {Number} id must be an integer. - * @returns {Array} array of component objects if successful, null otherwise. - */ -const getComponentUnusedByRegionId = async (id) => { - const rawComponents = await knex - .select('*') - .from(constants.TABLE_COMPONENT) - .where(constants.COLUMN_REGION_ID, id) - .whereNull(constants.COLUMN_FACILITY_ID) - .catch(e => { - console.error(e); - }); - - const componentTypes = await getComponentTypeAll(); - - const resources = await resourceServices.getResourceAll(); - - if (rawComponents.length === 0 || componentTypes === null || resources === null) return null; - - let components = []; - - for (let rawComponent of rawComponents) { - let componentValue = rawComponent.value; - - if (rawComponent.componentTypeId === 3) { - componentValue = resources[parseInt(rawComponent.value.split(';')[1]) - 1] - } - - let component = new Component( - rawComponent.componentId, - rawComponent.name, - componentTypes[rawComponent.componentTypeId - 1], - rawComponent.regionId, - rawComponent.facilityId, - componentValue, - rawComponent.activationTime, - rawComponent.isChild, - rawComponent.parentId - ); - - components.push(component); - } - - for (let component of components) { - if (component.isChild) { - for (let parentComponent of components) { - if (component.parentId === parentComponent.componentId) { - component.parent = parentComponent; - break; - } - } - } - } - - return components; -} - -/** - * Creates a new component. - * @param {Component} component must be a component object. - * @returns {Boolean} true if successful, false otherwise. - */ -const addComponent = async (component) => { - let resStatus = "OK"; - - let newValue = null; - - if (component.value !== null) { - if (typeof (component.value) === 'number') newValue = `i;${component.value}`; - else if (typeof (component.value) === 'string') newValue = `s;${component.value}`; - else if (typeof (component.value) === 'object') newValue = `i;${component.value.resourceId}`; - } - - let newIsChild = 0; - let newParentId = null; - - if (component.isChild) { - newIsChild = 1; - newParentId = component.parentId; - } - - let costCalcReturn = await calculateComponentCost(component.cost, component.regionId) - - if(costCalcReturn === "Cost exceeding treasury amount"){ - resStatus = costCalcReturn; - return resStatus - } - - await knex - .insert({ - name: component.componentName, - componentTypeId: component.componentType.componentTypeId, - regionId: component.regionId, - facilityId: component.facilityId, - value: newValue, - parentId: newParentId, - activationTime: component.activationTime, - isChild: newIsChild - }) - .into(constants.TABLE_COMPONENT) - .catch(e => { - console.error(e); - resStatus = "SQL Error. Something went wrong when inserting component"; - }); - - return resStatus; -} - -/** - * Calculates component cost by subtracting treasury amount by cost - * @param {Number} cost must be an integer. - * @param {Number} regionId must be an integer. - * @returns {Boolean} true if successful, false otherwise. - */ -const calculateComponentCost = async (cost, regionId) => { - let resStatus = "OK"; - let stateTreasury = await knex - .select(constants.COLUMN_TREASURY_AMT, constants.TABLE_STATE + '.' + constants.COLUMN_STATE_ID) - .from(constants.TABLE_REGION) - .join( - constants.TABLE_STATE, - constants.TABLE_REGION + '.' + constants.COLUMN_STATE_ID, - constants.TABLE_STATE + '.' + constants.COLUMN_STATE_ID - ) - .where(constants.TABLE_REGION + '.' + constants.COLUMN_REGION_ID, regionId); - - let newTreasuryAmt = stateTreasury[0].treasuryAmt - cost; - - if(newTreasuryAmt < 0){ - resStatus = "Cost exceeding treasury amount"; - return resStatus; - } - - await knex(constants.TABLE_STATE) - .update({ treasuryAmt: newTreasuryAmt }) - .where(constants.TABLE_STATE + '.' + constants.COLUMN_STATE_ID, stateTreasury[0].stateId) - .catch(e => { - console.error(e); - resStatus = "SQL Error. Something went wrong when updating Treasury"; - }); - //console.log(resStatus); - return resStatus; -} - -/** - * Updates the information of a component. - * @param {Component} component must be a component object. - * @returns {Boolean} true if successful, false otherwise. - */ -const updateComponent = async (component) => { - let resStatus = true; - - let newValue = null; - - if (component.value !== null) { - if (typeof (component.value) === 'number') newValue = `i;${component.value}`; - else if (typeof (component.value) === 'string') newValue = `s;${component.value}`; - else if (typeof (component.value) === 'object') newValue = `i;${component.value.resourceId}`; - } - - let newIsChild = 0; - let newParentId = null; - - if (component.isChild) { - newIsChild = 1; - newParentId = component.parentId; - } - - await knex(constants.TABLE_COMPONENT) - .where({ componentId: component.componentId }) - .update({ - name: component.componentName, - componentTypeId: component.componentType.componentTypeId, - regionId: component.regionId, - facilityId: component.facilityId, - value: newValue, - parentId: newParentId, - activationTime: component.activationTime, - isChild: newIsChild - }) - .catch(e => { - console.error(e); - resStatus = false; - }); - - return resStatus; -} - -/** - * Deletes the component of a given ID. - * If the component was being used by a facility and if the facility was functional, it will be set non-functional. - * @param {Number} id must be an integer. - * @returns {Boolean} true if successful, false otherwise. - */ -const deleteComponentById = async (id) => { - let resStatus = true; - - let facility = await knex - .select(constants.COLUMN_FACILITY_ID) - .from(constants.TABLE_COMPONENT) - .where(constants.COLUMN_COMPONENT_ID, id) - .catch(e => { - console.error(e); - resStatus = false; - }); - - let facilityId; - - if (facility.length !== 0) facilityId = facility[0].facilityId; - - await knex(constants.TABLE_COMPONENT) - .where({ componentId: id }) - .del() - .catch(e => { - console.error(e); - resStatus = false; - }); - - if (resStatus) { - await knex(constants.TABLE_FACILITY) - .where(constants.COLUMN_FACILITY_ID, facilityId) - .update({ - isFunctional: 0 - }) - .catch(e => { - console.error(e); - resStatus = false; - }); - } - - return resStatus; -} - -/** - * Gets all component types. - * @returns {Array} array of component type objects if successful, null otherwise. - */ -const getComponentTypeAll = async () => { - const rawComponentTypes = await knex - .select('*') - .from(constants.TABLE_COMPONENT_TYPE) - .catch(e => { - console.error(e); - }); - - if (rawComponentTypes.length === 0) return null; - - let componentTypes = []; - - for (let rawComponentType of rawComponentTypes) { - let componentType = new ComponentType(rawComponentType.componentTypeId, rawComponentType.name); - - componentTypes.push(componentType); - } - - return componentTypes; -}; - -/** - * Sorts given components into parent-child relationships. - * Children will be part of the children property of their parent component. - * @param {Array} components must be an array of component objects. - * @returns {Array} array of component objects if successful, false otherwise. - */ -const sortChildComponents = async (components) => { - let parentComponents = []; - let childComponents = []; - - if (components == null) { - return null; - } - - // separate parent and child components - for (let component of components) { - if (component.isChild) childComponents.push(component); - else parentComponents.push(component); - } - - // console.log(parentComponents); - // console.log(childComponents); - - while (childComponents.length > 0) { - let newParentComponents = []; - - for (let parentComponent of parentComponents) { - let childComponentsNum = childComponents.length; - - for (let i = 0; i < childComponentsNum; i++) { - if (childComponents[i].parentId === parentComponent.componentId) { - newParentComponents.push(childComponents[i]); - childComponents.splice(i, 1); - childComponentsNum = childComponents.length; - } - } - } - - parentComponents = parentComponents.concat(newParentComponents); - } - - return parentComponents; -} - -/** - * Creates multiple components at once - * @param {Array} components must be an array of component objects. - * @returns {Boolean} true if successful, false otherwise. - */ -const addMultipleComponents = async (components) => { - let resStatus = true; - - // First, transform array of components into two insertable components arrays - // Parent component(s) array and Child component(s) array - let parentArray = []; - let childrenArray = []; - let mapUniqueIDwithComponentIDDict = {}; - let totalCost = 0; - components.forEach((component) => { - let tempValue = null; - if (component.value !== null) { - if (typeof (component.value) === 'number') tempValue = `i;${component.value}`; - else if (typeof (component.value) === 'string') tempValue = `s;${component.value}`; - else if (typeof (component.value) === 'object') tempValue = `i;${component.value.resourceId}`; - } - - let tempComponent = { - name: component.componentName, - componentTypeId: component.componentType.componentTypeId, - regionId: component.regionId, - facilityId: component.facilityId, - value: tempValue, - activationTime: component.activationTime, - }; - - if (component.isChild) { - tempComponent.isChild = 1; - tempComponent.parentUniqueID = component.parentId; - childrenArray.push(tempComponent); - } else { - tempComponent.isChild = 0; - tempComponent.parentId = null; - parentArray.push(tempComponent); - mapUniqueIDwithComponentIDDict[component.uniqueID] = null; - } - - totalCost += parseInt(component.cost); - }); - - let costCalcReturn = await calculateComponentCost(totalCost, components[0].regionId) - - if(costCalcReturn === "Cost exceeding treasury amount"){ - resStatus = false; - return resStatus - } - - try { - await knex.transaction(async trx => { - await trx.insert(parentArray).into(constants.TABLE_COMPONENT); - - // Get last n inserted components to get their ids - const insertedParentComponents = await trx(constants.TABLE_COMPONENT) - .orderBy(constants.COLUMN_COMPONENT_ID, 'desc') - .limit(parentArray.length) - .pluck(constants.COLUMN_COMPONENT_ID); - - let i = parentArray.length - 1; - for (const uniqueID in mapUniqueIDwithComponentIDDict) { - mapUniqueIDwithComponentIDDict[uniqueID] = insertedParentComponents[i]; - i--; - } - - childrenArray = childrenArray.map(child => { - child.parentId = mapUniqueIDwithComponentIDDict[child.parentUniqueID]; - delete child.parentUniqueID; - return child; - }); - - if(childrenArray.length){ - await trx.insert(childrenArray).into(constants.TABLE_COMPONENT); - } - }) - } catch (error) { - console.error(error); - resStatus = false; - } - - return resStatus; -} - -exports.getComponentByRegionId = getComponentByRegionId; -exports.getComponentByFacilityId = getComponentByFacilityId; -exports.getComponentFunctionalByRegionId = getComponentFunctionalByRegionId; -exports.getComponentResourceFunctionalByStateId = getComponentResourceFunctionalByStateId; -exports.getComponentFunctionalByIds = getComponentFunctionalByIds; -exports.getComponentUnusedByRegionId = getComponentUnusedByRegionId; -exports.addComponent = addComponent; -exports.calculateComponentCost = calculateComponentCost; -exports.updateComponent = updateComponent; -exports.deleteComponentById = deleteComponentById; -exports.getComponentTypeAll = getComponentTypeAll; -exports.sortChildComponents = sortChildComponents; -exports.addMultipleComponents = addMultipleComponents; - -// FOR DEBUGGING -// getComponentByRegionId(1) -// .then(data => { -// sortChildComponents(data) -// .then(test => console.dir(test)); -// }) -//getComponentByFacilityId(1).then(data => console.dir(data)); -//getComponentTypeAll().then(data => console.log(data)); -// deleteComponentById(5) -// .then(data => console.log(data)); -//getComponentFunctionalByRegionId(1).then(data => console.log(data)); - //getComponentUnusedByRegionId(1).then(data => console.log(data)); -// getComponentResourceFunctionalByStateId(8).then(data => console.log(data)); -//calculateComponentCost(8000, 1); \ No newline at end of file +const config = require('./config.json'); + +const { constants } = config; +const resourceServices = require('./resourceServices'); +const dbContext = require('../repository/DbContext'); + +const knex = dbContext.getKnexObject(); + +const Component = require('../models/componentModel'); +const ComponentType = require('../models/componentTypeModel'); + +/** + * Gets all component types. + * @returns {Array} array of component type objects if successful, null otherwise. + */ +const getComponentTypeAll = async () => { + const rawComponentTypes = await knex + .select('*') + .from(constants.TABLE_COMPONENT_TYPE) + .catch((e) => { + console.error(e); + }); + + if (rawComponentTypes.length === 0) return null; + + const componentTypes = []; + + rawComponentTypes.forEach((rawComponentType) => { + const componentType = new ComponentType( + rawComponentType.componentTypeId, + rawComponentType.name, + ); + + componentTypes.push(componentType); + }); + + return componentTypes; +}; + +/** + * Gets all components of a given region. + * @param {Number} id must be an integer. + * @returns {Array} array of component objects if successful, null otherwise. + */ +const getComponentByRegionId = async (id) => { + const rawComponents = await knex + .select('*') + .from(constants.TABLE_COMPONENT) + .where(constants.COLUMN_REGION_ID, id) + .catch((e) => { + console.error(e); + }); + + const componentTypes = await getComponentTypeAll(); + + const resources = await resourceServices.getResourceAll(); + + if (rawComponents.length === 0 || componentTypes === null || resources === null) return null; + + const components = []; + + for (const rawComponent of rawComponents) { + let componentValue = rawComponent.value; + + if (rawComponent.componentTypeId === 3) { + componentValue = resources[parseInt(rawComponent.value.split(';')[1]) - 1]; + } + + const component = new Component( + rawComponent.componentId, + rawComponent.name, + componentTypes[rawComponent.componentTypeId - 1], + rawComponent.regionId, + rawComponent.facilityId, + componentValue, + rawComponent.activationTime, + rawComponent.isChild, + rawComponent.parentId, + ); + + components.push(component); + } + + for (const component of components) { + if (component.isChild) { + for (const parentComponent of components) { + if (component.parentId === parentComponent.componentId) { + component.parent = parentComponent; + break; + } + } + } + } + + return components; +}; + +/** + * Gets all components of a given facility. + * @param {Number} id must be an integer. + * @returns {Array} array of component objects if successful, null otherwise. + */ +const getComponentByFacilityId = async (id) => { + const rawComponents = await knex + .select('*') + .from(constants.TABLE_COMPONENT) + .where(constants.COLUMN_FACILITY_ID, id) + .catch((e) => { + console.error(e); + }); + + const componentTypes = await getComponentTypeAll(); + + const resources = await resourceServices.getResourceAll(); + + if (rawComponents.length === 0 || componentTypes === null || resources === null) return null; + + const components = []; + + for (const rawComponent of rawComponents) { + let componentValue = rawComponent.value; + + if (rawComponent.componentTypeId === 3) { + componentValue = resources[parseInt(rawComponent.value.split(';')[1]) - 1]; + } + + const component = new Component( + rawComponent.componentId, + rawComponent.name, + componentTypes[rawComponent.componentTypeId - 1], + rawComponent.regionId, + rawComponent.facilityId, + componentValue, + rawComponent.activationTime, + rawComponent.isChild, + rawComponent.parentId, + ); + + components.push(component); + } + + for (const component of components) { + if (component.isChild) { + for (const parentComponent of components) { + if (component.parentId === parentComponent.componentId) { + component.parent = parentComponent; + break; + } + } + } + } + + return components; +}; + +/** + * Gets all functional components of a given region. + * @param {Number} id must be an integer. + * @returns {Array} array of component objects if successful, null otherwise. + */ +const getComponentFunctionalByRegionId = async (id) => { + const rawComponents = await knex + .select(`${constants.TABLE_COMPONENT}.` + '*') + .from(constants.TABLE_COMPONENT) + .innerJoin( + constants.TABLE_FACILITY, + `${constants.TABLE_COMPONENT}.${constants.COLUMN_FACILITY_ID}`, + `${constants.TABLE_FACILITY}.${constants.COLUMN_FACILITY_ID}`, + ) + .where(`${constants.TABLE_COMPONENT}.${constants.COLUMN_REGION_ID}`, id) + .andWhere(`${constants.TABLE_FACILITY}.${constants.COLUMN_IS_FUNCTIONAL}`, 1) + .catch((e) => { + console.error(e); + }); + + const componentTypes = await getComponentTypeAll(); + + const resources = await resourceServices.getResourceAll(); + + if (rawComponents.length === 0 || componentTypes === null || resources === null) return null; + + const components = []; + + for (const rawComponent of rawComponents) { + let componentValue = rawComponent.value; + + if (rawComponent.componentTypeId === 3) { + componentValue = resources[parseInt(rawComponent.value.split(';')[1]) - 1]; + } + + const component = new Component( + rawComponent.componentId, + rawComponent.name, + componentTypes[rawComponent.componentTypeId - 1], + rawComponent.regionId, + rawComponent.facilityId, + componentValue, + rawComponent.activationTime, + rawComponent.isChild, + rawComponent.parentId, + ); + + components.push(component); + } + + for (const component of components) { + if (component.isChild) { + for (const parentComponent of components) { + if (component.parentId === parentComponent.componentId) { + component.parent = parentComponent; + break; + } + } + } + } + + return components; +}; + +/** + * Gets all functional resource components of a given state. + * @param {Number} id must be an integer. + * @returns {Array} array of component objects if successful, null otherwise. + */ +const getComponentResourceFunctionalByStateId = async (id) => { + const rawComponents = await knex + .select(`${constants.TABLE_COMPONENT}.` + '*') + .from(constants.TABLE_COMPONENT) + .innerJoin( + constants.TABLE_FACILITY, + `${constants.TABLE_COMPONENT}.${constants.COLUMN_FACILITY_ID}`, + `${constants.TABLE_FACILITY}.${constants.COLUMN_FACILITY_ID}`, + ) + .leftJoin( + constants.TABLE_REGION, + `${constants.TABLE_COMPONENT}.${constants.COLUMN_REGION_ID}`, + `${constants.TABLE_REGION}.${constants.COLUMN_REGION_ID}`, + ) + .where(`${constants.TABLE_REGION}.${constants.COLUMN_STATE_ID}`, id) + .andWhere(`${constants.TABLE_FACILITY}.${constants.COLUMN_IS_FUNCTIONAL}`, 1) + .andWhere(constants.COLUMN_COMPONENT_TYPE_ID, 3) + .catch((e) => { + console.error(e); + }); + + const componentTypes = await getComponentTypeAll(); + + const resources = await resourceServices.getResourceAll(); + + if (rawComponents.length === 0 || componentTypes === null || resources === null) return null; + + const components = []; + + for (const rawComponent of rawComponents) { + let componentValue = rawComponent.value; + + if (rawComponent.componentTypeId === 3) { + componentValue = resources[parseInt(rawComponent.value.split(';')[1]) - 1]; + } + + const component = new Component( + rawComponent.componentId, + rawComponent.name, + componentTypes[rawComponent.componentTypeId - 1], + rawComponent.regionId, + rawComponent.facilityId, + componentValue, + rawComponent.activationTime, + rawComponent.isChild, + rawComponent.parentId, + ); + + components.push(component); + } + + for (const component of components) { + if (component.isChild) { + for (const parentComponent of components) { + if (component.parentId === parentComponent.componentId) { + component.parent = parentComponent; + break; + } + } + } + } + + return components; +}; + +/** + * Gets all functional components of the given IDs. + * @param {Array} ids must be an array of integers. + * @returns {Array} array of component objects if successful, null otherwise. + */ +const getComponentFunctionalByIds = async (ids) => { + const rawComponents = await knex + .select(`${constants.TABLE_COMPONENT}.` + '*') + .from(constants.TABLE_COMPONENT) + .innerJoin( + constants.TABLE_FACILITY, + `${constants.TABLE_COMPONENT}.${constants.COLUMN_FACILITY_ID}`, + `${constants.TABLE_FACILITY}.${constants.COLUMN_FACILITY_ID}`, + ) + .whereIn(constants.COLUMN_COMPONENT_ID, ids) + .andWhere(`${constants.TABLE_FACILITY}.${constants.COLUMN_IS_FUNCTIONAL}`, 1) + .catch((e) => { + console.error(e); + }); + + const componentTypes = await getComponentTypeAll(); + + const resources = await resourceServices.getResourceAll(); + + if (rawComponents.length === 0 || componentTypes === null || resources === null) return null; + + const components = []; + + for (const rawComponent of rawComponents) { + let componentValue = rawComponent.value; + + if (rawComponent.componentTypeId === 3) { + componentValue = resources[parseInt(rawComponent.value.split(';')[1]) - 1]; + } + + const component = new Component( + rawComponent.componentId, + rawComponent.name, + componentTypes[rawComponent.componentTypeId - 1], + rawComponent.regionId, + rawComponent.facilityId, + componentValue, + rawComponent.activationTime, + rawComponent.isChild, + rawComponent.parentId, + ); + + components.push(component); + } + + for (const component of components) { + if (component.isChild) { + for (const parentComponent of components) { + if (component.parentId === parentComponent.componentId) { + component.parent = parentComponent; + break; + } + } + } + } + + return components; +}; + +/** + * Gets all components that don't belong to a facility in a given region. + * @param {Number} id must be an integer. + * @returns {Array} array of component objects if successful, null otherwise. + */ +const getComponentUnusedByRegionId = async (id) => { + const rawComponents = await knex + .select('*') + .from(constants.TABLE_COMPONENT) + .where(constants.COLUMN_REGION_ID, id) + .whereNull(constants.COLUMN_FACILITY_ID) + .catch((e) => { + console.error(e); + }); + + const componentTypes = await getComponentTypeAll(); + + const resources = await resourceServices.getResourceAll(); + + if (rawComponents.length === 0 || componentTypes === null || resources === null) return null; + + const components = []; + + for (const rawComponent of rawComponents) { + let componentValue = rawComponent.value; + + if (rawComponent.componentTypeId === 3) { + componentValue = resources[parseInt(rawComponent.value.split(';')[1]) - 1]; + } + + const component = new Component( + rawComponent.componentId, + rawComponent.name, + componentTypes[rawComponent.componentTypeId - 1], + rawComponent.regionId, + rawComponent.facilityId, + componentValue, + rawComponent.activationTime, + rawComponent.isChild, + rawComponent.parentId, + ); + + components.push(component); + } + + for (const component of components) { + if (component.isChild) { + for (const parentComponent of components) { + if (component.parentId === parentComponent.componentId) { + component.parent = parentComponent; + break; + } + } + } + } + + return components; +}; + +/** + * Creates a new component. + * @param {Component} component must be a component object. + * @returns {Boolean} true if successful, false otherwise. + */ +const addComponent = async (component) => { + let resStatus = 'OK'; + + let newValue = null; + + if (component.value !== null) { + if (typeof (component.value) === 'number') newValue = `i;${component.value}`; + else if (typeof (component.value) === 'string') newValue = `s;${component.value}`; + else if (typeof (component.value) === 'object') newValue = `i;${component.value.resourceId}`; + } + + let newIsChild = 0; + let newParentId = null; + + if (component.isChild) { + newIsChild = 1; + newParentId = component.parentId; + } + + const costCalcReturn = await calculateComponentCost(component.cost, component.regionId); + + if (costCalcReturn === 'Cost exceeding treasury amount') { + resStatus = costCalcReturn; + return resStatus; + } + + await knex + .insert({ + name: component.componentName, + componentTypeId: component.componentType.componentTypeId, + regionId: component.regionId, + facilityId: component.facilityId, + value: newValue, + parentId: newParentId, + activationTime: component.activationTime, + isChild: newIsChild, + }) + .into(constants.TABLE_COMPONENT) + .catch((e) => { + console.error(e); + resStatus = 'SQL Error. Something went wrong when inserting component'; + }); + + return resStatus; +}; + +/** + * Calculates component cost by subtracting treasury amount by cost + * @param {Number} cost must be an integer. + * @param {Number} regionId must be an integer. + * @returns {Boolean} true if successful, false otherwise. + */ +const calculateComponentCost = async (cost, regionId) => { + let resStatus = 'OK'; + const stateTreasury = await knex + .select(constants.COLUMN_TREASURY_AMT, `${constants.TABLE_STATE}.${constants.COLUMN_STATE_ID}`) + .from(constants.TABLE_REGION) + .join( + constants.TABLE_STATE, + `${constants.TABLE_REGION}.${constants.COLUMN_STATE_ID}`, + `${constants.TABLE_STATE}.${constants.COLUMN_STATE_ID}`, + ) + .where(`${constants.TABLE_REGION}.${constants.COLUMN_REGION_ID}`, regionId); + + const newTreasuryAmt = stateTreasury[0].treasuryAmt - cost; + + if (newTreasuryAmt < 0) { + resStatus = 'Cost exceeding treasury amount'; + return resStatus; + } + + await knex(constants.TABLE_STATE) + .update({ treasuryAmt: newTreasuryAmt }) + .where(`${constants.TABLE_STATE}.${constants.COLUMN_STATE_ID}`, stateTreasury[0].stateId) + .catch((e) => { + console.error(e); + resStatus = 'SQL Error. Something went wrong when updating Treasury'; + }); + // console.log(resStatus); + return resStatus; +}; + +/** + * Updates the information of a component. + * @param {Component} component must be a component object. + * @returns {Boolean} true if successful, false otherwise. + */ +const updateComponent = async (component) => { + let resStatus = true; + + let newValue = null; + + if (component.value !== null) { + if (typeof (component.value) === 'number') newValue = `i;${component.value}`; + else if (typeof (component.value) === 'string') newValue = `s;${component.value}`; + else if (typeof (component.value) === 'object') newValue = `i;${component.value.resourceId}`; + } + + let newIsChild = 0; + let newParentId = null; + + if (component.isChild) { + newIsChild = 1; + newParentId = component.parentId; + } + + await knex(constants.TABLE_COMPONENT) + .where({ componentId: component.componentId }) + .update({ + name: component.componentName, + componentTypeId: component.componentType.componentTypeId, + regionId: component.regionId, + facilityId: component.facilityId, + value: newValue, + parentId: newParentId, + activationTime: component.activationTime, + isChild: newIsChild, + }) + .catch((e) => { + console.error(e); + resStatus = false; + }); + + return resStatus; +}; + +/** + * Deletes the component of a given ID. + * If the component was being used by a facility and if the facility was functional, it will be set non-functional. + * @param {Number} id must be an integer. + * @returns {Boolean} true if successful, false otherwise. + */ +const deleteComponentById = async (id) => { + let resStatus = true; + + const facility = await knex + .select(constants.COLUMN_FACILITY_ID) + .from(constants.TABLE_COMPONENT) + .where(constants.COLUMN_COMPONENT_ID, id) + .catch((e) => { + console.error(e); + resStatus = false; + }); + + let facilityId; + + if (facility.length !== 0) facilityId = facility[0].facilityId; + + await knex(constants.TABLE_COMPONENT) + .where({ componentId: id }) + .del() + .catch((e) => { + console.error(e); + resStatus = false; + }); + + if (resStatus) { + await knex(constants.TABLE_FACILITY) + .where(constants.COLUMN_FACILITY_ID, facilityId) + .update({ + isFunctional: 0, + }) + .catch((e) => { + console.error(e); + resStatus = false; + }); + } + + return resStatus; +}; + +/** + * Sorts given components into parent-child relationships. + * Children will be part of the children property of their parent component. + * @param {Array} components must be an array of component objects. + * @returns {Array} array of component objects if successful, false otherwise. + */ +const sortChildComponents = async (components) => { + let parentComponents = []; + const childComponents = []; + + if (components == null) { + return null; + } + + // separate parent and child components + for (const component of components) { + if (component.isChild) childComponents.push(component); + else parentComponents.push(component); + } + + // console.log(parentComponents); + // console.log(childComponents); + + while (childComponents.length > 0) { + const newParentComponents = []; + + for (const parentComponent of parentComponents) { + let childComponentsNum = childComponents.length; + + for (let i = 0; i < childComponentsNum; i++) { + if (childComponents[i].parentId === parentComponent.componentId) { + newParentComponents.push(childComponents[i]); + childComponents.splice(i, 1); + childComponentsNum = childComponents.length; + } + } + } + + parentComponents = parentComponents.concat(newParentComponents); + } + + return parentComponents; +}; + +/** + * Creates multiple components at once + * @param {Array} components must be an array of component objects. + * @returns {Boolean} true if successful, false otherwise. + */ +const addMultipleComponents = async (components) => { + let resStatus = true; + + // First, transform array of components into two insertable components arrays + // Parent component(s) array and Child component(s) array + const parentArray = []; + let childrenArray = []; + const mapUniqueIDwithComponentIDDict = {}; + let totalCost = 0; + components.forEach((component) => { + let tempValue = null; + if (component.value !== null) { + if (typeof (component.value) === 'number') tempValue = `i;${component.value}`; + else if (typeof (component.value) === 'string') tempValue = `s;${component.value}`; + else if (typeof (component.value) === 'object') tempValue = `i;${component.value.resourceId}`; + } + + const tempComponent = { + name: component.componentName, + componentTypeId: component.componentType.componentTypeId, + regionId: component.regionId, + facilityId: component.facilityId, + value: tempValue, + activationTime: component.activationTime, + }; + + if (component.isChild) { + tempComponent.isChild = 1; + tempComponent.parentUniqueID = component.parentId; + childrenArray.push(tempComponent); + } else { + tempComponent.isChild = 0; + tempComponent.parentId = null; + parentArray.push(tempComponent); + mapUniqueIDwithComponentIDDict[component.uniqueID] = null; + } + + totalCost += parseInt(component.cost); + }); + + const costCalcReturn = await calculateComponentCost(totalCost, components[0].regionId); + + if (costCalcReturn === 'Cost exceeding treasury amount') { + resStatus = false; + return resStatus; + } + + try { + await knex.transaction(async (trx) => { + await trx.insert(parentArray).into(constants.TABLE_COMPONENT); + + // Get last n inserted components to get their ids + const insertedParentComponents = await trx(constants.TABLE_COMPONENT) + .orderBy(constants.COLUMN_COMPONENT_ID, 'desc') + .limit(parentArray.length) + .pluck(constants.COLUMN_COMPONENT_ID); + + let i = parentArray.length - 1; + for (const uniqueID in mapUniqueIDwithComponentIDDict) { + mapUniqueIDwithComponentIDDict[uniqueID] = insertedParentComponents[i]; + i--; + } + + childrenArray = childrenArray.map((child) => { + child.parentId = mapUniqueIDwithComponentIDDict[child.parentUniqueID]; + delete child.parentUniqueID; + return child; + }); + + if (childrenArray.length) { + await trx.insert(childrenArray).into(constants.TABLE_COMPONENT); + } + }); + } catch (error) { + console.error(error); + resStatus = false; + } + + return resStatus; +}; + +exports.getComponentByRegionId = getComponentByRegionId; +exports.getComponentByFacilityId = getComponentByFacilityId; +exports.getComponentFunctionalByRegionId = getComponentFunctionalByRegionId; +exports.getComponentResourceFunctionalByStateId = getComponentResourceFunctionalByStateId; +exports.getComponentFunctionalByIds = getComponentFunctionalByIds; +exports.getComponentUnusedByRegionId = getComponentUnusedByRegionId; +exports.addComponent = addComponent; +exports.calculateComponentCost = calculateComponentCost; +exports.updateComponent = updateComponent; +exports.deleteComponentById = deleteComponentById; +exports.getComponentTypeAll = getComponentTypeAll; +exports.sortChildComponents = sortChildComponents; +exports.addMultipleComponents = addMultipleComponents; + +// FOR DEBUGGING +// getComponentByRegionId(1) +// .then(data => { +// sortChildComponents(data) +// .then(test => console.dir(test)); +// }) +// getComponentByFacilityId(1).then(data => console.dir(data)); +// getComponentTypeAll().then(data => console.log(data)); +// deleteComponentById(5) +// .then(data => console.log(data)); +// getComponentFunctionalByRegionId(1).then(data => console.log(data)); +// getComponentUnusedByRegionId(1).then(data => console.log(data)); +// getComponentResourceFunctionalByStateId(8).then(data => console.log(data)); +// calculateComponentCost(8000, 1); diff --git a/src/services/componentServices.v2.js b/src/services/componentServices.v2.js new file mode 100644 index 0000000..17dd152 --- /dev/null +++ b/src/services/componentServices.v2.js @@ -0,0 +1,324 @@ +// TEMPORARY FILE TO STORE VERSION 2 OF SOME FUNCTIONS IN componentServices.js +// FUNCTIONS IN THIS FILE WILL BE MOVED INTO THE ORIGINAL FILE ONCE REQUIRED + +const config = require('./config.json'); + +const { constants } = config; +const facilityServices = require(config.paths.facilityServices); +const dbContext = require('../repository/DbContext'); + +const Component = require(config.paths.componentModel); + +const INSUFFICIENT_TREASURY_ERROR_MESSAGE = 'Cost exceeding treasury amount'; + +/** + * Creates a new component. + * @param {Component} component must be a component object. + * @param {String} tempFacilityName temporary facility name which component will be assigned to, + * if it's null, then the component's facility will be assigned with facility id inside the component object + * @returns {Boolean} true if successful, false otherwise. + */ +const addComponent = async ({ component, tempFacilityName = null }) => { + let resStatus = 'OK'; + + const knex = dbContext.getKnexObject(); + + const newValue = convertComponentInputValue(component.value); + + let newIsChild = 0; + let newParentId = null; + + if (component.isChild) { + newIsChild = 1; + newParentId = component.parentId; + } + + const isTreasurySufficient = await calculateComponentCost(component.cost, component.regionId); + + if (!isTreasurySufficient) { + return INSUFFICIENT_TREASURY_ERROR_MESSAGE; + } + + if (tempFacilityName != null) { + const mapKey = null; + component.facilityId = mapKey; + const tempFacilityMap = new Map([[mapKey, tempFacilityName]]); + const facilityInsertStatus = await allocateTemporaryFacilitiesToComponents([component], tempFacilityMap); + if (!facilityInsertStatus) { + resStatus = 'There is an error when inserting temporary facility'; + return resStatus; + } + } + + await knex + .insert({ + name: component.componentName, + componentTypeId: component.componentType.componentTypeId, + regionId: component.regionId, + facilityId: component.facilityId, + value: newValue, + parentId: newParentId, + activationTime: component.activationTime, + isChild: newIsChild, + }) + .into(constants.TABLE_COMPONENT) + .catch((e) => { + console.error(e); + resStatus = 'SQL Error. Something went wrong when inserting component'; + }); + + return resStatus; +}; + +/** + * Updates the information of a component. + * @param {Component} component must be a component object. + * @param {String} tempFacilityName temporary facility name which component will be assigned to, + * if it's null, then the component's facility will be assigned with facility id inside the component object + * @returns {Boolean} true if successful, false otherwise. + */ +const updateComponent = async ({ component, tempFacilityName = null }) => { + let resStatus = true; + + const knex = dbContext.getKnexObject(); + + const newValue = convertComponentInputValue(component.value); + + let newIsChild = 0; + let newParentId = null; + + if (component.isChild) { + newIsChild = 1; + newParentId = component.parentId; + } + + if (tempFacilityName != null) { + const mapKey = null; + component.facilityId = mapKey; + const tempFacilityMap = new Map([[mapKey, tempFacilityName]]); + const facilityInsertStatus = await allocateTemporaryFacilitiesToComponents([component], tempFacilityMap); + if (!facilityInsertStatus) { + resStatus = 'There is an error when inserting temporary facility'; + return resStatus; + } + } + + await knex(constants.TABLE_COMPONENT) + .where({ componentId: component.componentId }) + .update({ + name: component.componentName, + componentTypeId: component.componentType.componentTypeId, + regionId: component.regionId, + facilityId: component.facilityId, + value: newValue, + parentId: newParentId, + activationTime: component.activationTime, + isChild: newIsChild, + }) + .catch((e) => { + console.error(e); + resStatus = false; + }); + + return resStatus; +}; + +/** + * Creates multiple components at once + * @param {Array} components must be an array of component objects. + * @param {Map} tempFacilityMap A map with temporary facility ID as the key and its name as the value + * @returns {Boolean} true if successful, false otherwise. + */ +const addMultipleComponents = async ({ components, tempFacilityMap = null }) => { + let resStatus = true; + + const knex = dbContext.getKnexObject(); + + // First, transform array of components into two insertable components arrays + // Parent component(s) array and Child component(s) array + const parentArray = []; + let childrenArray = []; + const mapUniqueIDwithComponentIDDict = {}; + let totalCost = 0; + components.forEach((component) => { + const tempValue = convertComponentInputValue(component.value); + + const tempComponent = { + name: component.componentName, + componentTypeId: component.componentType.componentTypeId, + regionId: component.regionId, + facilityId: component.facilityId, + value: tempValue, + activationTime: component.activationTime, + }; + + if (component.isChild) { + tempComponent.isChild = 1; + tempComponent.parentUniqueID = component.parentId; + childrenArray.push(tempComponent); + } else { + tempComponent.isChild = 0; + tempComponent.parentId = null; + parentArray.push(tempComponent); + mapUniqueIDwithComponentIDDict[component.uniqueID] = null; + } + + totalCost += parseInt(component.cost); + }); + + const isTreasurySufficient = await calculateComponentCost(totalCost, components[0].regionId); + + if (!isTreasurySufficient) { + return false; + } + + try { + if (tempFacilityMap != null) { + const facilityInsertStatus = await allocateTemporaryFacilitiesToComponents(parentArray, tempFacilityMap); + if (!facilityInsertStatus) { + throw 'There is an error when inserting temporary facilities'; + } + } + await knex.transaction(async (trx) => { + await trx.insert(parentArray).into(constants.TABLE_COMPONENT); + + // Get last n inserted components to get their ids + const insertedParentComponents = await trx(constants.TABLE_COMPONENT) + .orderBy(constants.COLUMN_COMPONENT_ID, 'desc') + .limit(parentArray.length) + .pluck(constants.COLUMN_COMPONENT_ID); + + let i = parentArray.length - 1; + for (const uniqueID in mapUniqueIDwithComponentIDDict) { + mapUniqueIDwithComponentIDDict[uniqueID] = insertedParentComponents[i]; + i--; + } + + childrenArray = childrenArray.map((child) => { + child.parentId = mapUniqueIDwithComponentIDDict[child.parentUniqueID]; + delete child.parentUniqueID; + return child; + }); + + if (childrenArray.length) { + await trx.insert(childrenArray).into(constants.TABLE_COMPONENT); + } + }); + } catch (error) { + console.error(error); + resStatus = false; + } + + return resStatus; +}; + +/** + * Calculates component cost by subtracting treasury amount by cost + * @param {Number} cost must be an integer. + * @param {Number} regionId must be an integer. + * @returns {Boolean} true if successful, false otherwise. + */ +const calculateComponentCost = async (cost, regionId) => { + let resStatus = true; + const knex = dbContext.getKnexObject(); + const stateTreasury = await knex + .select(constants.COLUMN_TREASURY_AMT, `${constants.TABLE_STATE}.${constants.COLUMN_STATE_ID}`) + .from(constants.TABLE_REGION) + .join( + constants.TABLE_STATE, + `${constants.TABLE_REGION}.${constants.COLUMN_STATE_ID}`, + `${constants.TABLE_STATE}.${constants.COLUMN_STATE_ID}`, + ) + .where(`${constants.TABLE_REGION}.${constants.COLUMN_REGION_ID}`, regionId); + + const newTreasuryAmt = stateTreasury[0].treasuryAmt - cost; + + if (newTreasuryAmt < 0) { + return false; + } + + await knex(constants.TABLE_STATE) + .update({ treasuryAmt: newTreasuryAmt }) + .where(`${constants.TABLE_STATE}.${constants.COLUMN_STATE_ID}`, stateTreasury[0].stateId) + .catch((e) => { + console.error(e); + resStatus = false; + }); + return resStatus; +}; + +const convertComponentInputValue = (value) => { + let newValue = null; + + if (value !== null) { + if (typeof (value) === 'number') newValue = `i;${value}`; + else if (typeof (value) === 'string') newValue = `s;${value}`; + else if (typeof (value) === 'object') newValue = `i;${value.resourceId}`; + } + + return newValue; +}; + +/** + * Create temporary facilities and assign them into input components + * @param {Array} components must be an array of component objects. + * @param {Map} tempFacilityMap A map with temporary facility ID as the key and its name as the value + * @returns {Boolean} true if successful, false otherwise. + */ +const allocateTemporaryFacilitiesToComponents = async (components, tempFacilityMap) => { + const { regionId } = components[0]; + const insertedFacilityMap = await processTemporaryFacilities(tempFacilityMap, regionId); + if (insertedFacilityMap == null) { + return false; + } + components.forEach((component) => { + if (insertedFacilityMap.has(component.facilityId)) { + component.facilityId = insertedFacilityMap.get(component.facilityId); + } + }); + return true; +}; + +/** + * Process a map with temporary facility and transforms it into + * a map with temporary facility ID as the key and its Database facility ID as the value + * @param {Map} tempFacilityMap A map with temporary facility ID as the key and its name as the value + * @param {Number} regionId Region ID for the facilities + * @returns {Boolean} true if successful, false otherwise + */ +const processTemporaryFacilities = async (tempFacilityMap, regionId) => { + const temporaryFacilities = []; + for (const facilityName of tempFacilityMap.values()) { + temporaryFacilities.push({ + regionId, + facilityName, + isFunctional: false, + }); + } + const facilityInsertStatus = await facilityServices.addMultipleFacilities(temporaryFacilities); + if (!facilityInsertStatus) { + return null; + } + return obtainCreatedTemporaryFacilities(tempFacilityMap); +}; + +const obtainCreatedTemporaryFacilities = async (tempFacilityMap) => { + const knex = dbContext.getKnexObject(); + const insertedFacilityIDs = await knex(constants.TABLE_FACILITY) + .orderBy(constants.COLUMN_FACILITY_ID, 'desc') + .limit(tempFacilityMap.size) + .pluck(constants.COLUMN_FACILITY_ID); + + let i = tempFacilityMap.size - 1; + for (const tempFacilityID of tempFacilityMap.keys()) { + tempFacilityMap.set(tempFacilityID, insertedFacilityIDs[i]); + i--; + } + return tempFacilityMap; +}; + +module.exports = { + addComponent, + updateComponent, + addMultipleComponents, +}; diff --git a/src/services/config.json b/src/services/config.json index 915d5a9..8ebd191 100644 --- a/src/services/config.json +++ b/src/services/config.json @@ -1,4 +1,11 @@ { + "environment": { + "name": { + "Development": "dev", + "Test": "test", + "Production": "prod" + } + }, "knexConfig": { "client": "sqlite3", "connection": { @@ -7,9 +14,11 @@ }, "sqlite_config": { "filepath": [ - "db/ces.db", - "resources/app/db/ces.db" - ] + "db/", + "resources/app/db/" + ], + "production_db": "ces.db", + "test_db": "ces.test.db" }, "constants": { "TABLE_BIOME": "MsBiome", @@ -25,6 +34,7 @@ "TABLE_STATE": "MsState", "TABLE_TRADE_AGREEMENT_DETAIL": "DetailTradeAgreement", "TABLE_TRADE_AGREEMENT_HEADER": "HeaderTradeAgreement", + "TABLE_SQLITE_SEQUENCE": "sqlite_sequence", "COLUMN_NAME": "name", "COLUMN_POPULATION": "population", "COLUMN_DESC": "desc", diff --git a/src/services/facilityServices.js b/src/services/facilityServices.js index 1fcd094..6936d48 100644 --- a/src/services/facilityServices.js +++ b/src/services/facilityServices.js @@ -1,260 +1,284 @@ -const config = require('./config.json'); -const constants = config.constants; -const componentServices = require(config.paths.componentServices); -const dbContext = require('../repository/DbContext'); -const knex = dbContext.getKnexObject(); - -const Facility = require(config.paths.facilityModel); -const RootComponentCollection = require(config.paths.rootComponentCollection); - -/** - * Gets all facilities of a given region. - * @param {Number} id must be an integer. - * @returns {Array} array of facility objects if successful, null otherwise. - */ -const getFacilitiesByRegionId = async (id) => { - let rawFacilities = await knex - .select('*') - .from(constants.TABLE_FACILITY) - .where(constants.COLUMN_REGION_ID, id) - .catch(e => { - console.error(e); - }); - - let components = await componentServices.getComponentByRegionId(id); - let sortedComponents = await componentServices.sortChildComponents(components); - let rootComponentCollection = new RootComponentCollection(components); - - // if (rawFacilities.length === 0 || sortedComponents === null) return null; - if (rawFacilities.length === 0 || rootComponentCollection === null) return null; - - let facilities = []; - - for (let rawFacility of rawFacilities) { - let facility = new Facility( - rawFacility.facilityId, - rawFacility.regionId, - rawFacility.name, - rawFacility.isFunctional - ); - - let facilityComponents = []; - - // for (let component of sortedComponents) { - for (let componentId in rootComponentCollection.componentDict) { - // if (component.facilityId === facility.facilityId) { - // facilityComponents.push(component); - // } - let rootComponent = rootComponentCollection.findRoot(componentId); - if (rootComponent.facilityId === facility.facilityId) { - facilityComponents.push(rootComponentCollection.componentDict[componentId]); - } - } - - facility.summarise(facilityComponents); - - facilities.push(facility); - } - - return facilities; -} - -const getFacilitiesByStateId = async (id) => { - let facilityList = await knex(constants.TABLE_FACILITY) - .select( - constants.TABLE_REGION + "." + constants.COLUMN_NAME + " AS regionName", - constants.TABLE_FACILITY +"." + constants.COLUMN_NAME + " AS facilityName", - constants.COLUMN_IS_FUNCTIONAL - ) - .leftJoin( - constants.TABLE_REGION, - constants.TABLE_FACILITY + "." + constants.COLUMN_REGION_ID, - constants.TABLE_REGION + "." + constants.COLUMN_REGION_ID - ) - .where(constants.COLUMN_STATE_ID, id) - .catch((e) => { - console.error(e); - resValue = -1; - }); - - resValue = facilityList; - - return resValue; -}; - -/** - * Gets the number of functional facilities that the state of the given ID has. - * @param {Number} id must be an integer. - * @returns a positive integer if successful, -1 otherwise. - */ -const getFacilityCountByStateId = async (id) => { - let resValue = 0; - - let facilityCount = await knex(constants.TABLE_FACILITY) - .count(constants.COLUMN_FACILITY_ID + ' AS count') - .leftJoin( - constants.TABLE_REGION, - constants.TABLE_FACILITY + '.' + constants.COLUMN_REGION_ID, - constants.TABLE_REGION + '.' + constants.COLUMN_REGION_ID - ) - .where(constants.COLUMN_STATE_ID, id) - .andWhere(constants.COLUMN_IS_FUNCTIONAL, 1) - .catch(e => { - console.error(e); - resValue = -1; - }); - - resValue = facilityCount[0].count; - - return resValue; -} - -/** - * Creates a new facility. - * @param {Facility} facility must be a facility object. - * @returns {Boolean} true if successful, false otherwise. - */ -const addFacility = async (facility) => { - let resValue = true; - - let newIsFunctional = 0; - - if (facility.isFunctional) newIsFunctional = 1; - - await knex - .insert({ - regionId: facility.regionId, - name: facility.facilityName, - isFunctional: newIsFunctional - }) - .into(constants.TABLE_FACILITY) - .catch(e => { - console.error(e); - resValue = false; - }) - - return resValue; -} - -/** - * Updates the information of a facility. - * @param {Facility} facility must be a facility object. - * @returns {Boolean} true if successful, false otherwise. - */ -const updateFacility = async (facility) => { - let resValue = true; - - let newIsFunctional = 0; - - if (facility.isFunctional) newIsFunctional = 1; - - await knex(constants.TABLE_FACILITY) - .where({ facilityId: facility.facilityId }) - .update({ - regionId: facility.regionId, - name: facility.facilityName, - isFunctional: newIsFunctional - }) - .catch(e => { - console.error(e); - resStatus = false; - }); - - return resValue; -} - -/** - * Deletes the facility of a given ID without deleting its components. - * @param {Number} id must be an integer. - * @returns {Boolean} true if successful, false otherwise. - */ -const deleteFacilityById = async (id) => { - let resStatus = true; - - await knex(constants.TABLE_FACILITY) - .where({ facilityId: id }) - .del() - .catch(e => { - console.error(e); - resStatus = false; - }); - - await knex(constants.TABLE_COMPONENT) - .where({ facilityId: id }) - .update({ facilityId: null }) - .catch(e => { - console.error(e); - resStatus = false; - }); - - return resStatus; -} - -/** - * Deletes the facility of a given ID and its components. - * @param {Number} id must be an integer. - * @returns {Boolean} true if successful, false otherwise. - */ -const destroyFacilityById = async (id) => { - let resStatus = true; - - await knex(constants.TABLE_FACILITY) - .where({ facilityId: id }) - .del() - .catch(e => { - console.error(e); - resStatus = false; - }); - - if (resStatus) { - await knex(constants.TABLE_COMPONENT) - .where({ facilityId: id }) - .del() - .catch(e => { - console.error(e); - resStatus = false; - }) - } - - return resStatus; -} - -/** - * Assigns components to a facility. - * @param {Number} facilityId must be an integer. - * @param {Array} componentIds must be an array of integers. - * @returns {Boolean} true if successful, false otherwise. - */ -const assignFacilityComponents = async (facilityId, componentIds) => { - let resStatus = true; - - let promises = []; - - for (let componentId of componentIds) { - let promise = knex(constants.TABLE_COMPONENT) - .where({ componentId: componentId }) - .update({ - facilityId: facilityId - }) - .catch(e => { - console.error(e); - resStatus = false; - }); - - promises.push(promise); - } - - await Promise.all(promises); - - return resStatus; -} - -exports.getFacilitiesByRegionId = getFacilitiesByRegionId; -exports.getFacilityCountByStateId = getFacilityCountByStateId; -exports.addFacility = addFacility; -exports.updateFacility = updateFacility; -exports.deleteFacilityById = deleteFacilityById; -exports.destroyFacilityById = destroyFacilityById; -exports.assignFacilityComponents = assignFacilityComponents; -exports.getFacilitiesByStateId = getFacilitiesByStateId; - +const config = require('./config.json'); + +const { constants } = config; +const componentServices = require(config.paths.componentServices); +const dbContext = require('../repository/DbContext'); + +const knex = dbContext.getKnexObject(); + +const Facility = require(config.paths.facilityModel); +const RootComponentCollection = require(config.paths.rootComponentCollection); + +/** + * Gets all facilities of a given region. + * @param {Number} id must be an integer. + * @returns {Array} array of facility objects if successful, null otherwise. + */ +const getFacilitiesByRegionId = async (id) => { + const rawFacilities = await knex + .select('*') + .from(constants.TABLE_FACILITY) + .where(constants.COLUMN_REGION_ID, id) + .catch((e) => { + console.error(e); + }); + + const components = await componentServices.getComponentByRegionId(id); + const sortedComponents = await componentServices.sortChildComponents(components); + const rootComponentCollection = new RootComponentCollection(components); + + // if (rawFacilities.length === 0 || sortedComponents === null) return null; + if (rawFacilities.length === 0 || rootComponentCollection === null) return null; + + const facilities = []; + + for (const rawFacility of rawFacilities) { + const facility = new Facility( + rawFacility.facilityId, + rawFacility.regionId, + rawFacility.name, + rawFacility.isFunctional, + ); + + const facilityComponents = []; + + // for (let component of sortedComponents) { + for (const componentId in rootComponentCollection.componentDict) { + // if (component.facilityId === facility.facilityId) { + // facilityComponents.push(component); + // } + const rootComponent = rootComponentCollection.findRoot(componentId); + if (rootComponent.facilityId === facility.facilityId) { + facilityComponents.push(rootComponentCollection.componentDict[componentId]); + } + } + + facility.summarise(facilityComponents); + + facilities.push(facility); + } + + return facilities; +}; + +const getFacilitiesByStateId = async (id) => { + const facilityList = await knex(constants.TABLE_FACILITY) + .select( + `${constants.TABLE_REGION}.${constants.COLUMN_NAME} AS regionName`, + `${constants.TABLE_FACILITY}.${constants.COLUMN_NAME} AS facilityName`, + constants.COLUMN_IS_FUNCTIONAL, + ) + .leftJoin( + constants.TABLE_REGION, + `${constants.TABLE_FACILITY}.${constants.COLUMN_REGION_ID}`, + `${constants.TABLE_REGION}.${constants.COLUMN_REGION_ID}`, + ) + .where(constants.COLUMN_STATE_ID, id) + .catch((e) => { + console.error(e); + resValue = -1; + }); + + resValue = facilityList; + + return resValue; +}; + +/** + * Gets the number of functional facilities that the state of the given ID has. + * @param {Number} id must be an integer. + * @returns a positive integer if successful, -1 otherwise. + */ +const getFacilityCountByStateId = async (id) => { + let resValue = 0; + + const facilityCount = await knex(constants.TABLE_FACILITY) + .count(`${constants.COLUMN_FACILITY_ID} AS count`) + .leftJoin( + constants.TABLE_REGION, + `${constants.TABLE_FACILITY}.${constants.COLUMN_REGION_ID}`, + `${constants.TABLE_REGION}.${constants.COLUMN_REGION_ID}`, + ) + .where(constants.COLUMN_STATE_ID, id) + .andWhere(constants.COLUMN_IS_FUNCTIONAL, 1) + .catch((e) => { + console.error(e); + resValue = -1; + }); + + resValue = facilityCount[0].count; + + return resValue; +}; + +/** + * Creates a new facility. + * @param {Facility} facility must be a facility object. + * @returns {Boolean} true if successful, false otherwise. + */ +const addFacility = async (facility) => { + let resValue = true; + + let newIsFunctional = 0; + + if (facility.isFunctional) newIsFunctional = 1; + + await knex + .insert({ + regionId: facility.regionId, + name: facility.facilityName, + isFunctional: newIsFunctional, + }) + .into(constants.TABLE_FACILITY) + .catch((e) => { + console.error(e); + resValue = false; + }); + + return resValue; +}; + +/** + * Updates the information of a facility. + * @param {Facility} facility must be a facility object. + * @returns {Boolean} true if successful, false otherwise. + */ +const updateFacility = async (facility) => { + const resValue = true; + + let newIsFunctional = 0; + + if (facility.isFunctional) newIsFunctional = 1; + + await knex(constants.TABLE_FACILITY) + .where({ facilityId: facility.facilityId }) + .update({ + regionId: facility.regionId, + name: facility.facilityName, + isFunctional: newIsFunctional, + }) + .catch((e) => { + console.error(e); + resStatus = false; + }); + + return resValue; +}; + +const addMultipleFacilities = async (facilities) => { + let resValue = true; + + const knex = dbContext.getKnexObject(); + + const mappedFacilities = facilities.map((facility) => ({ + regionId: facility.regionId, + name: facility.facilityName, + isFunctional: facility.isFunctional ? 1 : 0, + })); + + await knex + .insert(mappedFacilities) + .into(constants.TABLE_FACILITY) + .catch((e) => { + console.error(e); + resValue = false; + }); + + return resValue; +}; + +/** + * Deletes the facility of a given ID without deleting its components. + * @param {Number} id must be an integer. + * @returns {Boolean} true if successful, false otherwise. + */ +const deleteFacilityById = async (id) => { + let resStatus = true; + + await knex(constants.TABLE_FACILITY) + .where({ facilityId: id }) + .del() + .catch((e) => { + console.error(e); + resStatus = false; + }); + + await knex(constants.TABLE_COMPONENT) + .where({ facilityId: id }) + .update({ facilityId: null }) + .catch((e) => { + console.error(e); + resStatus = false; + }); + + return resStatus; +}; + +/** + * Deletes the facility of a given ID and its components. + * @param {Number} id must be an integer. + * @returns {Boolean} true if successful, false otherwise. + */ +const destroyFacilityById = async (id) => { + let resStatus = true; + + await knex(constants.TABLE_FACILITY) + .where({ facilityId: id }) + .del() + .catch((e) => { + console.error(e); + resStatus = false; + }); + + if (resStatus) { + await knex(constants.TABLE_COMPONENT) + .where({ facilityId: id }) + .del() + .catch((e) => { + console.error(e); + resStatus = false; + }); + } + + return resStatus; +}; + +/** + * Assigns components to a facility. + * @param {Number} facilityId must be an integer. + * @param {Array} componentIds must be an array of integers. + * @returns {Boolean} true if successful, false otherwise. + */ +const assignFacilityComponents = async (facilityId, componentIds) => { + let resStatus = true; + + const promises = []; + + for (const componentId of componentIds) { + const promise = knex(constants.TABLE_COMPONENT) + .where({ componentId }) + .update({ + facilityId, + }) + .catch((e) => { + console.error(e); + resStatus = false; + }); + + promises.push(promise); + } + + await Promise.all(promises); + + return resStatus; +}; + +exports.getFacilitiesByRegionId = getFacilitiesByRegionId; +exports.getFacilityCountByStateId = getFacilityCountByStateId; +exports.addFacility = addFacility; +exports.updateFacility = updateFacility; +exports.deleteFacilityById = deleteFacilityById; +exports.destroyFacilityById = destroyFacilityById; +exports.assignFacilityComponents = assignFacilityComponents; +exports.getFacilitiesByStateId = getFacilitiesByStateId; +exports.addMultipleFacilities = addMultipleFacilities; diff --git a/src/tests/services/componentServices.test.js b/src/tests/services/componentServices.test.js new file mode 100644 index 0000000..dc1eecf --- /dev/null +++ b/src/tests/services/componentServices.test.js @@ -0,0 +1,571 @@ +const config = require('../../services/config.json'); + +const { constants } = config; +const dbSetup = require('../setups/database'); +const dbSeeder = require('../setups/db-seeder'); +const dbContext = require('../../repository/DbContext'); +const componentServices = require('../../services/componentServices.v2'); +const facilityServices = require('../../services/facilityServices'); + +let knex = null; + +beforeAll(async () => { + knex = dbSetup.createTestDBConnection(); + dbContext.getKnexObject = jest.fn(() => knex); + await dbSeeder.seedAllMaster(knex); +}); + +afterAll(async () => { + await knex.destroy(); + await dbSetup.dropTestDB(); +}); + +const stateObj = { + stateName: 'Test State', + treasuryAmt: 1000, + desc: '', + expenses: 0, +}; + +const stateSeeder = async (knex) => { + await knex + .insert({ + name: stateObj.stateName, + treasuryAmt: stateObj.treasuryAmt, + desc: stateObj.desc, + expenses: stateObj.expenses, + }) + .into(constants.TABLE_STATE); +}; + +const regionObj = { + regionName: 'Test Region', + stateId: 1, + corruptionId: 1, + biomeId: 1, + developmentId: 1, + population: 10, + desc: '', + taxRate: 0.05, +}; + +const regionSeeder = async (knex) => { + await knex + .insert({ + name: regionObj.regionName, + stateId: regionObj.stateId, + corruptionId: regionObj.corruptionId, + biomeId: regionObj.biomeId, + developmentId: regionObj.developmentId, + population: regionObj.population, + desc: regionObj.desc, + taxRate: regionObj.taxRate, + }) + .into(constants.TABLE_REGION); +}; + +const regionId = 1; + +describe('Modify Component Table', () => { + facilityServices.addMultipleFacilities = jest.fn(async (facilities) => { + const mappedFacilities = facilities.map((facility) => ({ + regionId: facility.regionId, + name: facility.facilityName, + isFunctional: facility.isFunctional ? 1 : 0, + })); + + await knex + .insert(mappedFacilities) + .into(constants.TABLE_FACILITY); + return true; + }); + + beforeEach(async () => { + await stateSeeder(knex); + await regionSeeder(knex); + }); + + afterEach(async () => { + const resetTables = [ + constants.TABLE_FACILITY, + constants.TABLE_COMPONENT, + constants.TABLE_REGION, + constants.TABLE_STATE, + ]; + await dbSetup.resetTables(knex, resetTables); + }); + + describe('addComponent', () => { + test('given just a component model should insert a component, decrease state treasury by its cost, and return OK', async () => { + const newComponent = { + componentName: 'New Component', + componentType: { + componentTypeId: 1, + }, + cost: 250, + regionId, + facilityId: null, + isChild: false, + parentId: null, + value: 5, + activationTime: 1, + }; + + let expectedComponent = { + ...newComponent, + name: newComponent.componentName, + componentTypeId: newComponent.componentType.componentTypeId, + value: 'i;5', + isChild: 0, + }; + expectedComponent = JSON.parse(JSON.stringify(expectedComponent)); + + delete expectedComponent.componentName; + delete expectedComponent.componentType; + delete expectedComponent.cost; + + const resStatus = await componentServices.addComponent({ component: newComponent }); + + const componentResult = await knex(constants.TABLE_COMPONENT).select('*'); + const facilityResult = await knex(constants.TABLE_FACILITY).select('*'); + const stateResult = (await knex(constants.TABLE_STATE).select('*'))[0]; + + expect(resStatus).toBe('OK'); + expect(facilityResult).toHaveLength(0); + expect(componentResult).toHaveLength(1); + if (componentResult.length > 0) { + const componentResultObject = componentResult[0]; + const expectedTreasury = stateObj.treasuryAmt - newComponent.cost; + expect(componentResultObject).toMatchObject(expectedComponent); + expect(stateResult.treasuryAmt).toBe(expectedTreasury); + } + }); + + test('given a component model with higher cost than the state treasury should return "Cost exceeding treasury amount"', async () => { + const newComponent = { + componentName: 'New Component', + componentType: { + componentTypeId: 1, + }, + cost: 1100, + regionId, + facilityId: null, + isChild: false, + parentId: null, + value: 5, + activationTime: 1, + }; + + const resStatus = await componentServices.addComponent({ component: newComponent }); + + const componentResult = await knex(constants.TABLE_COMPONENT).select('*'); + const facilityResult = await knex(constants.TABLE_FACILITY).select('*'); + const stateResult = (await knex(constants.TABLE_STATE).select('*'))[0]; + + expect(resStatus).toBe('Cost exceeding treasury amount'); + expect(facilityResult).toHaveLength(0); + expect(componentResult).toHaveLength(0); + expect(stateResult.treasuryAmt).toBe(stateObj.treasuryAmt); + }); + + test('given a component model with temporary facility name should insert a component, insert a facility, decrease state treasury by its cost, and return OK', async () => { + const newComponent = { + componentName: 'New Component', + componentType: { + componentTypeId: 1, + }, + cost: 250, + regionId, + facilityId: null, + isChild: false, + parentId: null, + value: 5, + activationTime: 1, + }; + + const expectedFacilityId = 1; + let expectedComponent = { + ...newComponent, + name: newComponent.componentName, + componentTypeId: newComponent.componentType.componentTypeId, + value: 'i;5', + isChild: 0, + facilityId: expectedFacilityId, + }; + expectedComponent = JSON.parse(JSON.stringify(expectedComponent)); + + delete expectedComponent.componentName; + delete expectedComponent.componentType; + delete expectedComponent.cost; + + const tempFacilityName = 'New Facility'; + const addComponentParameter = { + component: newComponent, + tempFacilityName, + }; + const resStatus = await componentServices.addComponent(addComponentParameter); + + const componentResult = await knex(constants.TABLE_COMPONENT).select('*'); + const facilityResult = await knex(constants.TABLE_FACILITY).select('*'); + const stateResult = (await knex(constants.TABLE_STATE).select('*'))[0]; + + expect(resStatus).toBe('OK'); + expect(componentResult).toHaveLength(1); + + if (componentResult.length > 0) { + const componentResultObject = componentResult[0]; + const expectedTreasury = stateObj.treasuryAmt - newComponent.cost; + expect(componentResultObject).toMatchObject(expectedComponent); + expect(stateResult.treasuryAmt).toBe(expectedTreasury); + } + + expect(facilityResult).toHaveLength(1); + if (facilityResult.length > 0) { + const expectedFacility = { + regionId, + name: tempFacilityName, + isFunctional: 0, + }; + const facilityResultObject = facilityResult[0]; + expect(facilityResultObject).toMatchObject(expectedFacility); + } + }); + + test.todo('given a component model with facility id not existing in facility table should not insert the component and should return error'); + }); + + describe('updateComponent', () => { + const beforeUpdateComponent = { + name: 'Before Update Component', + componentTypeId: 1, + regionId, + facilityId: null, + parentId: null, + value: 'i;5', + activationTime: 1, + isChild: 0, + }; + + const baseExpectedComponent = { + name: 'After Update Component', + componentTypeId: 4, + regionId, + facilityId: null, + parentId: null, + value: 'i;10', + activationTime: 0, + isChild: 0, + }; + + beforeEach(async () => { + await knex + .insert(beforeUpdateComponent) + .into(constants.TABLE_COMPONENT); + }); + + test('given just a component model should update the component and return OK', async () => { + const updateComponent = { + componentId: 1, + componentName: baseExpectedComponent.name, + componentType: { + componentTypeId: baseExpectedComponent.componentTypeId, + }, + regionId: baseExpectedComponent.regionId, + facilityId: baseExpectedComponent.facilityId, + isChild: (baseExpectedComponent.isChild == 1), + parentId: null, + value: 10, + activationTime: baseExpectedComponent.activationTime, + }; + + const resStatus = await componentServices.updateComponent({ component: updateComponent }); + + const componentResult = await knex(constants.TABLE_COMPONENT).select('*'); + const facilityResult = await knex(constants.TABLE_FACILITY).select('*'); + const stateResult = (await knex(constants.TABLE_STATE).select('*'))[0]; + + expect(resStatus).toBeTruthy(); + expect(facilityResult).toHaveLength(0); + expect(componentResult).toHaveLength(1); + if (componentResult.length > 0) { + const componentResultObject = componentResult[0]; + expect(componentResultObject).toMatchObject(baseExpectedComponent); + expect(stateResult.treasuryAmt).toBe(stateObj.treasuryAmt); + } + }); + + test('given a component model with temporary facility name update the component, insert a facility, and return OK', async () => { + const updateComponent = { + componentId: 1, + componentName: baseExpectedComponent.name, + componentType: { + componentTypeId: baseExpectedComponent.componentTypeId, + }, + regionId: baseExpectedComponent.regionId, + facilityId: baseExpectedComponent.facilityId, + isChild: (baseExpectedComponent.isChild == 1), + parentId: null, + value: 10, + activationTime: baseExpectedComponent.activationTime, + }; + + const expectedComponent = JSON.parse(JSON.stringify(baseExpectedComponent)); + expectedComponent.facilityId = 1; + + const tempFacilityName = 'New Facility'; + const updateComponentParameter = { + component: updateComponent, + tempFacilityName, + }; + const resStatus = await componentServices.updateComponent(updateComponentParameter); + + const componentResult = await knex(constants.TABLE_COMPONENT).select('*'); + const facilityResult = await knex(constants.TABLE_FACILITY).select('*'); + const stateResult = (await knex(constants.TABLE_STATE).select('*'))[0]; + + expect(resStatus).toBeTruthy(); + expect(componentResult).toHaveLength(1); + if (componentResult.length > 0) { + const componentResultObject = componentResult[0]; + expect(componentResultObject).toMatchObject(expectedComponent); + expect(stateResult.treasuryAmt).toBe(stateObj.treasuryAmt); + } + + expect(facilityResult).toHaveLength(1); + if (facilityResult.length > 0) { + const expectedFacility = { + regionId, + name: tempFacilityName, + isFunctional: 0, + }; + const facilityResultObject = facilityResult[0]; + expect(facilityResultObject).toMatchObject(expectedFacility); + } + }); + + test.todo('given a component model with facility id not existing in facility table should not update the component and should return error'); + }); + + describe('addMultipleComponents', () => { + const componentGeneralGenerator = (name, componentTypeId, cost, facilityId, value, activationTime) => ({ + componentModel: { + componentName: name, + componentType: { + componentTypeId, + }, + cost, + regionId, + facilityId, + isChild: false, + parentId: null, + value, + activationTime, + }, + databaseModel: { + name, + componentTypeId, + regionId, + facilityId, + parentId: null, + value: (value == null) ? null : `i;${value}`, + activationTime, + isChild: 0, + }, + }); + + test('given component models without temporary facility map should insert those components, decrease state treasury by total cost, and return OK', async () => { + const addComponents = []; + const expectedComponents = []; + let totalCost = 0; + + const components = [ + { + name: 'New Component 1', + componentTypeId: 1, + cost: 250, + facilityId: null, + value: 5, + activationTime: 1, + }, + { + name: 'New Component 2', + componentTypeId: 2, + cost: 250, + facilityId: null, + value: null, + activationTime: 1, + }, + { + name: 'New Component 3', + componentTypeId: 4, + cost: 0, + facilityId: null, + value: 500, + activationTime: 0, + }, + ]; + + components.forEach((component) => { + const tempComponent = componentGeneralGenerator(component.name, component.componentTypeId, component.cost, component.facilityId, component.value, component.activationTime); + addComponents.push(tempComponent.componentModel); + expectedComponents.push(tempComponent.databaseModel); + totalCost += component.cost; + }); + + const resStatus = await componentServices.addMultipleComponents({ components: addComponents }); + + const componentResult = await knex(constants.TABLE_COMPONENT).select('*'); + const facilityResult = await knex(constants.TABLE_FACILITY).select('*'); + const stateResult = (await knex(constants.TABLE_STATE).select('*'))[0]; + + expect(resStatus).toBeTruthy(); + expect(facilityResult).toHaveLength(0); + expect(componentResult).toHaveLength(expectedComponents.length); + if (componentResult.length == expectedComponents.length) { + const expectedTreasury = stateObj.treasuryAmt - totalCost; + expect(stateResult.treasuryAmt).toBe(expectedTreasury); + + for (let i = 0; i < componentResult.length; i++) { + expect(componentResult[i]).toMatchObject(expectedComponents[i]); + } + } + }); + + test('given component models with total costs higher than the state treasury should not insert those components and return error', async () => { + const addComponents = []; + + const components = [ + { + name: 'New Component 1', + componentTypeId: 1, + cost: 500, + facilityId: null, + value: 5, + activationTime: 1, + }, + { + name: 'New Component 2', + componentTypeId: 1, + cost: 500, + facilityId: null, + value: null, + activationTime: 1, + }, + { + name: 'New Component 3', + componentTypeId: 1, + cost: 100, + facilityId: null, + value: 500, + activationTime: 1, + }, + ]; + + components.forEach((component) => { + const tempComponent = componentGeneralGenerator(component.name, component.componentTypeId, component.cost, component.facilityId, component.value, component.activationTime); + addComponents.push(tempComponent.componentModel); + }); + + const resStatus = await componentServices.addMultipleComponents({ components: addComponents }); + + const componentResult = await knex(constants.TABLE_COMPONENT).select('*'); + const facilityResult = await knex(constants.TABLE_FACILITY).select('*'); + + expect(resStatus).not.toBeTruthy(); + expect(facilityResult).toHaveLength(0); + expect(componentResult).toHaveLength(0); + }); + + test('given component models with temporary facility map should insert those components, decrease state treasury by total cost, insert the facilities, and return OK', async () => { + const addComponents = []; + const expectedComponents = []; + let totalCost = 0; + + const components = [ + { + name: 'New Component 1', + componentTypeId: 1, + cost: 250, + facilityId: -2, + value: 5, + activationTime: 1, + }, + { + name: 'New Component 2', + componentTypeId: 2, + cost: 250, + facilityId: null, + value: null, + activationTime: 1, + }, + { + name: 'New Component 3', + componentTypeId: 4, + cost: 0, + facilityId: -1, + value: 500, + activationTime: 0, + }, + ]; + + // In this test, we use negative index so that its absolute value matches actual facility id in the table + const facilityMap = new Map([ + [-1, 'Facility 1'], + [-2, 'Facility 2'], + ]); + const expectedFacilities = [ + { + facilityId: 1, + regionId, + name: 'Facility 1', + isFunctional: 0, + }, + { + facilityId: 2, + regionId, + name: 'Facility 2', + isFunctional: 0, + }, + ]; + + components.forEach((component) => { + const tempComponent = componentGeneralGenerator(component.name, component.componentTypeId, component.cost, component.facilityId, component.value, component.activationTime); + tempComponent.databaseModel.facilityId = tempComponent.databaseModel.facilityId == null + ? null + : Math.abs(tempComponent.databaseModel.facilityId); + addComponents.push(tempComponent.componentModel); + expectedComponents.push(tempComponent.databaseModel); + totalCost += component.cost; + }); + + const addComponentsParameter = { + components: addComponents, + tempFacilityMap: facilityMap, + }; + + const resStatus = await componentServices.addMultipleComponents(addComponentsParameter); + + const componentResult = await knex(constants.TABLE_COMPONENT).select('*'); + const facilityResult = await knex(constants.TABLE_FACILITY).select('*'); + const stateResult = (await knex(constants.TABLE_STATE).select('*'))[0]; + + expect(resStatus).toBeTruthy(); + expect(componentResult).toHaveLength(expectedComponents.length); + if (componentResult.length == expectedComponents.length) { + const expectedTreasury = stateObj.treasuryAmt - totalCost; + expect(stateResult.treasuryAmt).toBe(expectedTreasury); + + for (let i = 0; i < componentResult.length; i++) { + expect(componentResult[i]).toMatchObject(expectedComponents[i]); + } + } + + expect(facilityResult).toHaveLength(expectedFacilities.length); + if (facilityResult.length == expectedFacilities.length) { + for (let i = 0; i < facilityResult.length; i++) { + expect(facilityResult[i]).toMatchObject(expectedFacilities[i]); + } + } + }); + }); +}); diff --git a/src/tests/services/facilityServices.test.js b/src/tests/services/facilityServices.test.js new file mode 100644 index 0000000..5fd1516 --- /dev/null +++ b/src/tests/services/facilityServices.test.js @@ -0,0 +1,130 @@ +const config = require('../../services/config.json'); + +const { constants } = config; +const dbSetup = require('../setups/database'); +const dbSeeder = require('../setups/db-seeder'); +const dbContext = require('../../repository/DbContext'); +const facilityServices = require('../../services/facilityServices'); + +let knex = null; + +const stateObj = { + stateName: 'Test State', + treasuryAmt: 1000, + desc: '', + expenses: 0, +}; + +const stateSeeder = async (knex) => { + await knex + .insert({ + name: stateObj.stateName, + treasuryAmt: stateObj.treasuryAmt, + desc: stateObj.desc, + expenses: stateObj.expenses, + }) + .into(constants.TABLE_STATE); +}; + +const regionObj = { + regionName: 'Test Region', + stateId: 1, + corruptionId: 1, + biomeId: 1, + developmentId: 1, + population: 10, + desc: '', + taxRate: 0.05, +}; + +const regionSeeder = async (knex) => { + await knex + .insert({ + name: regionObj.regionName, + stateId: regionObj.stateId, + corruptionId: regionObj.corruptionId, + biomeId: regionObj.biomeId, + developmentId: regionObj.developmentId, + population: regionObj.population, + desc: regionObj.desc, + taxRate: regionObj.taxRate, + }) + .into(constants.TABLE_REGION); +}; + +const regionId = 1; + +beforeAll(async () => { + knex = dbSetup.createTestDBConnection(); + dbContext.getKnexObject = jest.fn(() => knex); + await dbSeeder.seedAllMaster(knex); + await stateSeeder(knex); + await regionSeeder(knex); +}); + +afterAll(async () => { + await knex.destroy(); + await dbSetup.dropTestDB(); +}); + +describe('Modify Facility Table', () => { + afterEach(async () => { + const resetTables = [ + constants.TABLE_FACILITY, + ]; + await dbSetup.resetTables(knex, resetTables); + }); + + describe('addMultipleFacilities', () => { + const facilityGeneralGenerator = (name, isFunctional) => ({ + facilityModel: { + regionId, + facilityName: name, + isFunctional, + }, + databaseModel: { + regionId, + name, + isFunctional: isFunctional ? 1 : 0, + }, + }); + + test('given facility models should insert those facilities and return OK', async () => { + const addFacilities = []; + const expectedFacilities = []; + + const facilities = [ + { + name: 'Facility 1', + isFunctional: true, + }, + { + name: 'Facility 2', + isFunctional: false, + }, + { + name: 'Facility 3', + isFunctional: true, + }, + ]; + + facilities.forEach((facility) => { + const tempfacility = facilityGeneralGenerator(facility.name, facility.isFunctional); + addFacilities.push(tempfacility.facilityModel); + expectedFacilities.push(tempfacility.databaseModel); + }); + + const resStatus = await facilityServices.addMultipleFacilities(addFacilities); + + const facilityResult = await knex(constants.TABLE_FACILITY).select('*'); + + expect(resStatus).toBeTruthy(); + expect(facilityResult).toHaveLength(expectedFacilities.length); + if (facilityResult.length == expectedFacilities.length) { + for (let i = 0; i < facilityResult.length; i++) { + expect(facilityResult[i]).toMatchObject(expectedFacilities[i]); + } + } + }); + }); +}); diff --git a/src/tests/setups/database.js b/src/tests/setups/database.js new file mode 100644 index 0000000..9788764 --- /dev/null +++ b/src/tests/setups/database.js @@ -0,0 +1,81 @@ +const fs = require('fs'); +const config = require('../../services/config.json'); + +const { constants } = config; +const dbContext = require('../../repository/DbContext'); + +const DB_SCHEMA_FILE_PATH = 'db/ces-schema_only.db'; +const TEST_DB_FILE_PATH = 'db/ces.test.db'; + +function createTestDBConnection() { + if (!fs.existsSync(TEST_DB_FILE_PATH)) { + fs.copyFileSync(DB_SCHEMA_FILE_PATH, TEST_DB_FILE_PATH); + } + return dbContext.getKnexObject(config.environment.name.Test); +} + +async function dropTestDB() { + if (fs.existsSync(TEST_DB_FILE_PATH)) { + fs.unlinkSync(TEST_DB_FILE_PATH); + } +} + +/** + * Clear all records from tables and reset their autoincrement with the options to exclude some tables or only inlude some tables. + * Be mindful of what tables to be excluded and their levels (check the comments inside this method). + * @param {Knex} knex database knex object + * @param {Array} includedTables Tables that are needed to be deleted without deleting others. + * Mutually exclusive with excludedTables parameter. + * @param {Array} excludedTables Tables that are excluded from being cleared. + * Be mindful of FK dependendy of the tables to be excluded. + * Mutually exclusive with includedTables parameter. + */ +async function resetTables(knex, includedTables = [], excludedTables = []) { + // This array is ordered from tables that is not being depended on to most depended tables + // Deeper level tables depend on the tables above their level(s) + let deletedTables = [ + // LEVEL 3 + constants.TABLE_FACILITY, + // LEVEL 2 + constants.TABLE_COMPONENT, + constants.TABLE_TRADE_AGREEMENT_DETAIL, + // LEVEL 1 + constants.TABLE_REGION, + constants.TABLE_RESOURCE, + // LEVEL 0 + constants.TABLE_STATE, + constants.TABLE_RESOURCE_TIER, + constants.TABLE_TRADE_AGREEMENT_HEADER, + constants.TABLE_CORRUPTION, + constants.TABLE_DEVELOPMENT, + constants.TABLE_BIOME, + constants.TABLE_COMPONENT_TYPE, + constants.TABLE_SEASON, + ]; + + if (includedTables.length > 0) { + deletedTables = deletedTables.filter((table) => includedTables.includes(table)); + } + + if (excludedTables.length > 0) { + deletedTables = deletedTables.filter((table) => !(excludedTables.includes(table))); + } + + deletedTables.forEach(async (table) => { + await knex(table).del().then((res) => { + // console.log(`DELETE TEST TABLE ${table} res`, res); + }).catch((e) => { + console.error(e); + }); + }); + + await knex(constants.TABLE_SQLITE_SEQUENCE) + .whereIn('name', deletedTables) + .update('seq', 0); +} + +module.exports = { + createTestDBConnection, + dropTestDB, + resetTables, +}; diff --git a/src/tests/setups/db-seeder.js b/src/tests/setups/db-seeder.js new file mode 100644 index 0000000..d11ac57 --- /dev/null +++ b/src/tests/setups/db-seeder.js @@ -0,0 +1,55 @@ +const config = require('../../services/config.json'); + +const { constants } = config; +const masterSeeds = require('./master-seed.json'); + +const seedBiome = async (knex) => { + await knex + .insert(masterSeeds.Biome) + .into(constants.TABLE_BIOME); +}; + +const seedComponentType = async (knex) => { + await knex + .insert(masterSeeds.ComponentType) + .into(constants.TABLE_COMPONENT_TYPE); +}; + +const seedCorruption = async (knex) => { + await knex + .insert(masterSeeds.Corruption) + .into(constants.TABLE_CORRUPTION); +}; + +const seedDevelopment = async (knex) => { + await knex + .insert(masterSeeds.Development) + .into(constants.TABLE_DEVELOPMENT); +}; + +const seedResource = async (knex) => { + await knex + .insert(masterSeeds.ResourceTier) + .into(constants.TABLE_RESOURCE_TIER); + + await knex + .insert(masterSeeds.Resource) + .into(constants.TABLE_RESOURCE); +}; + +const seedAllMaster = async (knex) => { + seedBiome(knex); + seedComponentType(knex); + seedCorruption(knex); + seedDevelopment(knex); + seedResource(knex); +}; + +module.exports = { + seedBiome, + seedComponentType, + seedCorruption, + seedDevelopment, + seedResource, + seedAllMaster, +}; diff --git a/src/tests/setups/master-seed.json b/src/tests/setups/master-seed.json new file mode 100644 index 0000000..3531261 --- /dev/null +++ b/src/tests/setups/master-seed.json @@ -0,0 +1,349 @@ +{ + "Biome": + [ + { + "biomeId": 1, + "name": "Plains" + }, + { + "biomeId": 2, + "name": "Savannah" + }, + { + "biomeId": 3, + "name": "Forest" + }, + { + "biomeId": 4, + "name": "Birch Forest" + }, + { + "biomeId": 5, + "name": "Roofed Forest" + }, + { + "biomeId": 6, + "name": "Boreal Forest" + }, + { + "biomeId": 7, + "name": "Jungle" + }, + { + "biomeId": 8, + "name": "Desert" + }, + { + "biomeId": 9, + "name": "Mesa" + }, + { + "biomeId": 10, + "name": "Taiga" + }, + { + "biomeId": 11, + "name": "Cold Taiga" + }, + { + "biomeId": 12, + "name": "Swampland" + }, + { + "biomeId": 13, + "name": "Mountains" + }, + { + "biomeId": 14, + "name": "Extreme Hills" + }, + { + "biomeId": 15, + "name": "Ice Plains" + }, + { + "biomeId": 16, + "name": "Mushroom Island" + }, + { + "biomeId": 17, + "name": "Ice Plains Spike" + }, + { + "biomeId": 18, + "name": "Nether Wasteland" + } + ], + "ComponentType": + [ + { + "componentTypeId": 1, + "name": "Population" + }, + { + "componentTypeId": 2, + "name": "Building" + }, + { + "componentTypeId": 3, + "name": "Resource" + }, + { + "componentTypeId": 4, + "name": "Food" + }, + { + "componentTypeId": 5, + "name": "Money" + }, + { + "componentTypeId": 6, + "name": "Special" + } + ], + "Corruption": + [ + { + "corruptionId": 1, + "name": "Nonexistent", + "rate": 0 + }, + { + "corruptionId": 2, + "name": "Petty", + "rate": 0.02 + }, + { + "corruptionId": 3, + "name": "Noticeable", + "rate": 0.05 + }, + { + "corruptionId": 4, + "name": "Moderate", + "rate": 0.1 + }, + { + "corruptionId": 5, + "name": "Significant", + "rate": 0.2 + }, + { + "corruptionId": 6, + "name": "Widespread", + "rate": 0.35 + }, + { + "corruptionId": 7, + "name": "Rampant", + "rate": 0.5 + } + ], + "Development": + [ + { + "developmentId": 1, + "name": "Hamlet", + "populationCap": 10, + "militaryTier": 1, + "growthModifier": 2, + "shrinkageModifier": 0.5 + }, + { + "developmentId": 2, + "name": "Village", + "populationCap": 20, + "militaryTier": 1, + "growthModifier": 2, + "shrinkageModifier": 0.5 + }, + { + "developmentId": 3, + "name": "Small Town", + "populationCap": 40, + "militaryTier": 2, + "growthModifier": 1, + "shrinkageModifier": 1 + }, + { + "developmentId": 4, + "name": "Town", + "populationCap": 60, + "militaryTier": 2, + "growthModifier": 1, + "shrinkageModifier": 1 + }, + { + "developmentId": 5, + "name": "City", + "populationCap": 80, + "militaryTier": 3, + "growthModifier": 1, + "shrinkageModifier": 1 + }, + { + "developmentId": 6, + "name": "Large City", + "populationCap": 100, + "militaryTier": 3, + "growthModifier": 1, + "shrinkageModifier": 1 + } + ], + "ResourceTier": + [ + { + "resourceTierId": 1, + "name": "Tier I", + "tradePower": 0.06 + }, + { + "resourceTierId": 2, + "name": "Tier II", + "tradePower": 0.05 + }, + { + "resourceTierId": 3, + "name": "Tier III", + "tradePower": 0.04 + }, + { + "resourceTierId": 4, + "name": "Tier IV", + "tradePower": 0.03 + }, + { + "resourceTierId": 5, + "name": "Tier V", + "tradePower": 0.025 + }, + { + "resourceTierId": 6, + "name": "Tier VI", + "tradePower": 0.02 + } + ], + "Resource": + [ + { + "resourceId": 1, + "name": "Diamond", + "resourceTierId": 1 + }, + { + "resourceId": 2, + "name": "Emerald", + "resourceTierId": 1 + }, + { + "resourceId": 3, + "name": "Ruby", + "resourceTierId": 1 + }, + { + "resourceId": 4, + "name": "Gold", + "resourceTierId": 2 + }, + { + "resourceId": 5, + "name": "Silk", + "resourceTierId": 2 + }, + { + "resourceId": 6, + "name": "Spices", + "resourceTierId": 2 + }, + { + "resourceId": 7, + "name": "Prismarine", + "resourceTierId": 2 + }, + { + "resourceId": 8, + "name": "Pearls", + "resourceTierId": 2 + }, + { + "resourceId": 9, + "name": "Silver", + "resourceTierId": 3 + }, + { + "resourceId": 10, + "name": "Quartz", + "resourceTierId": 3 + }, + { + "resourceId": 11, + "name": "Lapis Lazuli", + "resourceTierId": 3 + }, + { + "resourceId": 12, + "name": "Redstone", + "resourceTierId": 3 + }, + { + "resourceId": 13, + "name": "Glowstone", + "resourceTierId": 3 + }, + { + "resourceId": 14, + "name": "Ancient Tomes", + "resourceTierId": 3 + }, + { + "resourceId": 15, + "name": "Sugar", + "resourceTierId": 4 + }, + { + "resourceId": 16, + "name": "Wine", + "resourceTierId": 4 + }, + { + "resourceId": 17, + "name": "Tea", + "resourceTierId": 4 + }, + { + "resourceId": 18, + "name": "Oil", + "resourceTierId": 5 + }, + { + "resourceId": 19, + "name": "Furs", + "resourceTierId": 5 + }, + { + "resourceId": 20, + "name": "Iron", + "resourceTierId": 5 + }, + { + "resourceId": 21, + "name": "Fine Wood", + "resourceTierId": 5 + }, + { + "resourceId": 22, + "name": "Warhorses", + "resourceTierId": 5 + }, + { + "resourceId": 23, + "name": "Pottery", + "resourceTierId": 6 + }, + { + "resourceId": 24, + "name": "Coal", + "resourceTierId": 6 + } + ] +} \ No newline at end of file