diff --git a/src/controllers/index.js b/src/controllers/index.js index 18d9b601..2b9de780 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -7,3 +7,4 @@ export * as LabelController from './label.controller'; export * as AuditController from './audit.controller'; export * as GovernanceController from './governance.controller'; export * as FileStoreController from './fileStore.controller'; +export * as OfferController from './offer.controller'; diff --git a/src/controllers/offer.controller.js b/src/controllers/offer.controller.js new file mode 100644 index 00000000..855c10fa --- /dev/null +++ b/src/controllers/offer.controller.js @@ -0,0 +1,54 @@ +import { Meta, Staging } from '../models'; + +import { + assertHomeOrgExists, + assertNoPendingCommits, + assertWalletIsSynced, + assertIfReadOnlyMode, + assertStagingTableNotEmpty, +} from '../utils/data-assertions'; + +import datalayer from '../datalayer/persistance'; + +export const generateOfferFile = async (req, res) => { + try { + await assertIfReadOnlyMode(); + await assertStagingTableNotEmpty(); + await assertHomeOrgExists(); + await assertWalletIsSynced(); + await assertNoPendingCommits(); + + const offerFile = await Staging.generateOfferFile(); + res.json(offerFile); + } catch (error) { + console.trace(error); + res.status(400).json({ + message: 'Error generating offer file.', + error: error.message, + }); + } +}; + +export const cancelActiveOffer = async (req, res) => { + try { + const activeOffer = await Meta.findOne({ + where: { metaKey: 'activeOfferTradeId' }, + }); + + if (!activeOffer) { + throw new Error(`There is no active offer to cancel`); + } + + await datalayer.cancelActiveOffer(activeOffer.metaValue); + await Meta.destroy({ where: { metaKey: 'activeOfferTradeId' } }); + + res.json({ + message: 'Active offer has been canceled.', + }); + } catch (error) { + res.status(400).json({ + message: 'Can not cancel active offer', + error: error.message, + }); + } +}; diff --git a/src/controllers/staging.controller.js b/src/controllers/staging.controller.js index 69d8da11..e1038d8a 100644 --- a/src/controllers/staging.controller.js +++ b/src/controllers/staging.controller.js @@ -71,25 +71,6 @@ export const findAll = async (req, res) => { } }; -export const generateOfferFile = async (req, res) => { - try { - await assertIfReadOnlyMode(); - await assertStagingTableNotEmpty(); - await assertHomeOrgExists(); - await assertWalletIsSynced(); - await assertNoPendingCommits(); - - const offerFile = await Staging.generateOfferFile(); - res.json(offerFile); - } catch (error) { - console.trace(error); - res.status(400).json({ - message: 'Error generating offer file.', - error: error.message, - }); - } -}; - export const commit = async (req, res) => { try { await assertIfReadOnlyMode(); diff --git a/src/datalayer/persistance.js b/src/datalayer/persistance.js index 2f20de5e..c27fb786 100644 --- a/src/datalayer/persistance.js +++ b/src/datalayer/persistance.js @@ -514,6 +514,32 @@ const makeOffer = async (offer) => { } }; +const cancelOffer = async (tradeId) => { + const options = { + url: `${CONFIG.DATALAYER_URL}/cancel_offer `, + body: JSON.stringify({ + trade_id: tradeId, + }), + }; + + try { + const response = await request( + Object.assign({}, getBaseOptions(), options), + ); + + const data = JSON.parse(response); + + if (data.success) { + return data; + } + + throw new Error(data.error); + } catch (error) { + console.log(error); + throw error; + } +}; + export { addMirror, makeOffer, @@ -530,4 +556,5 @@ export { pushChangeListToDataLayer, createDataLayerStore, getSubscriptions, + cancelOffer, }; diff --git a/src/models/staging/staging.model.js b/src/models/staging/staging.model.js index 2c815c4e..c1431d17 100644 --- a/src/models/staging/staging.model.js +++ b/src/models/staging/staging.model.js @@ -1,476 +1,483 @@ -'use strict'; - -import _ from 'lodash'; -import Sequelize from 'sequelize'; -const Op = Sequelize.Op; - -const { Model } = Sequelize; -import { Project, Unit, Organization, Issuance } from '../../models'; -import { encodeHex, generateOffer } from '../../utils/datalayer-utils'; - -import * as rxjs from 'rxjs'; -import { sequelize } from '../../database'; - -import datalayer from '../../datalayer'; -import { makeOffer } from '../../datalayer/persistance'; - -import ModelTypes from './staging.modeltypes.cjs'; -import { formatModelAssociationName } from '../../utils/model-utils.js'; - -import { - createXlsFromSequelizeResults, - transformFullXslsToChangeList, -} from '../../utils/xls'; - -class Staging extends Model { - static changes = new rxjs.Subject(); - - static async create(values, options) { - Staging.changes.next(['staging']); - return super.create(values, options); - } - - static async destroy(values) { - Staging.changes.next(['staging']); - return super.destroy(values); - } - - static async upsert(values, options) { - Staging.changes.next(['staging']); - return super.upsert(values, options); - } - - static generateOfferFile = async () => { - const stagingRecord = await Staging.findOne({ - // where: { isTransfer: true }, - where: { commited: false }, - raw: true, - }); - - const takerProjectRecord = _.head(JSON.parse(stagingRecord.data)); - - const myOrganization = await Organization.findOne({ - where: { isHome: true }, - raw: true, - }); - - const maker = { inclusions: [] }; - const taker = { inclusions: [] }; - - // The record still has the orgUid of the takerProjectRecord, - // we will update this to the correct orgUId later - maker.storeId = takerProjectRecord.orgUid; - taker.storeId = myOrganization.orgUid; - - const makerProjectRecord = await Project.findOne({ - where: { warehouseProjectId: takerProjectRecord.warehouseProjectId }, - include: Project.getAssociatedModels().map((association) => { - return { - model: association.model, - as: formatModelAssociationName(association), - }; - }), - }); - - makerProjectRecord.projectStatus = 'Transitioned'; - - const issuanceIds = makerProjectRecord.issuances.reduce((ids, issuance) => { - if (!ids.includes(issuance.id)) { - ids.push(issuance.id); - } - return ids; - }, []); - - let unitMakerRecords = await Unit.findAll({ - where: { - issuanceId: { [Op.in]: issuanceIds }, - }, - raw: true, - }); - - // Takers get an unlatered copy of all the project units from the maker - const unitTakerRecords = _.cloneDeep(unitMakerRecords); - - unitMakerRecords = unitMakerRecords.map((record) => { - record.unitStatus = 'Exported'; - return record; - }); - - const primaryProjectKeyMap = { - project: 'warehouseProjectId', - projectLocations: 'id', - labels: 'id', - issuances: 'id', - coBenefits: 'id', - relatedProjects: 'id', - estimations: 'id', - projectRatings: 'id', - }; - - const primaryUnitKeyMap = { - unit: 'warehouseUnitId', - labels: 'id', - label_units: 'id', - issuances: 'id', - }; - - const makerProjectXslsSheets = createXlsFromSequelizeResults({ - rows: [makerProjectRecord], - model: Project, - toStructuredCsv: true, - }); - - const takerProjectXslsSheets = createXlsFromSequelizeResults({ - rows: [takerProjectRecord], - model: Project, - toStructuredCsv: true, - }); - - const makerUnitXslsSheets = createXlsFromSequelizeResults({ - rows: unitMakerRecords, - model: Unit, - toStructuredCsv: true, - }); - - const takerUnitXslsSheets = createXlsFromSequelizeResults({ - rows: unitTakerRecords, - model: Unit, - toStructuredCsv: true, - }); - - const takerProjectInclusions = await transformFullXslsToChangeList( - takerProjectXslsSheets, - 'insert', - primaryProjectKeyMap, - ); - - const makerProjectInclusions = await transformFullXslsToChangeList( - makerProjectXslsSheets, - 'insert', - primaryProjectKeyMap, - ); - - const makerUnitInclusions = await transformFullXslsToChangeList( - makerUnitXslsSheets, - 'insert', - primaryUnitKeyMap, - ); - - const takerUnitInclusions = await transformFullXslsToChangeList( - takerUnitXslsSheets, - 'insert', - primaryUnitKeyMap, - ); - - /* Object.keys(maker.inclusions).forEach((table) => { - maker.inclusions[table] = maker.inclusions[table] - .filter((inclusion) => inclusion.action !== 'delete') - .map((inclusion) => ({ key: inclusion.key, value: inclusion.value })); - });*/ - - maker.inclusions.push( - ...makerProjectInclusions.project - .filter((inclusion) => inclusion.action !== 'delete') - .map((inclusion) => ({ - key: inclusion.key, - value: inclusion.value, - })), - ); - - maker.inclusions.push( - ...makerUnitInclusions.unit - .filter((inclusion) => inclusion.action !== 'delete') - .map((inclusion) => ({ - key: inclusion.key, - value: inclusion.value, - })), - ); - - taker.inclusions.push( - ...takerProjectInclusions.project - .filter((inclusion) => inclusion.action !== 'delete') - .map((inclusion) => ({ - key: inclusion.key, - value: inclusion.value, - })), - ); - - taker.inclusions.push( - ...takerUnitInclusions.unit - .filter((inclusion) => inclusion.action !== 'delete') - .map((inclusion) => ({ - key: inclusion.key, - value: inclusion.value, - })), - ); - - const offer = generateOffer(maker, taker); - return makeOffer(offer); - }; - - // If the record was commited but the diff.original is null - // that means that the original record no longer exists and - // the staging record should be cleaned up. - static cleanUpCommitedAndInvalidRecords = async () => { - const stagingRecords = await Staging.findAll({ raw: true }); - - const stagingRecordsToDelete = await Promise.all( - stagingRecords.filter(async (record) => { - if (record.commited === 1) { - const { uuid, table, action, data } = record; - const diff = await Staging.getDiffObject(uuid, table, action, data); - return diff.original == null; - } - return false; - }), - ); - - await Staging.destroy({ - where: { uuid: stagingRecordsToDelete.map((record) => record.uuid) }, - }); - }; - - static getDiffObject = async (uuid, table, action, data) => { - const diff = {}; - if (action === 'INSERT') { - diff.original = {}; - diff.change = JSON.parse(data); - } - - if (action === 'UPDATE') { - diff.change = JSON.parse(data); - - let original; - - if (table === 'Projects') { - original = await Project.findOne({ - where: { warehouseProjectId: uuid }, - include: Project.getAssociatedModels().map((association) => { - return { - model: association.model, - as: formatModelAssociationName(association), - }; - }), - }); - - // Show the issuance data if its being reused - // this is just for view purposes onlys - await Promise.all( - diff.change.map(async (record) => { - if (record.issuanceId) { - const issuance = await Issuance.findOne({ - where: { id: record.issuanceId }, - }); - - record.issuance = issuance.dataValues; - } - }), - ); - } else if (table === 'Units') { - original = await Unit.findOne({ - where: { warehouseUnitId: uuid }, - include: Unit.getAssociatedModels().map((association) => { - return { - model: association.model, - as: formatModelAssociationName(association), - }; - }), - }); - - // Show the issuance data if its being reused, - // this is just for view purposes onlys - await Promise.all( - diff.change.map(async (record) => { - if (record.issuanceId) { - const issuance = await Issuance.findOne({ - where: { id: record.issuanceId }, - }); - - record.issuance = issuance.dataValues; - } - }), - ); - } - - diff.original = original; - } - - if (action === 'DELETE') { - let original; - - if (table === 'Projects') { - original = await Project.findOne({ - where: { warehouseProjectId: uuid }, - include: Project.getAssociatedModels().map((association) => { - return { - model: association.model, - as: formatModelAssociationName(association), - }; - }), - }); - } else if (table === 'Units') { - original = await Unit.findOne({ - where: { warehouseUnitId: uuid }, - include: Unit.getAssociatedModels().map((association) => { - return { - model: association.model, - as: formatModelAssociationName(association), - }; - }), - }); - } - - diff.original = original; - diff.change = {}; - } - - return diff; - }; - - static seperateStagingDataIntoActionGroups = (stagedData, table) => { - const insertRecords = []; - const updateRecords = []; - const deleteChangeList = []; - - stagedData - .filter((stagingRecord) => stagingRecord.table === table) - .forEach((stagingRecord) => { - // TODO: Think of a better place to mark the records as commited - Staging.update( - { commited: true }, - { where: { uuid: stagingRecord.uuid } }, - ); - if (stagingRecord.action === 'INSERT') { - insertRecords.push(...JSON.parse(stagingRecord.data)); - } else if (stagingRecord.action === 'UPDATE') { - let tablePrefix = table.toLowerCase(); - // hacky fix to account for the units and projects table not - // being lowercase and plural in the xsls transformation - if (tablePrefix === 'units' || tablePrefix === 'projects') { - tablePrefix = tablePrefix.replace(/s\s*$/, ''); - } - - deleteChangeList.push({ - action: 'delete', - key: encodeHex(`${tablePrefix}|${stagingRecord.uuid}`), - }); - - // TODO: Child table records are getting orphaned in the datalayer, - // because we need to generate a delete action for each one - - updateRecords.push(...JSON.parse(stagingRecord.data)); - } else if (stagingRecord.action === 'DELETE') { - let tablePrefix = table.toLowerCase(); - - // hacky fix to account for the units and projects table not - // being lowercase and plural in the xsls transformation - if (tablePrefix === 'units' || tablePrefix === 'projects') { - tablePrefix = tablePrefix.replace(/s\s*$/, ''); - } - - deleteChangeList.push({ - action: 'delete', - key: encodeHex(`${tablePrefix}|${stagingRecord.uuid}`), - }); - - // TODO: Child table records are getting orphaned in the datalayer, - // because we need to generate a delete action for each one - } - }); - - return [insertRecords, updateRecords, deleteChangeList]; - }; - - static async pushToDataLayer(tableToPush, comment, author, ids = []) { - let stagedRecords; - - if (tableToPush) { - stagedRecords = await Staging.findAll({ - where: { - commited: false, - table: tableToPush, - ...(ids.length - ? { - uuid: { - [Sequelize.Op.in]: ids, - }, - } - : {}), - }, - raw: true, - }); - } else { - stagedRecords = await Staging.findAll({ - where: { - commited: false, - ...(ids.length - ? { - uuid: { - [Sequelize.Op.in]: ids, - }, - } - : {}), - }, - raw: true, - }); - } - - if (!stagedRecords.length) { - throw new Error('No records to send to datalayer'); - } - - const unitsChangeList = await Unit.generateChangeListFromStagedData( - stagedRecords, - comment, - author, - ); - - const projectsChangeList = await Project.generateChangeListFromStagedData( - stagedRecords, - comment, - author, - ); - - const unifiedChangeList = { - ...projectsChangeList, - ...unitsChangeList, - issuances: [ - ...unitsChangeList.issuances, - ...projectsChangeList.issuances, - ], - labels: [...unitsChangeList.labels, ...projectsChangeList.labels], - }; - - const myOrganization = await Organization.findOne({ - where: { isHome: true }, - raw: true, - }); - - // sort so that deletes are first and inserts second - const finalChangeList = _.uniqBy( - _.sortBy(_.flatten(_.values(unifiedChangeList)), 'action'), - (v) => [v.action, v.key].join(), - ); - - await datalayer.pushDataLayerChangeList( - myOrganization.registryId, - finalChangeList, - async () => { - // The push failed so revert the commited staging records. - await Staging.update( - { failedCommit: true }, - { where: { commited: true } }, - ); - }, - ); - } -} - -Staging.init(ModelTypes, { - sequelize, - modelName: 'staging', - freezeTableName: true, - timestamps: true, -}); - -export { Staging }; +'use strict'; + +import _ from 'lodash'; +import Sequelize from 'sequelize'; +const Op = Sequelize.Op; + +const { Model } = Sequelize; +import { Project, Unit, Organization, Issuance, Meta } from '../../models'; +import { encodeHex, generateOffer } from '../../utils/datalayer-utils'; + +import * as rxjs from 'rxjs'; +import { sequelize } from '../../database'; + +import datalayer from '../../datalayer'; +import { makeOffer } from '../../datalayer/persistance'; + +import ModelTypes from './staging.modeltypes.cjs'; +import { formatModelAssociationName } from '../../utils/model-utils.js'; + +import { + createXlsFromSequelizeResults, + transformFullXslsToChangeList, +} from '../../utils/xls'; + +class Staging extends Model { + static changes = new rxjs.Subject(); + + static async create(values, options) { + Staging.changes.next(['staging']); + return super.create(values, options); + } + + static async destroy(values) { + Staging.changes.next(['staging']); + return super.destroy(values); + } + + static async upsert(values, options) { + Staging.changes.next(['staging']); + return super.upsert(values, options); + } + + static generateOfferFile = async () => { + const stagingRecord = await Staging.findOne({ + // where: { isTransfer: true }, + where: { commited: false }, + raw: true, + }); + + const takerProjectRecord = _.head(JSON.parse(stagingRecord.data)); + + const myOrganization = await Organization.findOne({ + where: { isHome: true }, + raw: true, + }); + + const maker = { inclusions: [] }; + const taker = { inclusions: [] }; + + // The record still has the orgUid of the takerProjectRecord, + // we will update this to the correct orgUId later + maker.storeId = takerProjectRecord.orgUid; + taker.storeId = myOrganization.orgUid; + + const makerProjectRecord = await Project.findOne({ + where: { warehouseProjectId: takerProjectRecord.warehouseProjectId }, + include: Project.getAssociatedModels().map((association) => { + return { + model: association.model, + as: formatModelAssociationName(association), + }; + }), + }); + + makerProjectRecord.projectStatus = 'Transitioned'; + + const issuanceIds = makerProjectRecord.issuances.reduce((ids, issuance) => { + if (!ids.includes(issuance.id)) { + ids.push(issuance.id); + } + return ids; + }, []); + + let unitMakerRecords = await Unit.findAll({ + where: { + issuanceId: { [Op.in]: issuanceIds }, + }, + raw: true, + }); + + // Takers get an unlatered copy of all the project units from the maker + const unitTakerRecords = _.cloneDeep(unitMakerRecords); + + unitMakerRecords = unitMakerRecords.map((record) => { + record.unitStatus = 'Exported'; + return record; + }); + + const primaryProjectKeyMap = { + project: 'warehouseProjectId', + projectLocations: 'id', + labels: 'id', + issuances: 'id', + coBenefits: 'id', + relatedProjects: 'id', + estimations: 'id', + projectRatings: 'id', + }; + + const primaryUnitKeyMap = { + unit: 'warehouseUnitId', + labels: 'id', + label_units: 'id', + issuances: 'id', + }; + + const makerProjectXslsSheets = createXlsFromSequelizeResults({ + rows: [makerProjectRecord], + model: Project, + toStructuredCsv: true, + }); + + const takerProjectXslsSheets = createXlsFromSequelizeResults({ + rows: [takerProjectRecord], + model: Project, + toStructuredCsv: true, + }); + + const makerUnitXslsSheets = createXlsFromSequelizeResults({ + rows: unitMakerRecords, + model: Unit, + toStructuredCsv: true, + }); + + const takerUnitXslsSheets = createXlsFromSequelizeResults({ + rows: unitTakerRecords, + model: Unit, + toStructuredCsv: true, + }); + + const takerProjectInclusions = await transformFullXslsToChangeList( + takerProjectXslsSheets, + 'insert', + primaryProjectKeyMap, + ); + + const makerProjectInclusions = await transformFullXslsToChangeList( + makerProjectXslsSheets, + 'insert', + primaryProjectKeyMap, + ); + + const makerUnitInclusions = await transformFullXslsToChangeList( + makerUnitXslsSheets, + 'insert', + primaryUnitKeyMap, + ); + + const takerUnitInclusions = await transformFullXslsToChangeList( + takerUnitXslsSheets, + 'insert', + primaryUnitKeyMap, + ); + + /* Object.keys(maker.inclusions).forEach((table) => { + maker.inclusions[table] = maker.inclusions[table] + .filter((inclusion) => inclusion.action !== 'delete') + .map((inclusion) => ({ key: inclusion.key, value: inclusion.value })); + });*/ + + maker.inclusions.push( + ...makerProjectInclusions.project + .filter((inclusion) => inclusion.action !== 'delete') + .map((inclusion) => ({ + key: inclusion.key, + value: inclusion.value, + })), + ); + + maker.inclusions.push( + ...makerUnitInclusions.unit + .filter((inclusion) => inclusion.action !== 'delete') + .map((inclusion) => ({ + key: inclusion.key, + value: inclusion.value, + })), + ); + + taker.inclusions.push( + ...takerProjectInclusions.project + .filter((inclusion) => inclusion.action !== 'delete') + .map((inclusion) => ({ + key: inclusion.key, + value: inclusion.value, + })), + ); + + taker.inclusions.push( + ...takerUnitInclusions.unit + .filter((inclusion) => inclusion.action !== 'delete') + .map((inclusion) => ({ + key: inclusion.key, + value: inclusion.value, + })), + ); + + const offerInfo = generateOffer(maker, taker); + const offer = makeOffer(offerInfo); + + await Meta.upsert({ + metaKey: 'activeOfferTradeId', + metaValue: offer.trade_id, + }); + + return offer; + }; + + // If the record was commited but the diff.original is null + // that means that the original record no longer exists and + // the staging record should be cleaned up. + static cleanUpCommitedAndInvalidRecords = async () => { + const stagingRecords = await Staging.findAll({ raw: true }); + + const stagingRecordsToDelete = await Promise.all( + stagingRecords.filter(async (record) => { + if (record.commited === 1) { + const { uuid, table, action, data } = record; + const diff = await Staging.getDiffObject(uuid, table, action, data); + return diff.original == null; + } + return false; + }), + ); + + await Staging.destroy({ + where: { uuid: stagingRecordsToDelete.map((record) => record.uuid) }, + }); + }; + + static getDiffObject = async (uuid, table, action, data) => { + const diff = {}; + if (action === 'INSERT') { + diff.original = {}; + diff.change = JSON.parse(data); + } + + if (action === 'UPDATE') { + diff.change = JSON.parse(data); + + let original; + + if (table === 'Projects') { + original = await Project.findOne({ + where: { warehouseProjectId: uuid }, + include: Project.getAssociatedModels().map((association) => { + return { + model: association.model, + as: formatModelAssociationName(association), + }; + }), + }); + + // Show the issuance data if its being reused + // this is just for view purposes onlys + await Promise.all( + diff.change.map(async (record) => { + if (record.issuanceId) { + const issuance = await Issuance.findOne({ + where: { id: record.issuanceId }, + }); + + record.issuance = issuance.dataValues; + } + }), + ); + } else if (table === 'Units') { + original = await Unit.findOne({ + where: { warehouseUnitId: uuid }, + include: Unit.getAssociatedModels().map((association) => { + return { + model: association.model, + as: formatModelAssociationName(association), + }; + }), + }); + + // Show the issuance data if its being reused, + // this is just for view purposes onlys + await Promise.all( + diff.change.map(async (record) => { + if (record.issuanceId) { + const issuance = await Issuance.findOne({ + where: { id: record.issuanceId }, + }); + + record.issuance = issuance.dataValues; + } + }), + ); + } + + diff.original = original; + } + + if (action === 'DELETE') { + let original; + + if (table === 'Projects') { + original = await Project.findOne({ + where: { warehouseProjectId: uuid }, + include: Project.getAssociatedModels().map((association) => { + return { + model: association.model, + as: formatModelAssociationName(association), + }; + }), + }); + } else if (table === 'Units') { + original = await Unit.findOne({ + where: { warehouseUnitId: uuid }, + include: Unit.getAssociatedModels().map((association) => { + return { + model: association.model, + as: formatModelAssociationName(association), + }; + }), + }); + } + + diff.original = original; + diff.change = {}; + } + + return diff; + }; + + static seperateStagingDataIntoActionGroups = (stagedData, table) => { + const insertRecords = []; + const updateRecords = []; + const deleteChangeList = []; + + stagedData + .filter((stagingRecord) => stagingRecord.table === table) + .forEach((stagingRecord) => { + // TODO: Think of a better place to mark the records as commited + Staging.update( + { commited: true }, + { where: { uuid: stagingRecord.uuid } }, + ); + if (stagingRecord.action === 'INSERT') { + insertRecords.push(...JSON.parse(stagingRecord.data)); + } else if (stagingRecord.action === 'UPDATE') { + let tablePrefix = table.toLowerCase(); + // hacky fix to account for the units and projects table not + // being lowercase and plural in the xsls transformation + if (tablePrefix === 'units' || tablePrefix === 'projects') { + tablePrefix = tablePrefix.replace(/s\s*$/, ''); + } + + deleteChangeList.push({ + action: 'delete', + key: encodeHex(`${tablePrefix}|${stagingRecord.uuid}`), + }); + + // TODO: Child table records are getting orphaned in the datalayer, + // because we need to generate a delete action for each one + + updateRecords.push(...JSON.parse(stagingRecord.data)); + } else if (stagingRecord.action === 'DELETE') { + let tablePrefix = table.toLowerCase(); + + // hacky fix to account for the units and projects table not + // being lowercase and plural in the xsls transformation + if (tablePrefix === 'units' || tablePrefix === 'projects') { + tablePrefix = tablePrefix.replace(/s\s*$/, ''); + } + + deleteChangeList.push({ + action: 'delete', + key: encodeHex(`${tablePrefix}|${stagingRecord.uuid}`), + }); + + // TODO: Child table records are getting orphaned in the datalayer, + // because we need to generate a delete action for each one + } + }); + + return [insertRecords, updateRecords, deleteChangeList]; + }; + + static async pushToDataLayer(tableToPush, comment, author, ids = []) { + let stagedRecords; + + if (tableToPush) { + stagedRecords = await Staging.findAll({ + where: { + commited: false, + table: tableToPush, + ...(ids.length + ? { + uuid: { + [Sequelize.Op.in]: ids, + }, + } + : {}), + }, + raw: true, + }); + } else { + stagedRecords = await Staging.findAll({ + where: { + commited: false, + ...(ids.length + ? { + uuid: { + [Sequelize.Op.in]: ids, + }, + } + : {}), + }, + raw: true, + }); + } + + if (!stagedRecords.length) { + throw new Error('No records to send to datalayer'); + } + + const unitsChangeList = await Unit.generateChangeListFromStagedData( + stagedRecords, + comment, + author, + ); + + const projectsChangeList = await Project.generateChangeListFromStagedData( + stagedRecords, + comment, + author, + ); + + const unifiedChangeList = { + ...projectsChangeList, + ...unitsChangeList, + issuances: [ + ...unitsChangeList.issuances, + ...projectsChangeList.issuances, + ], + labels: [...unitsChangeList.labels, ...projectsChangeList.labels], + }; + + const myOrganization = await Organization.findOne({ + where: { isHome: true }, + raw: true, + }); + + // sort so that deletes are first and inserts second + const finalChangeList = _.uniqBy( + _.sortBy(_.flatten(_.values(unifiedChangeList)), 'action'), + (v) => [v.action, v.key].join(), + ); + + await datalayer.pushDataLayerChangeList( + myOrganization.registryId, + finalChangeList, + async () => { + // The push failed so revert the commited staging records. + await Staging.update( + { failedCommit: true }, + { where: { commited: true } }, + ); + }, + ); + } +} + +Staging.init(ModelTypes, { + sequelize, + modelName: 'staging', + freezeTableName: true, + timestamps: true, +}); + +export { Staging }; diff --git a/src/routes/v1/index.js b/src/routes/v1/index.js index 3f4f7024..07ac5eb4 100644 --- a/src/routes/v1/index.js +++ b/src/routes/v1/index.js @@ -13,6 +13,7 @@ import { AuditRouter, GovernanceRouter, FileStoreRouter, + OfferRouter, } from './resources'; V1Router.use('/projects', ProjectRouter); @@ -24,5 +25,6 @@ V1Router.use('/labels', LabelRouter); V1Router.use('/audit', AuditRouter); V1Router.use('/governance', GovernanceRouter); V1Router.use('/filestore', FileStoreRouter); +V1Router.use('/offer', OfferRouter); export { V1Router }; diff --git a/src/routes/v1/resources/index.js b/src/routes/v1/resources/index.js index 8286bf94..ba674b15 100644 --- a/src/routes/v1/resources/index.js +++ b/src/routes/v1/resources/index.js @@ -7,3 +7,4 @@ export * from './labels'; export * from './audit'; export * from './governance'; export * from './filestore'; +export * from './offer'; diff --git a/src/routes/v1/resources/offer.js b/src/routes/v1/resources/offer.js new file mode 100644 index 00000000..9b696fed --- /dev/null +++ b/src/routes/v1/resources/offer.js @@ -0,0 +1,16 @@ +'use strict'; + +import express from 'express'; +import { OfferController } from '../../../controllers'; + +const OfferRouter = express.Router(); + +OfferRouter.get('/', (req, res) => { + return OfferController.generateOfferFile(req, res); +}); + +OfferRouter.delete('/', (req, res) => { + return OfferController.cancelActiveOffer(req, res); +}); + +export { OfferRouter };