Advanced Tutorial: Adding a new database table column
Script
'use strict';
const {
app,
loggerPlugin,
toolsLib
} = this.pluginLibraries;
const lodash = require('lodash');
const sequelize = require('sequelize');
const umzug = require('umzug');
const cron = require('node-cron');
const { body, validationResult } = require('express-validator');
const AVAILABLE_REQUIREMENTS = {
kyc_verification: {
title: 'KYC Verification',
description: 'Require users to verify their identity'
},
email_verification: {
title: 'Email Verification',
description: 'Require users to verify their email'
},
sms_verification: {
title: 'SMS Verification',
description: 'Require users to input a valid phone number'
},
bank_verification: {
title: 'Bank Verification',
description: 'Require users to input a valid bank account and status is 3'
}
};
const init = async () => {
const umzugInstance = new umzug({
storage: 'sequelize',
storageOptions: {
sequelize: toolsLib.database.getModel('sequelize'),
modelName: 'PluginMigrations',
tableName: 'PluginMigrations'
},
upName: 'up',
downName: 'down',
migrations: umzug.migrationsList(
[
{
name: 'automatic_tier_upgrade-add_requirements_column',
up: (queryInterface, Sequelize) => queryInterface.describeTable('Tiers')
.then((table) => {
if (table['requirements']) {
return new Promise((resolve) => resolve());
} else {
return queryInterface.addColumn('Tiers', 'requirements', {
type: Sequelize.JSONB,
defaultValue: []
});
}
}),
down: (queryInterface, Sequelize) => queryInterface.describeTable('Tiers')
.then((table) => {
if (table['requirements']) {
return queryInterface.removeColumn('Tiers', 'requirements');
} else {
return true;
}
})
}
],
[toolsLib.database.getModel('sequelize').getQueryInterface(), sequelize]
)
});
const pending = await umzugInstance.pending();
if (pending.length > 0) {
await umzugInstance.up('automatic_tier_upgrade-add_requirements_column');
}
};
const findAllTiers = async () => {
return toolsLib.database.getModel('sequelize').query('SELECT * FROM "Tiers" ORDER BY id ASC', {
raw: true,
type: sequelize.QueryTypes.SELECT
});
};
const findTier = async (id) => {
return toolsLib.database.getModel('sequelize').query('SELECT * FROM "Tiers" WHERE id = :id', {
plain: true,
raw: true,
replacements: {
id
},
type: sequelize.QueryTypes.SELECT
});
};
const updateTier = async (id, requirements) => {
const tier = await toolsLib.database.getModel('sequelize').query('UPDATE "Tiers" SET requirements = :requirements WHERE id = :id RETURNING *', {
replacements: {
id,
requirements: JSON.stringify(requirements)
}
});
return tier[0][0];
};
const validateTierUpdate = async (tier_id, data = []) => {
const tiers = await findAllTiers();
// if removing all requirements, check if upper requirements exist
if (lodash.isEmpty(data)) {
const isInvalid = tiers.some((tier) => {
return tier.id > tier_id && !lodash.isEmpty(tier.requirements);
});
if (isInvalid) {
throw new Error('Cannot remove requirements if a higher tier has requirements set');
}
return;
}
// check if lower tier has requirements set if level is 3 or above
if (tier_id > 2) {
const isInvalid = tiers.some((tier) => {
return tier.id >= 2 && tier.id < tier_id && lodash.isEmpty(tier.requirements);
});
if (isInvalid) {
throw new Error('Lower tiers must have requirements set');
}
}
// check if given static requirements are already set for other tier levels
const isInvalid = tiers.some((tier) => {
const existingRequirements = lodash.intersection(data, tier.requirements);
return tier.id !== tier_id && !lodash.isEmpty(existingRequirements);
});
if (isInvalid) {
throw new Error('Static requirements can only be set for one tier');
}
};
const runner = async () => {
const tiers = await findAllTiers();
const users = await toolsLib.database.findAll('user', {
where: {
activated: true,
flagged: false
},
raw: true,
attributes: [
'id',
'email',
'phone_number',
'id_data',
'verification_level',
'email_verified',
'activated',
'bank_account',
'flagged'
]
});
const groupedUsers = lodash.groupBy(users, 'verification_level');
for (const level in groupedUsers) {
const userLevel = parseInt(level);
for (const user of groupedUsers[level]) {
loggerPlugin.debug(
'AUTO TIER UPGRADE PLUGIN',
`Checking verifications for user ${user.email} with level ${userLevel}`
);
let updatedLevel = userLevel;
const checkedTiers = tiers.filter((tier) => tier.id >= 2 && tier.id > userLevel);
const userVerifications = [];
if (user.id_data.status === 3) {
userVerifications.push('kyc_verification');
}
if (!lodash.isEmpty(user.phone_number)) {
userVerifications.push('sms_verification');
}
if (user.email_verified) {
userVerifications.push('email_verification');
}
if(!lodash.isEmpty(user.bank_account) && user.bank_account.some((account) => account.status === 3)){
userVerifications.push('bank_verification');
}
loggerPlugin.debug(
'AUTO TIER UPGRADE PLUGIN',
'User verifications',
userVerifications
);
for (const tier of checkedTiers) {
if (lodash.isEmpty(tier.requirements)) {
loggerPlugin.debug(
'AUTO TIER UPGRADE PLUGIN',
`Tier ${tier.id} does not have any requirement set`
);
break;
}
if (lodash.difference(tier.requirements, userVerifications).length === 0) {
loggerPlugin.verbose(
'AUTO TIER UPGRADE PLUGIN',
`User ${user.email} meets requirements for tier`,
tier.id
);
updatedLevel = tier.id;
}
}
if (updatedLevel > userLevel) {
loggerPlugin.verbose(
'AUTO TIER UPGRADE PLUGIN',
`User ${user.email} level will be changed from ${userLevel} to ${updatedLevel}`
);
await toolsLib.user.changeUserVerificationLevelById(user.id, updatedLevel);
}
}
}
};
const cronjob = cron.schedule('0 0 0 * * *', async () => {
loggerPlugin.verbose(
'/plugins/automatic-tier-upgrade Upgrade start'
);
try {
await runner();
} catch (err) {
loggerPlugin.error(
'/plugins/automatic-tier-upgrade error during upgrade:',
err.message
);
}
}, {
scheduled: false
});
init()
.then(() => {
cronjob.start();
app.get(
'/plugins/automatic-tier-upgrade/available-requirements',
[toolsLib.security.verifyBearerTokenExpressMiddleware(['admin'])],
(req, res) => {
loggerPlugin.verbose(
req.uuid,
'/plugins/automatic-tier-upgrade/available-requirements',
req.auth.sub
);
return res.json(AVAILABLE_REQUIREMENTS);
}
);
app.get(
'/plugins/automatic-tier-upgrade/requirements',
[toolsLib.security.verifyBearerTokenExpressMiddleware(['admin'])],
async (req, res) => {
loggerPlugin.verbose(
req.uuid,
'GET /plugins/automatic-tier-upgrade/requirements auth',
req.auth.sub
);
try {
const tiers = await findAllTiers();
let response = {};
for (const tier of tiers) {
response[tier.id] = tier.requirements;
}
return res.json(response);
} catch (err) {
loggerPlugin.error(
req.uuid,
'GET /plugins/automatic-tier-upgrade/requirements err',
err.message
);
return res.status(err.status || 400).json({message: err.message});
}
}
);
app.put(
'/plugins/automatic-tier-upgrade/requirements',
[
toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
body('tier').isInt({ min: 1 }),
body('requirements').isArray()
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({errors: errors.array()});
}
loggerPlugin.verbose(
req.uuid,
'PUT /plugins/automatic-tier-upgrade/requirements auth',
req.auth.sub
);
try {
const { tier: level, requirements } = req.body;
const tier = await findTier(level);
if (!tier) {
throw new Error(`Tier ${level} does not exist`);
}
const formattedRequirements = lodash.uniq(requirements);
if (lodash.difference(formattedRequirements, Object.keys(AVAILABLE_REQUIREMENTS)).length > 0) {
throw new Error('Invalid requirements given');
}
await validateTierUpdate(level, requirements);
const updatedTier = await updateTier(tier.id, requirements);
return res.json(
lodash.pick(updatedTier, ['id', 'requirements'])
);
} catch (err) {
loggerPlugin.error(
req.uuid,
'PUT /plugins/automatic-tier-upgrade/requirements err',
err.message
);
return res.status(err.status || 400).json({message: err.message});
}
});
})
.catch((err) => {
loggerPlugin.error(
'AUTOMATIC TIER UPGRADE PLUGIN error during initialization:',
err.message
);
});Breakdown
Create and run the new migration in the init function
Add helper functions for accessing the new column
PreviousAdvanced Tutorial: Using the user meta fieldNextAdvanced Tutorial: Creating a new database table
Last updated