Skip to content

Commit

Permalink
Allow creation of user docs from a CSV file
Browse files Browse the repository at this point in the history
Issue: #61
  • Loading branch information
alxndrsn committed Mar 21, 2018
1 parent 80a2dca commit a24b210
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 15 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ To achieve this, create a file called `settings.inherit.json` in your project's

* only upload things which have changed (this could be a separate mode - e.g. `update` vs `configure`)

# create-users **[ALPHA]**

N.B. this feature is currently in development, and probably not ready for production yet.

To create users on a remote server, use the `create-users` action. The CSV file should be called `users.csv`, and an example is available [in the tests directory](test/data/create-users/users.csv).

# csv-to-docs

To convert CSV to JSON docs, use the `csv-to-docs` action.
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "medic-conf",
"version": "1.11.7",
"version": "1.11.8",
"description": "Configure Medic Mobile deployments",
"main": "index.js",
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions src/cli/supported-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module.exports = [
'convert-app-forms',
'convert-collect-forms',
'convert-contact-forms',
'create-users',
'csv-to-docs',
'delete-forms',
'delete-all-forms',
Expand Down
48 changes: 48 additions & 0 deletions src/fn/create-users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const fs = require('../lib/sync-fs');
const info = require('../lib/log').info;
const request = require('request-promise-native');

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);

return rows.reduce((promiseChain, row) => {
const username = row[cols.indexOf('username')];
const password = row[cols.indexOf('password')];
const type = row[cols.indexOf('type')];

const contact = prefixedProperties(cols, row, 'contact.');
const place = prefixedProperties(cols, row, 'place.' );

const requestObject = { username, password, type, place, contact };

return promiseChain
.then(() => {
info('Creating user', username);
return request({
uri: `${instanceUrl}/api/v1/users`,
method: 'POST',
json: true,
body: requestObject,
});
});
}, Promise.resolve());
});
};

function prefixedProperties(cols, row, prefix) {
return cols
.filter(col => col.startsWith(prefix))
.reduce((obj, col) => {
obj[col.substring(prefix.length)] = row[cols.indexOf(col)];
return obj;
}, {});
}
14 changes: 2 additions & 12 deletions src/fn/csv-to-docs.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const csvParse = require('csv-parse/lib/sync');
const fs = require('../lib/sync-fs');
const info = require('../lib/log').info;
const stringify = require('canonical-json/index2');
Expand Down Expand Up @@ -94,13 +93,13 @@ module.exports = projectDir => {
}

function processReports(report_type, csv) {
const { rows, cols } = loadCsv(csv);
const { rows, cols } = fs.readCsv(csv);
return rows
.map(r => processCsv('data_record', cols, r, { form:report_type }));
}

function processContacts(contactType, csv) {
const { rows, cols } = loadCsv(csv);
const { rows, cols } = fs.readCsv(csv);
return rows
.map(r => processCsv(contactType, cols, r));
}
Expand Down Expand Up @@ -220,15 +219,6 @@ function removeExcludedField(exclusion) {
delete exclusion.doc[exclusion.propertyName];
}

function loadCsv(csv) {
const raw = csvParse(fs.read(csv));
if(!raw.length) return { cols:[], rows:[] };
return {
cols: raw[0],
rows: raw.slice(1),
};
}

function parseColumn(rawCol, rawVal) {
let val, reference, excluded = false;

Expand Down
13 changes: 12 additions & 1 deletion src/lib/sync-fs.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const csvParse = require('csv-parse/lib/sync');
const fs = require('fs');
const mkdirp = require('mkdirp').sync;
const os = require('os');
Expand All @@ -14,6 +15,15 @@ function read(path) {
}
}

function readCsv(path) {
const raw = csvParse(read(path));
if(!raw.length) return { cols:[], rows:[] };
return {
cols: raw[0],
rows: raw.slice(1),
};
}

