diff --git a/package-lock.json b/package-lock.json index 587aa45f..5c4ce084 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cadt", - "version": "1.7.15", + "version": "1.7.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cadt", - "version": "1.7.15", + "version": "1.7.16", "dependencies": { "@babel/eslint-parser": "^7.23.10", "async-mutex": "^0.4.1", diff --git a/src/controllers/audit.controller.js b/src/controllers/audit.controller.js index 2716df9c..eae5735e 100644 --- a/src/controllers/audit.controller.js +++ b/src/controllers/audit.controller.js @@ -1,9 +1,10 @@ import { Audit } from '../models'; - +import _ from 'lodash'; import { paginationParams, optionallyPaginatedResponse, } from '../utils/helpers'; +import { assertIfReadOnlyMode } from '../utils/data-assertions.js'; export const findAll = async (req, res) => { try { @@ -20,7 +21,7 @@ export const findAll = async (req, res) => { return res.json(optionallyPaginatedResponse(auditResults, page, limit)); } catch (error) { res.status(400).json({ - message: 'Can not retreive audit data', + message: 'Can not retrieve audit data', error: error.message, success: false, }); @@ -32,9 +33,73 @@ export const findConflicts = async (req, res) => { return res.json(await Audit.findConflicts()); } catch (error) { res.status(400).json({ - message: 'Can not retreive audit data', + message: 'Can not retrieve audit data', error: error.message, success: false, }); } }; + +export const resetToGeneration = async (req, res) => { + try { + await assertIfReadOnlyMode(); + const { generation, orgUid } = req.body; + + const result = await Audit.resetToGeneration(generation, orgUid); + if (_.isNil(result)) { + throw new Error('query failed'); + } + return res.json({ + message: result + ? 'reset to generation ' + String(generation) + : 'no matching records', + success: true, + }); + } catch (error) { + if (error.message === 'SQLITE_BUSY: database is locked') { + res.status(400).json({ + message: 'failed to change generation', + error: 'cadt is currently syncing, please try again later', + success: false, + }); + } else { + res.status(400).json({ + message: 'failed to change generation', + error: error.message, + success: false, + }); + } + } +}; + +export const resetToDate = async (req, res) => { + try { + await assertIfReadOnlyMode(); + const { date, orgUid, includeHomeOrg } = req.body; + + const result = orgUid + ? await Audit.resetOrgToDate(date, orgUid) + : await Audit.resetToDate(date, includeHomeOrg); + if (_.isNil(result)) { + throw new Error('query failed'); + } + return res.json({ + message: result ? 'reset to date ' + String(date) : 'no matching records', + success: true, + }); + } catch (error) { + if (error.message === 'SQLITE_BUSY: database is locked') { + res.status(400).json({ + message: 'failed to reset to date', + error: 'cadt is currently syncing, please try again later', + success: false, + }); + } else { + res.status(400).json({ + message: 'failed to reset to date', + error: error.message, + success: false, + }); + } + } +}; diff --git a/src/models/audit/audit.model.js b/src/models/audit/audit.model.js index 4851504a..e10057df 100644 --- a/src/models/audit/audit.model.js +++ b/src/models/audit/audit.model.js @@ -6,6 +6,7 @@ import { sequelize, safeMirrorDbHandler } from '../../database'; import { AuditMirror } from './audit.model.mirror'; import ModelTypes from './audit.modeltypes.cjs'; import findDuplicateIssuancesSql from './sql/find-duplicate-issuances.sql.js'; +import { Organization } from '../organizations/index.js'; class Audit extends Model { static async create(values, options) { @@ -45,6 +46,56 @@ class Audit extends Model { const [results] = await sequelize.query(findDuplicateIssuancesSql); return results; } + + static async resetToGeneration(generation, orgUid) { + const where = { + generation: { [Sequelize.Op.gt]: generation }, + }; + + if (orgUid) { + where.orgUid = orgUid; + } + + return await Audit.destroy({ where }); + } + + static async resetToDate(date, includeHomeOrg) { + const timestampInSeconds = Math.round(new Date(date).valueOf() / 1000); + const homeOrgUid = Organization.getHomeOrg()?.uid; + + const conditions = [ + sequelize.where( + sequelize.cast( + sequelize.col('onChainConfirmationTimeStamp'), + 'UNSIGNED', + ), + { [Sequelize.Op.gt]: timestampInSeconds }, + ), + ]; + + if (!includeHomeOrg && homeOrgUid) { + conditions.push({ orguid: { [Sequelize.Op.ne]: homeOrgUid } }); + } + + return await Audit.destroy({ where: { [Sequelize.Op.and]: conditions } }); + } + + static async resetOrgToDate(date, orgUid) { + const timestampInSeconds = Math.round(new Date(date).valueOf() / 1000); + + return await Audit.destroy({ + where: { + orgUid: orgUid, + [Sequelize.Op.and]: sequelize.where( + sequelize.cast( + sequelize.col('onchainConfirmationTimeStamp'), + 'UNSIGNED', + ), + { [Sequelize.Op.gt]: timestampInSeconds }, + ), + }, + }); + } } Audit.init(ModelTypes, { diff --git a/src/routes/v1/resources/audit.js b/src/routes/v1/resources/audit.js index 0a485eed..60bed495 100644 --- a/src/routes/v1/resources/audit.js +++ b/src/routes/v1/resources/audit.js @@ -4,7 +4,11 @@ import express from 'express'; import joiExpress from 'express-joi-validation'; import { AuditController } from '../../../controllers'; -import { auditGetSchema } from '../../../validations'; +import { + auditGetSchema, + auditResetToDateSchema, + auditResetToGenerationSchema, +} from '../../../validations'; const validator = joiExpress.createValidator({ passError: true }); const AuditRouter = express.Router(); @@ -17,4 +21,20 @@ AuditRouter.get('/findConflicts', (req, res) => { return AuditController.findConflicts(req, res); }); +AuditRouter.post( + '/resetToGeneration', + validator.body(auditResetToGenerationSchema), + (req, res) => { + return AuditController.resetToGeneration(req, res); + }, +); + +AuditRouter.post( + '/resetToDate', + validator.body(auditResetToDateSchema), + (req, res) => { + return AuditController.resetToDate(req, res); + }, +); + export { AuditRouter }; diff --git a/src/tasks/index.js b/src/tasks/index.js index 4ce68209..ac6c0efe 100644 --- a/src/tasks/index.js +++ b/src/tasks/index.js @@ -6,6 +6,7 @@ import syncRegistries from './sync-registries'; import syncOrganizationMeta from './sync-organization-meta'; import syncGovernanceBody from './sync-governance-body'; import mirrorCheck from './mirror-check'; +import resetAuditTable from './reset-audit-table'; const scheduler = new ToadScheduler(); @@ -25,6 +26,7 @@ const start = () => { syncRegistries, syncOrganizationMeta, mirrorCheck, + resetAuditTable, ]; defaultJobs.forEach((defaultJob) => { jobRegistry[defaultJob.id] = defaultJob; diff --git a/src/tasks/reset-audit-table.js b/src/tasks/reset-audit-table.js new file mode 100644 index 00000000..bf715819 --- /dev/null +++ b/src/tasks/reset-audit-table.js @@ -0,0 +1,66 @@ +import { SimpleIntervalJob, Task } from 'toad-scheduler'; +import { Audit, Meta } from '../models'; +import { logger } from '../config/logger.cjs'; +import dotenv from 'dotenv'; +import _ from 'lodash'; +dotenv.config(); + +const task = new Task('reset-audit-table', async () => { + try { + const metaResult = await Meta.findOne({ + where: { + metaKey: 'may2024AuditResetTaskHasRun', + }, + attributes: ['metaValue'], + raw: true, + }); + + const taskHasRun = metaResult?.metaValue; + + if (taskHasRun === 'true') { + return; + } + + logger.info('performing audit table reset'); + + const where = { type: 'NO CHANGE' }; + const noChangeEntries = await Audit.findAll({ where }); + + if (noChangeEntries.length) { + const result = await Audit.resetToDate('2024-05-11'); + logger.info( + 'audit table has been reset, records modified: ' + String(result), + ); + } + + if (_.isNil(taskHasRun)) { + await Meta.create({ + metaKey: 'may2024AuditResetTaskHasRun', + metaValue: 'true', + }); + } else { + await Meta.update( + { metavalue: 'true' }, + { + where: { + metakey: 'may2024AuditResetTaskHasRun', + }, + returning: true, + }, + ); + } + } catch (error) { + logger.error('Retrying in 600 seconds', error); + } +}); + +const job = new SimpleIntervalJob( + { + seconds: 600, + runImmediately: true, + }, + task, + { id: 'reset-audit-table', preventOverrun: true }, +); + +export default job; diff --git a/src/validations/audit.validations.js b/src/validations/audit.validations.js index df44011d..9a017b69 100644 --- a/src/validations/audit.validations.js +++ b/src/validations/audit.validations.js @@ -9,3 +9,14 @@ export const auditGetSchema = Joi.object() }) .with('page', 'limit') .with('limit', 'page'); + +export const auditResetToGenerationSchema = Joi.object().keys({ + generation: Joi.number(), + orgUid: Joi.string(), +}); + +export const auditResetToDateSchema = Joi.object().keys({ + date: Joi.date(), + orgUid: Joi.string().optional(), + includeHomeOrg: Joi.bool().optional(), +});