Skip to content

Commit

Permalink
Updates create-users to check users-info doc count before creation (#…
Browse files Browse the repository at this point in the history
…227)

Before creating users from csv, calls api/v1/users-info for each user and displays a warning and prompt when API warns about doc counts.

medic/cht-core#5362
  • Loading branch information
dianabarsan authored and ngaruko committed Sep 12, 2019
1 parent 91ca4db commit fc7ad56
Show file tree
Hide file tree
Showing 3 changed files with 347 additions and 56 deletions.
135 changes: 90 additions & 45 deletions src/fn/create-users.js
@@ -1,52 +1,97 @@
const fs = require('../lib/sync-fs');
const info = require('../lib/log').info;
const csvParse = require('csv-parse/lib/sync');
const { info, warn, error } = require('../lib/log');
const request = require('request-promise-native');
const readline = require('readline-sync');

module.exports = (projectDir, couchUrl) => {
if(!couchUrl) throw new Error('Server URL must be defined to use this function.');
const instanceUrl = couchUrl.replace(/\/medic$/, '');

return Promise.resolve()
.then(() => {
const csvPath = `${projectDir}/users.csv`;

if(!fs.exists(csvPath)) throw new Error(`User csv file not found at ${csvPath}`);

const { cols, rows } = fs.readCsv(csvPath);
const usernameIndex = cols.indexOf('username');
const passwordIndex = cols.indexOf('password');
const rolesIndex = cols.indexOf('roles');
const placeIdIndex = cols.indexOf('place');
const contactIndex = cols.indexOf('contact');

return rows.reduce((promiseChain, row) => {
const username = row[usernameIndex];
const password = row[passwordIndex];
const roles = row[rolesIndex].split(':');
const contact = contactIndex === -1 ? prefixedProperties(cols, row, 'contact.') : row[contactIndex];
const place = placeIdIndex === -1 ? prefixedProperties(cols, row, 'place.') : row[placeIdIndex];
const requestObject = { username, password, roles, place, contact };

return promiseChain
.then(() => {
info('Creating user', username);
return request({
uri: `${instanceUrl}/api/v1/users`,
method: 'POST',
json: true,
body: requestObject,
});
});
}, Promise.resolve());
const nestPrefixedProperties = (obj, name) => {
const nested = {};
const prefix = `${name}.`;
Object
.keys(obj)
.filter(key => key.startsWith(prefix))
.forEach(key => {
nested[key.substring(prefix.length)] = obj[key];
delete obj[key];
});
return obj[name] || nested;
};

function prefixedProperties (cols, row, prefix) {
const indices = {};
cols.forEach(col => {
if (col.startsWith(prefix)) {
indices[col.substring(prefix.length)] = row[cols.indexOf(col)];
}
const parseUsersData = (csvData) => {
const users = csvParse(csvData, { columns: true });
users.forEach(user => {
user.contact = nestPrefixedProperties(user, 'contact');
user.place = nestPrefixedProperties(user, 'place');
user.roles = user.roles && user.roles.split(':');
});
return indices;
}
return users;
};

const getUserInfo = async (instanceUrl, user) => {
const getId = (obj) => typeof obj === 'string' ? obj : obj._id;
const facilityId = getId(user.place);
if (!facilityId) {
// new place - nothing to check
return;
}

const params = {
facility_id: facilityId,
role: JSON.stringify(user.roles),
contact: getId(user.contact),
};

info(`Requesting user-info for "${user.username}"`);
let result;
try {
result = await request.get(`${instanceUrl}/api/v1/users-info`, { qs: params, json: true });
} catch (err) {
// we can safely ignore some errors
// - 404: This endpoint was only added in 3.7
// - 400: The endpoint throws an error if the requested roles are "online"
// - 400: Missing facility or role, the corresponding user create request will fail
if (err.statusCode !== 404 && err.statusCode !== 400) {
throw err;
}
}
return result;
};

const createUser = (instanceUrl, user) => {
return request.post(`${instanceUrl}/api/v1/users`, { json: true, body: user });
};

module.exports = async (projectDir, couchUrl) => {
if(!couchUrl) {
throw new Error('Server URL must be defined to use this function.');
}

const instanceUrl = couchUrl.replace(/\/medic$/, '');
const csvPath = `${projectDir}/users.csv`;
if(!fs.exists(csvPath)) {
throw new Error(`User csv file not found at ${csvPath}`);
}

const users = parseUsersData(fs.read(csvPath));
const warnings = [];
for (let user of users) {
const userInfo = await getUserInfo(instanceUrl, user);
if (userInfo && userInfo.warn) {
warnings.push(`The user "${user.username}" would replicate ${userInfo.total_docs}, which is above the recommended limit of ${userInfo.limit}.`);
}
}
if (warnings.length) {
warnings.forEach(warning => warn(warning));
warn('Are you sure you want to continue?');
if(!readline.keyInYN()) {
error('User failed to confirm action.');
process.exit(1);
return; // stop execution in tests
}
}

for (let user of users) {
info(`Creating user ${user.username}`);
await createUser(instanceUrl, user);
}
};
5 changes: 5 additions & 0 deletions test/data/create-users/multiple-existing-place/users.csv
@@ -0,0 +1,5 @@
username,password,roles,contact,place
todd,Secret_1,district-admin,contact_uuid_1,place_uuid_1
jack,Secret_1,district-admin:supervisor,contact_uuid_2,place_uuid_2
jill,Secret_1,role1:role2:role3,contact_uuid_3,place_uuid_3
john,Secret_1,role2:role3,contact_uuid_4,place_uuid_4

0 comments on commit fc7ad56

Please sign in to comment.