function readJson(path) {
try {
return JSON.parse(read(path));
Expand Down Expand Up @@ -75,8 +85,9 @@ module.exports = {
path: path,
posixPath: p => p.split(path.sep).join('/'),
read: read,
readJson: readJson,
readBinary: path => fs.readFileSync(path),
readCsv: readCsv,
readJson: readJson,
recurseFiles: recurseFiles,
readdir: fs.readdirSync,
withoutExtension: withoutExtension,
Expand Down
9 changes: 9 additions & 0 deletions test/api-stub.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@ const memdown = require('memdown');
const memPouch = require('pouchdb').defaults({ db:memdown });
const express = require('express');
const expressPouch = require('express-pouchdb');
const ExpressSpy = require('./express-spy');
const bodyParser = require('body-parser');

const mockMiddleware = new ExpressSpy();

const opts = {
inMemoryConfig: true,
logPath: 'express-pouchdb.log',
mode: 'fullCouchDB',
};
const app = express();
app.use(bodyParser.json());
app.all('/api/*', mockMiddleware.requestHandler);
app.use('/', stripAuth, expressPouch(memPouch, opts));

let server;

module.exports = {
db: new memPouch('medic'),
giveResponses: mockMiddleware.setResponses,
requestLog: () => mockMiddleware.requests.map(r => ({ method:r.method, url:r.originalUrl, body:r.body })),
start: () => {
if(server) throw new Error('Server already started.');
server = app.listen();
Expand All @@ -26,6 +34,7 @@ module.exports = {
server.close();
server = null;
delete module.exports.couchUrl;
mockMiddleware.reset();
},
};

Expand Down
1 change: 1 addition & 0 deletions test/bin/shell-completion.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe('shell-completion', () => {
'convert-app-forms',
'convert-collect-forms',
'convert-contact-forms',
'create-users',
'csv-to-docs',
'delete-forms',
'delete-all-forms',
Expand Down
3 changes: 3 additions & 0 deletions test/data/create-users/users.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
username,password,type,name,phone,contact.c_prop,place.c_prop,place.type,place.name,place.parent
alice,Secret_1,district-manager,Alice Example,+123456789,c_val_a,p_val_a,health_center,alice area,abc-123
bob,Secret_2,district-manager,Bob Demo,+987654321,c_val_b,p_val_b,health_center,bob area,def-456
34 changes: 34 additions & 0 deletions test/express-spy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module.exports = function() {
const requests = [];
const responses = [];

function reset() {
if(responses.length != 0) {
throw new Error(`Unused responses (${responses.length})!`);
}
}

function requestHandler(req, res) {
requests.push(req);

const response = responses.shift();

if(!response) return error(req, res);

res.status(response.status || 200);
res.type(response.type || 'json');
res.send(response.body || '');
}

function setResponses(...rr) {
reset();
rr.forEach(r => responses.push(r));
}

return { requests, requestHandler, reset, setResponses };
};

function error(req, res) {
res.status(500);
res.send(`Unexpected request: ${req.method} ${req.originalUrl} - no more API HTTP responses have been defined for this test. If you forgot to add one, use \`apiStub.requests.push({ status, type, body })\``);
}
66 changes: 66 additions & 0 deletions test/fn/create-users.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const api = require('../api-stub');
const assert = require('chai').assert;
const createUsers = require('../../src/fn/create-users');

describe('create-users', function() {
beforeEach(api.start);
afterEach(api.stop);

it('should create one user for each row in a CSV file', function(done) {

// given
const testDir = `data/create-users`;
api.giveResponses({ body: {} }, { body: {} });

// and
const alice = {
username: 'alice',
password: 'Secret_1',
type: 'district-manager',
contact: {
c_prop: 'c_val_a',
},
place: {
c_prop: 'p_val_a',
name: 'alice area',
parent: 'abc-123',
type: 'health_center',
},
};
const bob = {
username: 'bob',
password: 'Secret_2',
type: 'district-manager',
contact: {
c_prop: 'c_val_b',
},
place: {
c_prop: 'p_val_b',
name: 'bob area',
parent: 'def-456',
type: 'health_center',
},
};

assertDbEmpty()

.then(() => /* when */ createUsers(testDir, api.couchUrl))

.then(() => assert.deepEqual(
api.requestLog(),
[
{ method:'POST', url:'/api/v1/users', body:alice },
{ method:'POST', url:'/api/v1/users', body:bob },
]))

.then(done)
.catch(done);

});

});

function assertDbEmpty() {
return api.db.allDocs()
.then(res => assert.equal(res.rows.length, 0));
}

0 comments on commit a24b210

Please sign in to comment.