source: Dev/trunk/src/server/app.js @ 481

Last change on this file since 481 was 481, checked in by hendrikvanantwerpen, 11 years ago

Hide database details behind decent API. Client needs to start using it now.

File size: 10.6 KB
Line 
1var express = require("express")
2  , passport = require("passport")
3  , passportLocal = require("passport-local")
4  , fs = require("fs")
5  , path = require("path")
6  , proxy = require("./util/simple-http-proxy")
7  , CSV = require("ya-csv")
8  , CouchDB = require('./util/couch').CouchDB
9  , _ = require("underscore")
10  , tv4 = require("tv4")
11  ;
12
13function assertSetting(name, settings, validate) {
14    if ( typeof settings[name] === 'undefined' ) {
15        throw new Error("Required setting '"+name+"' undefined.");
16    }
17    if ( _.isFunction(validate) && !validate(settings[name]) ) {
18        throw new Error("Setting '"+name+"' with value '"+settings[name]+"' is invalid.");
19    }
20}
21
22exports.App = function(settings) {
23
24    assertSetting("couchDbURL", settings, _.isString);
25    var couch = new CouchDB(settings.couchDbURL);
26   
27    var schema = require("./config/couchdb-schema.json");
28    return couch.get("schemaInfo").then(function(schemaInfo){
29        if (schemaInfo.version !== schema.version) {
30            var msg =  "Database has version "+schemaInfo.version+" but we expect version "+schema.version;
31            throw new Error(msg);
32        }
33        return configureApp(settings,couch,schema);
34    });
35
36};
37
38function configureApp(settings,couch,schema) {
39
40    function clientPath(relativePath) {
41        return path.resolve(__dirname+'/../client/'+relativePath);
42    }
43
44    passport.use(new passportLocal.Strategy(function(username, password, done){
45        if ( username === "igor" && password === "mayer" ) {
46            done(null,{ username: "igor" });
47        } else {
48            done(null,false,{ message: 'Invalid credentials.' });
49        }
50    }));
51    passport.serializeUser(function(user, done) {
52        done(null, user.username);
53    });
54    passport.deserializeUser(function(id, done) {
55        done(null, {username: id});
56    });
57
58    var app = express();
59    app.use(express.logger());
60    app.use(express.compress());
61    app.use(express.favicon());
62
63    // cookies and session
64    app.use(express.cookieParser());
65    app.use(express.session({ secret: "quasi experimental design" }));
66    app.use('/api',express.bodyParser());
67
68    // passport
69    app.use(passport.initialize());
70    app.use(passport.session());
71    function ensureAuthenticated(req,res,next){
72        if (!req.user) {
73            return res.send(401,{error:"Login before accessing API."});
74        } else {
75            return next();
76        }
77    }
78    function returnUser(req,res) {
79        res.send(200, req.user);
80    }
81
82    // static resources
83    app.get('/', function(request, response){
84        response.sendfile(clientPath('index.html'));
85    });
86    app.get('/*.html', function(request, response) {
87        response.sendfile(clientPath(request.path));
88    });
89    _.each(['/dojo', '/dijit', '/dojox', '/qed', '/qed-client'], function(dir){
90        app.use(dir, express.static(clientPath(dir)));
91    });
92
93    // post to this url to login
94    app.post(
95        '/api/login',
96        passport.authenticate('local'),
97        returnUser);
98
99    // return the info for the current logged in user
100    app.get(
101        '/api/login',
102        ensureAuthenticated,
103        returnUser);
104
105    // explicitly logout this user
106    app.post(
107        '/api/logout',
108        ensureAuthenticated,
109        function(req,res){
110            req.logout();
111            res.send(200,{});
112        });
113
114    function makeDocsGet(type) {
115        return function(req,res) {
116            var url = '_design/default/_view/by_type?key='+JSON.stringify(type);
117            couch.get(url).then(function(result){
118                var items = _.map(result.rows, function(item) { return item.value; });
119                res.send(200, items);
120            }, function(error){
121                res.send(404, {error: "Cannot find collection"});
122            });
123        };
124    }
125    function makeDocGet_id(type) {
126        return function(req,res) {
127            var id = req.params.id;
128            couch.get(id).then(function(result){
129                if ( result.type === type ) {
130                    res.send(200, result);
131                } else {
132                    res.send(404, {error: "Document "+id+" is not a "+type});
133                }
134            }, function(error){
135                res.send(404, {error: "Cannot find survey run "+id});
136            });
137        };
138    }
139    function makeDocPut_id(type) {
140        return function(req,res) {
141            var id = req.params.id;
142            var doc = req.body;
143            if ( doc.type === type && tv4.validateResult(doc, schema) ) {
144                couch.put(id,doc).then(function(result){
145                    res.send(200, result);
146                }, function(error){
147                    res.send(500, error);
148                });
149            } else {
150                res.send(400,{error: "Document failed schema verification."});
151            }
152        };
153    }
154    function makeDocDel_id(type) {
155        return function(req,res) {
156            var id = req.params.id;
157            var rev = req.query.rev;
158            couch.delete(id,rev).then(function(result){
159                res.send(200, result);
160            }, function(error){
161                res.send(500, error);
162            });
163        };
164    }
165    function makeDocPost(type) {
166        return function(req,res) {
167            var doc = req.body;
168            if ( doc.type === type && tv4.validateResult(doc, schema) ) {
169                couch.post(req.body).then(function(result){
170                    res.send(200, result);
171                }, function(error){
172                    res.send(500, error);
173                });
174            } else {
175                res.send(400,{error: "Document failed schema verification."});
176            }
177        };
178    }
179    function notImplemented(req,res) {
180        res.send(501,{error:"API not implemented yet."});
181    }
182
183    // Questions
184    app.get('/api/questions',
185        ensureAuthenticated,
186        makeDocsGet('Question'));
187    app.get('/api/questions/:id',
188        ensureAuthenticated,
189        makeDocGet_id('Question'));
190    app.post('/api/questions',
191        ensureAuthenticated,
192        makeDocPost('Question'));
193    app.put('/api/questions/:id',
194        ensureAuthenticated,
195        makeDocPut_id('Question'));
196    app.delete('/api/questions/:id',
197        ensureAuthenticated,
198        makeDocDel_id('Question'));
199    app.get('/api/questions/categories',
200        ensureAuthenticated,
201        notImplemented);
202
203    // Surveys
204    app.get('/api/surveys',
205        ensureAuthenticated,
206        makeDocsGet('Survey'));
207    app.get('/api/surveys/:id',
208        ensureAuthenticated,
209        makeDocGet_id('Survey'));
210    app.post('/api/surveys',
211        ensureAuthenticated,
212        makeDocPost('Survey'));
213    app.put('/api/surveys/:id',
214        ensureAuthenticated,
215        makeDocPut_id('Survey'));
216    app.delete('/api/surveys/:id',
217        ensureAuthenticated,
218        makeDocDel_id('Survey'));
219
220    // SurveyRuns
221    app.get('/api/surveyRuns',
222        ensureAuthenticated,
223        makeDocsGet('SurveyRun'));
224    app.get('/api/surveyRuns/:id',
225        ensureAuthenticated,
226        makeDocGet_id('SurveyRun'));
227    app.get('/api/surveyRuns/:id/responses.csv',
228        ensureAuthenticated,
229        function(req, res) {
230            var id = req.params.id;
231            var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(id);
232            couch.get(url).then(function(result){
233                var responses = _.map(result.rows, function(item) { return item.value; });
234                var flatResponses = responsesToVariables(responses);
235                res.set({
236                    'Content-Type': 'text/csv',
237                    'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
238                });
239                res.status(200);
240                writeObjectsToCSVStream(flatResponses, res);
241                res.end();
242            }, function(error){
243                res.send(404, {error: "Cannot find responses for survey run "+id});
244            });
245        });
246    app.get('/api/surveyRuns/:id/responses',
247        ensureAuthenticated,
248        function(req,res) {
249            var id = req.params.id;
250            var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(id);
251            couch.get(url).then(function(result){
252                var responses = _.map(result.rows, function(item) { return item.value; });
253                res.send(200, responses);
254            }, function(error){
255                res.send(404, {error: "Cannot find responses for survey run "+id});
256            });
257        });
258   
259    // Responses
260    app.get('/api/responses',
261        ensureAuthenticated,
262        makeDocsGet('Response'));
263    app.get('/api/responses/:id',
264        ensureAuthenticated,
265        makeDocGet_id('Response'));
266    app.post('/api/responses',
267        ensureAuthenticated,
268        makeDocPost('Response'));
269    app.put('/api/responses/:id',
270        ensureAuthenticated,
271        makeDocPut_id('Response'));
272    app.delete('/api/responses/:id',
273        ensureAuthenticated,
274        makeDocDel_id('Response'));
275
276    return app;
277
278}
279
280function responsesToVariables(responses) {
281    return _.map(responses, responseToVariables);
282}
283
284function responseToVariables(response) {
285    var result = flattenObject(response.answers);
286    return result;
287}
288
289function flattenObject(value) {
290    var result = {};
291    (function agg(val,key,res){
292        if ( _.isObject(val) ) {
293            var keys = _.keys(val);
294            // FIXME : dirty hack for questions with only one input
295            if ( keys.length === 1 ) {
296                agg(val[keys[0]],key,res);
297            } else {
298                _.forEach(val, function(v,k){
299                    agg(v,(key ? key+'.' : '')+k,res);
300                });
301            }
302        } else if ( _.isArray(val) ) {
303            // FIXME : dirty hack for questions with only one input
304            if ( val.length === 1 ) {
305                agg(val[0],key,res);
306            } else {
307                _.forEach(val, function(v,i){
308                    agg(v,(key ? key+'.' : '')+i,res);
309                });
310            }
311        } else {
312            res[key] = val;
313        }
314    })(value,null,result);
315    return result;
316}
317
318function writeObjectsToCSVStream(objects, stream, prelude) {
319    var keys = _.chain(objects)
320                .map(_.keys)
321                .flatten()
322                .uniq()
323                .value();
324    var idxs = {};
325    _.forEach(keys, function(key,idx){
326        idxs[key] = idx;
327    });
328    var writer = new CSV.CsvWriter(stream);
329    if ( prelude ) {
330        _.forEach(prelude, function(val,key){
331            writer.writeRecord([key,val]);
332        });
333    }
334    writer.writeRecord(keys);
335    _.forEach(objects, function(obj){
336        var row = [];
337        _.forEach(obj, function(val,key){
338            row[idxs[key]] = val;
339        });
340        writer.writeRecord(row);
341    });
342}
Note: See TracBrowser for help on using the repository browser.