Changeset 487 for Dev/trunk/src/server
- Timestamp:
- 03/05/14 22:44:48 (11 years ago)
- Location:
- Dev/trunk/src/server
- Files:
-
- 7 added
- 10 edited
Legend:
- Unmodified
- Added
- Removed
-
Dev/trunk/src/server/app.js
r481 r487 9 9 , _ = require("underscore") 10 10 , tv4 = require("tv4") 11 , HTTPResult = require("./util/http-result") 12 , etags = require("./util/etags") 13 , cryptoken = require('./util/crypto-token') 11 14 ; 12 15 … … 22 25 exports.App = function(settings) { 23 26 24 assertSetting("couchDbURL", settings, _.isString); 25 var couch = new CouchDB(settings.couchDbURL); 27 assertSetting("couchServerURL", settings, _.isString); 28 assertSetting("dbName", settings, _.isString); 29 var couch = new CouchDB(settings.couchServerURL,settings.dbName); 26 30 27 31 var schema = require("./config/couchdb-schema.json"); … … 69 73 app.use(passport.initialize()); 70 74 app.use(passport.session()); 75 76 // various middlewares 71 77 function ensureAuthenticated(req,res,next){ 72 78 if (!req.user) { … … 78 84 function returnUser(req,res) { 79 85 res.send(200, req.user); 86 } 87 function notImplemented(req,res) { 88 res.send(501,{error:"API not implemented yet."}); 80 89 } 81 90 … … 112 121 }); 113 122 123 var JSON_MIME = 'application/json'; 124 var CSV_MIME = 'application/json'; 125 function ensureMIME(mimeType) { 126 return function(req,res,next) { 127 if (!req.accepts(mimeType)) { 128 res.send(406); 129 } else { 130 res.set({ 131 'Content-Type': mimeType 132 }); 133 next(); 134 } 135 }; 136 } 137 138 function stripAndReturnPrivates(obj) { 139 var priv = {}; 140 _.each(obj||{},function(val,key){ 141 if (key.substring(0,1) === '_') { 142 priv[key] = val; 143 delete obj[key]; 144 } 145 }); 146 return priv; 147 } 148 149 function identity(obj) { return obj; } 150 function handleUnknownResponse(status,error){ 151 return new HTTPResult(500,{error: "Unexpected database response", 152 innerStatus: status, innerResponse: error}); 153 } 154 function handleUnknownError(error){ 155 return new HTTPResult(500, {error: "Unknown error", innerError: error}); 156 } 157 function handleRowValues(result) { 158 return _.map(result.rows, function(item) { return item.value; }); 159 } 160 function handleRowDocs(result) { 161 return _.map(result.rows, function(item) { return item.doc; }); 162 } 163 164 function getDocumentsOfType (type) { 165 var url = '_design/default/_view/by_type?key='+JSON.stringify(type); 166 return HTTPResult.fromResponsePromise(couch.get(url).response, 167 handleUnknownError) 168 .handle({ 169 200: handleRowValues, 170 404: function() { return {error: "Cannot find collection of type "+type}; }, 171 default: handleUnknownResponse 172 }); 173 } 174 function getDocument(id,rev,type) { 175 var opts = {headers:{}}; 176 if (rev) { 177 opts.headers['If-Non-Match'] = '"'+rev+'"'; 178 } 179 return HTTPResult.fromResponsePromise(couch.get(id,opts).response, 180 handleUnknownError) 181 .handle({ 182 200: function(doc){ 183 if ( doc.type !== type ) { 184 return new HTTPResult(404,{error:"Document not found."}); 185 } else { 186 var priv = stripAndReturnPrivates(doc); 187 if ( priv._rev !== rev ) { 188 doc._id = priv._id; 189 doc._rev = priv._rev; 190 return doc; 191 } else { 192 return new HTTPResult(304); 193 } 194 } 195 }, 196 304: identity, 197 default: handleUnknownResponse 198 }); 199 } 200 function putDocument(id,rev,type,doc) { 201 var priv = stripAndReturnPrivates(doc); 202 if ( doc.type === type && tv4.validateResult(doc, schema) ) { 203 var opts = rev ? {headers:{'If-Match':'"'+rev+'"'}} : {}; 204 return HTTPResult.fromResponsePromise(couch.put(id,doc,opts).response, 205 handleUnknownError) 206 .handle({ 207 201: function(res){ 208 doc._id = res.id; 209 doc._rev = res.rev; 210 return doc; 211 }, 212 409: function(error) { 213 return {error: error.reason}; 214 }, 215 default: handleUnknownResponse 216 }); 217 } else { 218 return new HTTPResult(400,{error: "Document failed schema verification."}); 219 } 220 } 221 function deleteDocument(id,rev) { 222 if ( rev ) { 223 var opts = {headers:{'If-Match':'"'+rev+'"'}}; 224 return HTTPResult.fromResponsePromise(couch.delete(id,opts).response, 225 handleUnknownError) 226 .handle({ 227 200: identity, 228 409: function(error) { 229 return {error: error.reason}; 230 }, 231 default: handleUnknownResponse 232 }); 233 } else { 234 return new HTTPResult(409, {error: "Cannot identify document revision to delete."}); 235 } 236 } 237 function postDocument(type,doc) { 238 var priv = stripAndReturnPrivates(doc); 239 if ( doc.type === type && tv4.validateResult(doc, schema) ) { 240 return HTTPResult.fromResponsePromise(couch.post(doc).response, 241 handleUnknownError) 242 .handle({ 243 201: function(response) { 244 doc._id = response.id; 245 doc._rev = response.rev; 246 return doc; 247 }, 248 default: handleUnknownResponse 249 }); 250 } else { 251 return new HTTPResult(400,{error: "Document failed schema verification."}); 252 } 253 } 254 114 255 function makeDocsGet(type) { 115 256 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"}); 257 getDocumentsOfType(type) 258 .handle(res.send.bind(res)); 259 }; 260 } 261 function makeDocGet_id(type) { 262 return function(req,res) { 263 var id = req.params.id; 264 var rev = etags.parse(req.header('If-Non-Match'))[0]; 265 getDocument(id,rev,type) 266 .handle({ 267 200: function(doc){ 268 res.set({ 269 'ETag': etags.format([doc._rev]) 270 }).send(200, doc); 271 }, 272 default: res.send.bind(res) 122 273 }); 123 274 }; 124 275 } 125 function makeDoc Get_id(type) {276 function makeDocPut_id(type) { 126 277 return function(req,res) { 127 278 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}); 279 var doc = req.body; 280 var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev); 281 putDocument(id,rev,type,doc) 282 .handle({ 283 201: function(doc) { 284 res.set({ 285 'ETag': etags.format([doc._rev]) 286 }).send(201, doc); 287 }, 288 default: res.send.bind(res) 136 289 }); 137 290 }; 138 291 } 139 function makeDoc Put_id(type) {292 function makeDocDel_id(type) { 140 293 return function(req,res) { 141 294 var id = req.params.id; 142 295 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); 296 var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev); 297 deleteDocument(id,rev) 298 .handle(res.send.bind(res)); 299 }; 300 } 301 function makeDocPost(type) { 302 return function(req,res) { 303 var doc = req.body; 304 postDocument(type,doc) 305 .handle({ 306 201: function(doc) { 307 res.set({ 308 'ETag': etags.format([doc._rev]) 309 }).send(201, doc); 310 }, 311 default: res.send.bind(res) 312 }); 313 }; 314 } 315 316 // Questions 317 function getQuestionsWithCode(code) { 318 var url = '_design/questions/_view/by_code'; 319 var query = {include_docs:true,key:code}; 320 return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response, 321 handleUnknownError) 322 .handle({ 323 200: handleRowDocs, 324 default: handleUnknownResponse 325 }); 326 } 327 function getQuestionsWithTopic(topic) { 328 var url = '_design/questions/_view/all_topics'; 329 var query = {reduce:false,include_docs:true,key:topic}; 330 return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response, 331 handleUnknownError) 332 .handle({ 333 200: handleRowDocs, 334 default: handleUnknownResponse 335 }); 336 } 337 function getQuestionsWithCategoryAndTopic(category,topic) { 338 var hasTopic = typeof topic !== 'undefined'; 339 var url = '_design/questions/_view/all'; 340 var query = {reduce:false,include_docs:true, 341 startkey:hasTopic?[category,topic]:[category], 342 endkey:hasTopic?[category,topic]:[category,{}]}; 343 return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response, 344 handleUnknownError) 345 .handle({ 346 200: handleRowDocs, 347 default: handleUnknownResponse 348 }); 349 } 350 app.get('/api/questions', 351 ensureAuthenticated, 352 ensureMIME(JSON_MIME), 353 function(req,res) { 354 var hr; 355 if ( 'category' in req.query ) { 356 hr = getQuestionsWithCategoryAndTopic(req.query.category,req.query.topic); 357 } else if ( 'topic' in req.query ) { 358 hr = getQuestionsWithTopic(req.query.topic); 359 } else if ( 'code' in req.query ) { 360 hr = getQuestionsWithCode(req.query.code); 361 } 362 hr.handle(res.send.bind(res)); 363 }); 364 app.post('/api/questions', 365 ensureAuthenticated, 366 ensureMIME(JSON_MIME), 367 function (req,res) { 368 var doc = req.body; 369 getQuestionsWithCode(doc.code) 370 .handle({ 371 200: function(others) { 372 if ( others.length > 0 ) { 373 return new HTTPResult(403,{error:"Other question with this code already exists."}); 374 } else { 375 return postDocument('Question',doc); 376 } 377 } 378 }) 379 .handle(res.send.bind(res)); 380 }); 381 app.get('/api/questions/:id', 382 ensureAuthenticated, 383 ensureMIME(JSON_MIME), 384 makeDocGet_id('Question')); 385 app.put('/api/questions/:id', 386 ensureAuthenticated, 387 ensureMIME(JSON_MIME), 388 function (req,res) { 389 var id = req.params.id; 390 var doc = req.body; 391 var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev); 392 getQuestionsWithCode(doc.code) 393 .handle({ 394 200: function(others) { 395 if ( others.length > 0 ) { 396 return new HTTPResult(403,{error:"Other question with this code already exists."}); 397 } else { 398 return putDocument(id,rev,'Question',doc); 399 } 400 } 401 }) 402 .handle(res.send.bind(res)); 403 }); 404 app.delete('/api/questions/:id', 405 ensureAuthenticated, 406 ensureMIME(JSON_MIME), 407 makeDocDel_id('Question')); 408 409 410 // Categories and topics 411 app.get('/api/categories', 412 ensureAuthenticated, 413 ensureMIME(JSON_MIME), 414 function(req,res) { 415 var url = '_design/questions/_view/all'; 416 HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:1}}).response, 417 handleUnknownError) 418 .handle({ 419 200: function(result) { 420 return _.map(result.rows, function(item) { 421 return { name:item.key[0], count:item.value }; 422 }); 423 }, 424 default: handleUnknownResponse 425 }) 426 .handle(res.send.bind(res)); 427 }); 428 app.get('/api/categories/:category/topics', 429 ensureAuthenticated, 430 ensureMIME(JSON_MIME), 431 function(req,res) { 432 var category = req.params.category; 433 var url = '_design/questions/_view/all'; 434 HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:2,startkey:[category],endkey:[category,{}]}}).response, 435 handleUnknownError) 436 .handle({ 437 200: function(result) { 438 return _.map(result.rows, function(item) { return { name:item.key[1], count:item.value }; }); 439 }, 440 default: handleUnknownResponse 441 }) 442 .handle(res.send.bind(res)); 443 }); 444 app.get('/api/topics', 445 ensureAuthenticated, 446 ensureMIME(JSON_MIME), 447 function(req,res) { 448 var url = '_design/questions/_view/all_topics'; 449 HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true}}).response, 450 handleUnknownError) 451 .handle({ 452 200: function(result) { 453 return _.map(result.rows, function(item) { return {name:item.key, count:item.value}; }); 454 }, 455 default: handleUnknownResponse 456 }) 457 .handle(res.send.bind(res)); 458 }); 459 460 // Surveys 461 app.get('/api/surveys', 462 ensureAuthenticated, 463 ensureMIME(JSON_MIME), 464 function(req,res) { 465 var url; 466 if ( 'drafts' in req.query ) { 467 url = '_design/surveys/_view/drafts'; 468 } else if ( 'published' in req.query ) { 469 url = '_design/surveys/_view/published'; 470 } else { 471 url = '_design/default/_view/by_type?key='+JSON.stringify('Survey'); 472 } 473 HTTPResult.fromResponsePromise(couch.get(url).response, 474 handleUnknownError) 475 .handle({ 476 200: function(result) { 477 return _.map(result.rows, function(item) { return item.value; }); 478 }, 479 default: handleUnknownResponse 480 }) 481 .handle(res.send.bind(res)); 482 }); 483 app.post('/api/surveys', 484 ensureAuthenticated, 485 ensureMIME(JSON_MIME), 486 makeDocPost('Survey')); 487 app.get('/api/surveys/:id', 488 ensureAuthenticated, 489 ensureMIME(JSON_MIME), 490 makeDocGet_id('Survey')); 491 app.put('/api/surveys/:id', 492 ensureAuthenticated, 493 ensureMIME(JSON_MIME), 494 makeDocPut_id('Survey')); 495 app.delete('/api/surveys/:id', 496 ensureAuthenticated, 497 ensureMIME(JSON_MIME), 498 makeDocDel_id('Survey')); 499 500 // SurveyRuns 501 app.get('/api/surveyRuns', 502 ensureAuthenticated, 503 ensureMIME(JSON_MIME), 504 makeDocsGet('SurveyRun')); 505 app.post('/api/surveyRuns', 506 ensureAuthenticated, 507 ensureMIME(JSON_MIME), 508 function(req,res) { 509 var doc = req.body; 510 randomToken() 511 .handle({ 512 201: function(token) { 513 doc.secret = token; 514 return postDocument('SurveyRun',doc); 515 } 516 }) 517 .handle(res.send.bind(res)); 518 }); 519 app.get('/api/surveyRuns/:id', 520 ensureAuthenticated, 521 ensureMIME(JSON_MIME), 522 makeDocGet_id('SurveyRun')); 523 app.put('/api/surveyRuns/:id', 524 ensureAuthenticated, 525 ensureMIME(JSON_MIME), 526 function (req,res) { 527 var id = req.params.id; 528 var doc = req.body; 529 var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev); 530 var hr; 531 if ( typeof doc.secret === 'undefined' ) { 532 hr = randomToken() 533 .handle({ 534 201: function(token) { 535 doc.secret = token; 536 return putDocument(id,rev,'SurveyRun',doc); 537 } 148 538 }); 149 539 } else { 150 res.send(400,{error: "Document failed schema verification."});540 hr = putDocument(id,rev,'SurveyRun',doc); 151 541 } 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); 542 hr.handle(res.send.bind(res)); 543 }); 544 app.delete('/api/surveyRuns/:id', 545 ensureAuthenticated, 546 ensureMIME(JSON_MIME), 547 makeDocDel_id('SurveyRun')); 548 app.get('/api/surveyRuns/:id/responses', 549 ensureAuthenticated, 550 ensureMIME(JSON_MIME), 551 function(req,res) { 552 var id = req.params.id; 553 getResponsesBySurveyRunId(id) 554 .handle(res.send.bind(res)); 555 }); 556 app.get('/api/surveyRuns/:id/responses.csv', 557 ensureAuthenticated, 558 ensureMIME(CSV_MIME), 559 function(req, res) { 560 var id = req.params.id; 561 getResponsesBySurveyRunId(id) 562 .handle({ 563 200: function(responses) { 564 var flatResponses = responsesToVariables(responses); 565 res.set({ 566 'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv' 567 }); 568 res.status(200); 569 writeObjectsToCSVStream(flatResponses, res); 570 res.end(); 571 }, 572 default: res.send.bind(res) 162 573 }); 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); 574 }); 575 576 // Responses 577 function getResponsesBySurveyRunId(surveyRunId) { 578 var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(surveyRunId); 579 return HTTPResult.fromResponsePromise(couch.get(url).response, 580 handleUnknownError) 581 .handle({ 582 200: handleRowValues, 583 default: handleUnknownResponse 584 }); 585 } 586 app.get('/api/responses', 587 ensureAuthenticated, 588 ensureMIME(JSON_MIME), 589 function(req,res){ 590 var hr; 591 if ( 'surveyRunId' in req.query ) { 592 hr = getResponsesBySurveyRunId(req.query.surveyRunId); 593 } else { 594 hr = getDocumentsOfType('Response'); 595 } 596 hr.handle(res.send.bind(res)); 597 }); 598 app.get('/api/responses/:id', 599 ensureAuthenticated, 600 ensureMIME(JSON_MIME), 601 makeDocGet_id('Response')); 602 app.post('/api/responses', 603 ensureAuthenticated, 604 ensureMIME(JSON_MIME), 605 function (req,res) { 606 var doc = req.body; 607 randomToken() 608 .handle({ 609 201: function(token) { 610 doc.secret = token; 611 return postDocument('Response',doc); 612 } 613 }) 614 .handle(res.send.bind(res)); 615 }); 616 app.put('/api/responses/:id', 617 ensureAuthenticated, 618 ensureMIME(JSON_MIME), 619 function (req,res) { 620 var id = req.params.id; 621 var doc = req.body; 622 var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev); 623 var hr; 624 if ( typeof doc.secret === 'undefined' ) { 625 hr = randomToken() 626 .handle({ 627 201: function(token) { 628 doc.secret = token; 629 return putDocument(id,rev,'Response',doc); 630 } 173 631 }); 174 632 } else { 175 res.send(400,{error: "Document failed schema verification."});633 hr = putDocument(id,rev,'Response',doc); 176 634 } 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, 635 hr.handle(res.send.bind(res)); 636 }); 637 app.delete('/api/responses/:id', 638 ensureAuthenticated, 639 ensureMIME(JSON_MIME), 640 makeDocDel_id('Response')); 641 642 //respondents api 643 function isSurveyRunRunning(surveyRun) { 644 var now = new Date(); 645 var afterStart = !surveyRun.startDate || now >= new Date(surveyRun.startDate); 646 var beforeEnd = !surveyRun.endDate || now <= new Date(surveyRun.endDate); 647 return afterStart && beforeEnd; 648 } 649 app.get('/api/open/responses/:id', 650 ensureMIME(JSON_MIME), 248 651 function(req,res) { 249 652 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')); 653 var rev = etags.parse(req.header('If-Non-Match'))[0]; 654 var secret = req.query.secret; 655 getDocument(id,rev,'Response') 656 .handle({ 657 200: function(response) { 658 if ( response.secret === secret ) { 659 return getDocument(response.surveyRunId,[],'SurveyRun') 660 .handle({ 661 200: function(surveyRun) { 662 if ( !isSurveyRunRunning(surveyRun) ) { 663 return new HTTPResult(404,{error:"Survey is not running anymore."}); 664 } else { 665 response._surveyRun = surveyRun; 666 return response; 667 } 668 } 669 }); 670 } else { 671 return new HTTPResult(403,{error: "Wrong secret for response"}); 672 } 673 } 674 }) 675 .handle(res.send.bind(res)); 676 }); 677 app.post('/api/open/responses', 678 ensureMIME(JSON_MIME), 679 function(req,res) { 680 var secret = req.query.secret; 681 var response = req.body; 682 delete response._surveyRun; 683 getDocument(response.surveyRunId,[],'SurveyRun') 684 .handle({ 685 200: function(surveyRun) { 686 if ( surveyRun.secret !== secret ) { 687 return new HTTPResult(403,{error:"Wrong secret for surveyRun."}); 688 } else if ( !isSurveyRunRunning(surveyRun) ) { 689 return new HTTPResult(404,{error:"Survey is not running anymore."}); 690 } else if ( surveyRun.mode === 'closed' ) { 691 return new HTTPResult(403,{error:"Survey is closed and doesn't allow new responses."}); 692 } else { 693 return randomToken() 694 .handle({ 695 201: function(token) { 696 response.secret = token; 697 return postDocument('Response',response) 698 .handle({ 699 201: function(doc){ 700 doc._surveyRun = surveyRun; 701 return doc; 702 } 703 }); 704 } 705 }); 706 } 707 } 708 }) 709 .handle(res.send.bind(res)); 710 }); 711 app.put('/api/open/responses/:id', 712 ensureMIME(JSON_MIME), 713 function(req,res){ 714 var id = req.params.id; 715 var doc = req.body; 716 var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev); 717 var secret = req.query.secret || doc.secret; 718 delete doc._surveyRun; 719 getDocument(id,[],'Response') 720 .handle({ 721 200: function(prev) { 722 if ( prev.secret !== secret ) { 723 return new HTTPResult(403,{error: "Secrets are not the same."}); 724 } else if ( prev.surveyRunId !== doc.surveyRunId ) { 725 return new HTTPResult(400,{error:"Response cannot change it's surveyRunId."}); 726 } else { 727 doc.secret = secret; // restore in case it got lost or was changed 728 return getDocument(doc.surveyRunId,[],'SurveyRun') 729 .handle({ 730 200: function(surveyRun) { 731 if ( !isSurveyRunRunning(surveyRun) ) { 732 return new HTTPResult(404,{error:"Survey is not running anymore."}); 733 } else { 734 return putDocument(id,rev,'Response',doc) 735 .handle({ 736 200: function(doc) { 737 doc._surveyRun = surveyRun; 738 return new HTTPResult(201,doc); 739 } 740 }); 741 } 742 } 743 }); 744 } 745 } 746 }) 747 .handle(res.send.bind(res)); 748 }); 749 app.delete('/api/open/responses/:id', 750 ensureMIME(JSON_MIME), 751 function(req,res){ 752 var id = req.params.id; 753 var doc = req.body; 754 var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev); 755 var secret = req.query.secret || doc.secret; 756 delete doc._surveyRun; 757 getDocument(id,[],'Response') 758 .handle({ 759 200: function(prev) { 760 if ( prev.secret !== secret ) { 761 return new HTTPResult(403,{error: "Secrets are not the same."}); 762 } else { 763 return deleteDocument(id,rev,doc); 764 } 765 } 766 }) 767 .handle(res.send.bind(res)); 768 }); 769 770 // uuids 771 app.get('/api/uuids', 772 ensureAuthenticated, 773 ensureMIME(JSON_MIME), 774 function(req,res){ 775 var count = (req.query.count && parseInt(req.query.count,10)) || 1; 776 HTTPResult.fromResponsePromise(couch.uuids({query:{count:count}}).response, 777 handleUnknownError) 778 .handle({ 779 200: function(res) { 780 return res.uuids; 781 }, 782 default: handleUnknownResponse 783 }) 784 .handle(res.send.bind(res)); 785 }); 275 786 276 787 return app; 277 278 788 } 279 789 … … 341 851 }); 342 852 } 853 854 function randomToken() { 855 var result = new HTTPResult(); 856 cryptoken(8) 857 .then(function(token){ 858 result.set(201,token); 859 }, function(ex){ 860 result.set(500,{error:"Cannot generate secrets.", innerError: ex}); 861 }); 862 return result; 863 } -
Dev/trunk/src/server/bin/check-db.js
r479 r487 1 1 var env = require('../env') 2 , checkCouch = require('../config/check-couchdb'); 3 checkCouch(env.couchDbURL) 2 , checkCouch = require('../config/check-couchdb') 3 ; 4 checkCouch(env.couchServerURL,env.dbName) 4 5 .then(function(res){ 5 6 console.log("done",res); -
Dev/trunk/src/server/bin/config-db.js
r479 r487 1 1 var env = require('../env') 2 , checkCouch = require('../config/config-couchdb'); 3 checkCouch(env.couchServerURL) 2 , configCouch = require('../config/config-couchdb') 3 , designDocs = require('../config/couchdb-design-docs') 4 ; 5 configCouch(env.couchServerURL,env.dbName,designDocs) 4 6 .then(function(res){ 5 7 console.log("done",res); -
Dev/trunk/src/server/bin/heroku.js
r481 r487 1 var env = require('../env'); 2 var configCouch = require('../config/config-couchdb'); 1 var env = require('../env') 2 , configCouch = require('../config/config-couchdb') 3 , designDocs = require('../config/config-couchdb') 4 ; 3 5 4 6 console.log("Running on",env.couchServerURL); 5 7 6 8 require('../app').App({ 7 couchDbURL: env.couchDbURL 9 couchServerURL: env.couchServerURL, 10 dbName: env.dbName 8 11 }).then(function(app){ 9 configCouch(env.couchServerURL );12 configCouch(env.couchServerURL,env.dbName,designDocs); 10 13 return app; 11 14 }).then(function(app){ -
Dev/trunk/src/server/config/check-couchdb.js
r479 r487 7 7 var schema = require('./couchdb-schema'); 8 8 9 module.exports = function(couch DbURL) {10 var server = new CouchDB(couch DbURL);9 module.exports = function(couchServerURL,dbName) { 10 var server = new CouchDB(couchServerURL,dbName); 11 11 var designRe = /^_design\//; 12 12 return server.get('/_all_docs') -
Dev/trunk/src/server/config/config-couchdb.js
r479 r487 5 5 ; 6 6 7 var designDocs = require('./couchdb-design-docs'); 7 module.exports = function(couchServerURL, dbName, designDocs) { 8 var server = new CouchDB(couchServerURL); 8 9 9 module.exports = function(couchServerURL) { 10 var server = new CouchDB(couchServerURL); 10 if ( !designDocs ) { throw new Error("Forgot to pass design docs to checkCouch."); } 11 11 12 12 console.log("Configuring CouchDB for QED"); … … 14 14 return server.get('') 15 15 .then(function(info){ 16 if (info.version !== "1. 2.0" ) {16 if (info.version !== "1.4.0" ) { 17 17 console.log("Found "+info.version+", only tested with CouchDB 1.2.0"); 18 18 } else { 19 console.log("CouchDB 1. 2.0 found");19 console.log("CouchDB 1.4.0 found"); 20 20 } 21 21 }).then(function(res){ -
Dev/trunk/src/server/config/couchdb-design-docs.js
r480 r487 11 11 12 12 "qed/schemaInfo": { 13 version: " 1"13 version: "2" 14 14 }, 15 15 … … 31 31 language: "javascript", 32 32 validate_doc_update: function(newDoc, oldDoc, userCtx, secObj) { 33 if ( oldDoc && oldDoc.publicationDate ) { throw({forbidden:'Published documents cannot be modified.'}); } 33 if ( oldDoc && oldDoc.publicationDate ) { 34 throw({forbidden:'Published documents cannot be modified.'}); 35 } 34 36 }, 35 37 views: { … … 59 61 if ( doc.categories && doc.categories.length > 0 ) { 60 62 for ( var i = 0; i < doc.categories.length; i++ ) { 61 emit([doc.categories[i],doc.topic|| null],1);63 emit([doc.categories[i],doc.topic||"(default)"],1); 62 64 } 63 65 } else { 64 emit([ null,null],1);66 emit(["(default)","(default)"],1); 65 67 } 66 68 }, … … 72 74 if ( doc.categories && doc.categories.length > 0 ) { 73 75 for ( var i = 0; i < doc.categories.length; i++ ) { 74 emit([doc.categories[i],doc.topic|| null],1);76 emit([doc.categories[i],doc.topic||"(default)"],1); 75 77 } 76 78 } else { 77 emit([ null,null],1);79 emit(["(default)","(default)"],1); 78 80 } 79 81 }, … … 83 85 map: function(doc){ 84 86 if( doc.type !== 'Question' ){ return; } 85 emit(doc.topic );87 emit(doc.topic||"(default)",1); 86 88 }, 87 reduce: function(key, values, rereduce) { return null; }89 reduce: function(key, values, rereduce) { return sum(values); } 88 90 }, 89 91 published_topics: { 90 92 map: function(doc){ 91 93 if ( doc.type !== 'Question' || !doc.publicationDate ) { return; } 92 emit(doc.topic );94 emit(doc.topic||"(default)",1); 93 95 }, 94 reduce: function(key, values, rereduce) { return null; } 96 reduce: function(key, values, rereduce) { return sum(values); } 97 }, 98 by_code: { 99 map: function(doc){ 100 if ( doc.type !== 'Question' ) { return; } 101 emit(doc.code,doc); 102 } 95 103 } 96 104 } -
Dev/trunk/src/server/config/couchdb-schema.json
r481 r487 1 1 { 2 "$schema": "http://json-schema.org/draft-04/schema#", 2 3 "title": "QED Object Schema", 3 "version": " 1",4 "version": "2", 4 5 "type": "object", 5 6 "oneOf": [ 6 { "$ref": "#/ schemaInfo" },7 { "$ref": "#/d ocTypes/any" }7 { "$ref": "#/definitions/schemaInfo" }, 8 { "$ref": "#/definitions/docs/any" } 8 9 ], 9 "schemaInfo": { 10 "type": "object", 11 "properties": { 12 "_id": { "type": "string", "pattern": "schemaInfo" }, 13 "_rev": { "type": "string", "optional": true }, 14 "version": { "type": "string" } 15 }, 16 "additionalProperties": false 17 }, 18 "docTypes": { 19 "any": { 10 "definitions": { 11 "schemaInfo": { 20 12 "type": "object", 21 "oneOf": [22 { "$ref": "#/docTypes/Question" },23 { "$ref": "#/docTypes/Survey" },24 { "$ref": "#/docTypes/SurveyRun" },25 { "$ref": "#/docTypes/Response" }26 ]27 },28 "Question": {29 13 "properties": { 30 "type": { "type": "string", "pattern": "Question" }, 31 "_id": { "type": "string", "optional": true }, 32 "_rev": { "type": "string", "optional": true }, 33 "categories": { "type": "array", "items": { "type": "string" } }, 34 "code": { "type": "string" }, 35 "content": { "type": "array", "items": { "$ref": "#/contentTypes/any" } }, 36 "description": { "type": "string", "optional": true }, 37 "publicationDate": { "type": "string", "pattern": "", "optional": true }, 38 "title": { "type": "string" }, 39 "topic": { "type": "string", "optional": true } 14 "_id": { "type": "string", "pattern": "schemaInfo" }, 15 "_rev": { "type": "string" }, 16 "version": { "type": "string" } 40 17 }, 18 "required": ["_id","version"], 41 19 "additionalProperties": false 42 20 }, 43 " Survey": {44 " type": "object",45 "properties": {46 " type": { "type": "string", "pattern": "Survey" },47 "_id": { "type": "string", "optional": true},48 "_rev": { "type": "string", "optional": true},49 "publicationDate": { "type": "string", "pattern": "", "optional": true},50 "questions": { "type": "array", "items": { "$ref": "#/docTypes/Question" } },51 "title": { "type": "string" }21 "docs": { 22 "any": { 23 "type": "object", 24 "oneOf":[ 25 { "$ref": "#/definitions/docs/Question" }, 26 { "$ref": "#/definitions/docs/Survey" }, 27 { "$ref": "#/definitions/docs/SurveyRun" }, 28 { "$ref": "#/definitions/docs/Response" } 29 ] 52 30 }, 53 "additionalProperties": false 54 }, 55 "SurveyRun": { 56 "type": "object", 57 "properties": { 58 "type": { "type": "string", "pattern": "SurveyRun" }, 59 "_id": { "type": "string", "optional": true }, 60 "_rev": { "type": "string", "optional": true }, 61 "description": { "type": "string" }, 62 "endDate": { "type": "string", "pattern": "" }, 63 "mode": { "type": "string", "enum": [ "open", "closed" ] }, 64 "startDate": { "type": "string", "pattern": "" }, 65 "survey": { "$ref": "#/docTypes/Survey" }, 66 "title": { "type": "string" } 67 }, 68 "additionalProperties": false 69 }, 70 "Response": { 71 "type": "object", 72 "properties": { 73 "type": { "type": "string", "pattern": "Response" }, 74 "_id": { "type": "string", "optional": true }, 75 "_rev": { "type": "string", "optional": true }, 76 "answers": { "type": "object" }, 77 "publicationDate": { "type": "string", "pattern": "", "optional": true }, 78 "surveyRunId": { "type": "string" } 79 }, 80 "additionalProperties": false 81 } 82 }, 83 "contentTypes": { 84 "any": { 85 "type": "object", 86 "oneOf": [ 87 { "$ref": "#/contentTypes/Text" }, 88 { "$ref": "#/contentTypes/StringInput" }, 89 { "$ref": "#/contentTypes/ScaleInput" } 90 ] 91 }, 92 "Text": { 31 "Question": { 93 32 "type": "object", 94 33 "properties": { 95 "type": { "type": "string", "pattern": "Text" }, 96 "text": { "type": "string" } 34 "type": { "type": "string", "pattern": "Question" }, 35 "_id": { "type": "string" }, 36 "_rev": { "type": "string" }, 37 "categories": { "type": "array", "items": { "type": "string" } }, 38 "code": { "type": "string" }, 39 "content": { "type": "array", "items": { "$ref": "#/definitions/content/any" } }, 40 "description": { "type": "string" }, 41 "publicationDate": { "type": "string", "format": "datetime" }, 42 "title": { "type": "string" }, 43 "topic": { "type": "string" } 97 44 }, 45 "required": ["type","categories","code","content","title"], 98 46 "additionalProperties": false 99 },100 "StringInput": {47 }, 48 "Survey": { 101 49 "type": "object", 102 50 "properties": { 103 "type": { "type": "string", "pattern": "StringInput" }, 104 "text": { "type": "string" } 51 "type": { "type": "string", "pattern": "Survey" }, 52 "_id": { "type": "string" }, 53 "_rev": { "type": "string" }, 54 "publicationDate": { "type": "string", "format": "datetime" }, 55 "questions": { "type": "array", "items": { "$ref": "#/definitions/docs/Question" } }, 56 "title": { "type": "string" } 105 57 }, 58 "required": ["type","questions","title"], 106 59 "additionalProperties": false 107 },108 "ScaleInput": {60 }, 61 "SurveyRun": { 109 62 "type": "object", 110 63 "properties": { 111 "type": { "type": "string", "pattern": "ScaleInput" }, 112 "minLabel": { "type": "string", "optional": true }, 113 "min": { "type": "integer" }, 114 "max": { "type": "integer" }, 115 "maxLabel": { "type": "string", "optional": true }, 116 "naLabel": { "type": "string", "optional": true }, 117 "items": { "type": "array", "items": { 118 "type": "object", 119 "properties": { 120 "text": { "type": "string" }, 121 "minLabel": { "type": "string", "optional": true }, 122 "maxLabel": { "type": "string", "optional": true } 123 }, 124 "additionalProperties": false 125 } } 64 "type": { "type": "string", "pattern": "SurveyRun" }, 65 "_id": { "type": "string" }, 66 "_rev": { "type": "string" }, 67 "description": { "type": "string" }, 68 "endDate": { "type": "string", "format": "datetime" }, 69 "mode": { "type": "string", "enum": [ "open", "closed" ] }, 70 "secret": { "type": "string", "minLength": 8 }, 71 "startDate": { "type": "string", "format": "datetime" }, 72 "survey": { "$ref": "#/definitions/docs/Survey" }, 73 "title": { "type": "string" } 126 74 }, 75 "required": ["type","description","mode","secret","survey","title"], 127 76 "additionalProperties": false 77 }, 78 "Response": { 79 "type": "object", 80 "properties": { 81 "type": { "type": "string", "pattern": "Response" }, 82 "_id": { "type": "string" }, 83 "_rev": { "type": "string" }, 84 "answers": { "type": "object" }, 85 "publicationDate": { "type": "string", "format": "datetime" }, 86 "secret": { "type": "string", "minLength": 8 }, 87 "surveyRunId": { "type": "string" } 88 }, 89 "required": ["type","answers","secret","surveyRunId"], 90 "additionalProperties": false 91 } 92 }, 93 "content":{ 94 "any": { 95 "type": "object", 96 "oneOf": [ 97 { "$ref": "#/definitions/content/Text" }, 98 { "$ref": "#/definitions/content/StringInput" }, 99 { "$ref": "#/definitions/content/ScaleInput" } 100 ] 101 }, 102 "Text": { 103 "type": "object", 104 "properties": { 105 "type": { "type": "string", "pattern": "Text" }, 106 "text": { "type": "string" } 107 }, 108 "required": ["type","text"], 109 "additionalProperties": false 110 }, 111 "StringInput": { 112 "type": "object", 113 "properties": { 114 "type": { "type": "string", "pattern": "StringInput" }, 115 "text": { "type": "string" } 116 }, 117 "required":["type","text"], 118 "additionalProperties": false 119 }, 120 "ScaleInput": { 121 "type": "object", 122 "properties": { 123 "type": { "type": "string", "pattern": "ScaleInput" }, 124 "minLabel": { "type": "string" }, 125 "min": { "type": "integer" }, 126 "max": { "type": "integer" }, 127 "maxLabel": { "type": "string" }, 128 "naLabel": { "type": "string" }, 129 "items": { "type": "array", "items": { 130 "type": "object", 131 "properties": { 132 "text": { "type": "string" }, 133 "minLabel": { "type": "string" }, 134 "maxLabel": { "type": "string" } 135 }, 136 "required":["text"], 137 "additionalProperties": false 138 } } 139 }, 140 "required":["type","min","max","items"], 141 "additionalProperties": false 142 } 128 143 } 129 144 } -
Dev/trunk/src/server/env.js
r479 r487 3 3 port: process.env.PORT || 5000, 4 4 couchServerURL: couchServerURL, 5 couchDbURL: couchServerURL+'qed',5 dbName: 'qed' 6 6 }; -
Dev/trunk/src/server/util/couch.coffee
r479 r487 6 6 7 7 class CouchDB 8 constructor: (url) -> 9 @url = normalizeURL url 10 get: (id) -> 8 constructor: (url,db) -> 9 @serverURL = normalizeURL url 10 @db = db 11 if @db 12 @url = "#{@serverURL}/#{@db}" 13 else 14 @url = "#{@serverURL}" 15 get: (id, opts) -> 11 16 url = "#{@url}/#{id}" 12 couchRequest 'GET', url 13 post: (doc ) ->14 url = "#{@url} "15 couchRequest 'POST', url, doc 16 put: (id, doc ) ->17 couchRequest 'GET', url, null, opts 18 post: (doc, opts) -> 19 url = "#{@url}/" 20 couchRequest 'POST', url, doc, opts 21 put: (id, doc, opts) -> 17 22 url = "#{@url}/#{id}" 18 couchRequest 'PUT', url, doc 19 delete: (id, rev) -> 20 url = "#{@url}/#{id}?rev=#{rev}" 21 couchRequest 'DELETE', url 23 couchRequest 'PUT', url, doc, opts 24 delete: (id, opts) -> 25 url = "#{@url}/#{id}" 26 couchRequest 'DELETE', url, null, opts 27 uuids: (opts) -> 28 url = "#{@serverURL}/_uuids" 29 couchRequest 'GET', url, null, opts 22 30 23 31 normalizeURL = (url) -> 24 32 url.replace /\/$/, '' 25 33 26 couchRequest = (method, url, data) ->34 couchRequest = (method, url, body, opts) -> 27 35 options = 28 36 method: method, 29 37 headers: 30 'content-type': 'application/json; charset=utf-8' 31 'accept': 'application/json' 32 body: JSON.stringify (stringifyFunctions (data || {})) 38 'Content-Type': 'application/json; charset=utf-8' 39 'Accept': 'application/json' 40 body: JSON.stringify (stringifyFunctions (body || {})) 41 if opts 42 options.qs = createQueryObj opts.query if opts.query 43 _.extend options.headers, opts.headers if opts.headers 33 44 req = request(url, options) 34 req.response 35 .then (res) => 45 res = req.response.then (res) => 36 46 req.then (res) => 37 47 JSON.parse res … … 40 50 , (err) => 41 51 Q.reject err 52 res.response = req.response.then (res) => 53 statusCode: res.statusCode 54 headers: res.headers 55 body: JSON.parse res.body 56 , (err) => 57 Q.reject err 58 res 42 59 60 createQueryObj = (obj) -> 61 newObj = {} 62 _.each obj, (val,key) -> 63 newObj[key] = JSON.stringify val 64 newObj 65 43 66 stringifyFunctions = (value) -> 44 67 if value is null
Note: See TracChangeset
for help on using the changeset viewer.