var collection_json = require('collection_json') , pg = require('pg').native , util = require('util') , EmployeeDao = require('../lib/dao.js') , _ = require('underscore'); var database_url; module.exports.setup = function(options) { database_url = options.database_url; } function mapRow(row) { var data = _.map(row, function(value, key) { return {name: key, value: value}; }); return { data: data }; } function mapDepartments(res, departments) { return _.map(departments, function(row) { var item = mapRow(row); item.href = res.urlgenerator.department(row.dept_no); return item; }); } function mapEmployees(res, employees) { return _.map(employees, function(row) { var item = mapRow(row); item.href = res.urlgenerator.employee(row.emp_no); return item; }); } function pager(req) { var pageSize = 5; var offset = parseInt(req.query.offset) || 0; var limit = parseInt(req.query.limit) || pageSize; var prevOffset = offset - pageSize; var nextOffset = offset + pageSize; var self = {}; self.pageSize = pageSize; self.offset = offset; self.limit = limit; self.withCount = function(count) { self.pageCount = Math.round(count / pageSize); self.count = count; self.page = offset / pageSize + 1; self.prevOffset = prevOffset >= 0 ? prevOffset : undefined; self.nextOffset = nextOffset < count ? nextOffset : undefined; return self; } return self; } function connect(res, cb) { pg.connect(database_url, function(err, client) { if(err) { res.send('Unable to connect to PostgreSQL.', {'Content-Type': 'text/plain'}, 500); } else { cb(new EmployeeDao(client)); } }); } function after(res, callback) { return function() { var err = arguments[0]; if(err) { // Ideally we would be able to differenciate between // client and server errors, but I'm lazy. It might be // possible to map some values from // http://www.postgresql.org/docs/9.0/static/errcodes-appendix.html // - trygvis var body = JSON.stringify({ collection: { error: { title: 'Unable to insert employee.', message: err.message, code: err.code } }}); var headers = { 'Content-Type' : 'application/vnd.collection+json', 'Content-Length' : body.length } res.writeHead(400, headers); return res.end(body); } callback.apply(this, _.rest(arguments)); } } function method(handlers) { return function(req, res) { // I'm just too lazy to include two rows handlers.HEAD = handlers.GET; var handler = handlers[req.method]; if(handler) { return handler(req, res) } res.header('Allow', _.keys(handlers)); return res.send(405); } } function send_text(res, status, text) { res.writeHead(status, { 'Content-Type': 'text/plain', 'Content-Length': text.length }); res.write(text); res.write('\n'); res.end(); } function send_html(req, res, render) { if(req.method == 'HEAD') { // Can't be bothered to calculate Content-Length even if I // should.. res.writeHead(200, { 'Content-Type': 'text/html', }); res.end(); } else { render(); } } function send_cj(status, req, res, c) { var text = JSON.stringify(collection_json.fromObject(c)); var headers = { 'Content-Type': 'application/vnd.collection+json', 'Content-Length': text.length }; res.writeHead(status, headers); if(req.method != 'HEAD') { res.write(text); } res.end(); } function linksFromPager(req, p, urlgenerator) { var links = []; if(_.isNumber(p.prevOffset)) { links.push({ href: urlgenerator(_.extend({}, req.query, {offset: p.prevOffset})), rel: 'prev', prompt: 'Previous page (' + (p.page - 1) + ' of ' + p.pageCount + ')' }); } if(_.isNumber(p.nextOffset)) { links.push({ href: urlgenerator(_.extend({}, req.query, {offset: p.nextOffset})), rel: 'next', prompt: 'Next page (' + (p.page + 1) + ' of ' + p.pageCount + ')' }); } return links; } function parseEmpNo(req, res, f) { var emp_no = parseInt(req.params.emp_no) || 0; if(emp_no < 10000 || emp_no > 999999) { return send_text(res, 404, 'Illegal emp_no.'); } return f(emp_no); } function getIndex(req, res) { switch(req.accept.types.getBestMatch(['text/html', 'application/vnd.collection+json'])) { case 'text/html': send_html(req, res, _.bind(res.render, res, 'index', { title: 'Employee DB', urlgenerator: res.urlgenerator })); break; case 'application/vnd.collection+json': case '*/*': var c = {collection: { links: [ { rel: 'departments', prompt: 'Department List', href: res.urlgenerator.departments() }, { rel: 'employees', prompt: 'Employee List', href: res.urlgenerator.employees() } ] }}; send_cj(200, req, res, c); break; default: res.send(406); } }; exports.index = method({ GET: getIndex }); function getDepartments(req, res) { connect(res, function(dao) { var p = pager(req); dao.getDepartments(p, after(res, function(departments, count) { p.withCount(count); switch(req.accept.types.getBestMatch(['text/html', 'application/vnd.collection+json'])) { case 'text/html': send_html(req, res, _.bind(res.render, res, 'departments', { title: 'Department List', urlgenerator: res.urlgenerator, pager: p, query: req.query, departments: departments })); break; case 'application/vnd.collection+json': case '*/*': var c = {collection: { href: res.urlgenerator.departments(), items: mapDepartments(res, departments), links: linksFromPager(req, p, res.urlgenerator.departments), }}; send_cj(200, req, res, c); break; default: res.send(406); } })); }); }; exports.departments = method({ GET: getDepartments }); function getDepartment(req, res) { var dept_no = req.params.dept_no; switch(req.accept.types.getBestMatch(['text/html', 'application/vnd.collection+json'])) { case 'text/html': send_html(req, res, _.bind(res.render, res, 'department', { title: 'Department ' + dept_no, urlgenerator: res.urlgenerator, dept_no: dept_no })); break; case 'application/vnd.collection+json': case '*/*': var c = {collection: { href: res.urlgenerator.department(dept_no), links: [ { rel: 'employees', prompt: 'Employees in department ' + dept_no, href: res.urlgenerator.employeesInDepartment(dept_no) },{ rel: 'departments', prompt: 'All departments', href: res.urlgenerator.departments() } ] }}; send_cj(200, req, res, c); break; default: res.send(406); } } exports.department = method({ GET: getDepartment }); function getEmployeesInDepartment(req, res) { connect(res, function(dao) { var dept_no = req.params.dept_no; // TODO: Add dept_name to view // TODO: Add manager as a link var p = pager(req); dao.getEmployeesInDepartment(dept_no, p, after(res, function(employees, count) { p.withCount(count); switch(req.accept.types.getBestMatch(['text/html', 'application/vnd.collection+json'])) { case 'text/html': send_html(req, res, _.bind(res.render, res, 'employeesInDepartment', { title: 'Department: #' + dept_no, urlgenerator: res.urlgenerator, pager: p, query: req.query, dept_no: dept_no, employees: employees })); break; case 'application/vnd.collection+json': case '*/*': var c = {collection: { href: res.urlgenerator.employeesInDepartment(dept_no), links: _.flatten([{ rel: 'department', prompt: 'Department: #' + dept_no, href: res.urlgenerator.department(dept_no) }, linksFromPager(req, p, res.urlgenerator.employees)]), items: mapEmployees(res, employees) }}; send_cj(200, req, res, c); break; default: res.send(406); } })); }); }; exports.employeesInDepartment = method({ GET: getEmployeesInDepartment }); function getEmployees(req, res) { connect(res, function(dao) { // TODO: Support query by emp_no. var emp_no = req.params.emp_no; var qname = req.query.name; var p = pager(req); dao.getEmployeesByName(qname, p, after(res, function(employees, count) { p.withCount(count); switch(req.accept.types.getBestMatch(['text/html', 'application/vnd.collection+json'])) { case 'text/html': send_html(req, res, _.bind(res.render, res, 'employees', { title: 'Employee List', urlgenerator: res.urlgenerator, pager: p, query: req.query, employees: employees, query: req.query })); break; case 'application/vnd.collection+json': case '*/*': var c = {collection: { href: res.urlgenerator.employees(req.query), links: linksFromPager(req, p, res.urlgenerator.employees), queries: [{ href: res.urlgenerator.employees(), rel: 'search', name: 'employee-search', prompt: 'Employee search', data: [ { name: 'name', prompt: 'Name' } ] }], template: { data: [ { name: 'birth_date', prompt: 'Birth Date (YYYY-MM-DD)' }, { name: 'first_name', prompt: 'First Name' }, { name: 'last_name', prompt: 'Last Name' }, { name: 'gender', prompt: 'Gender (M/F)' }, { name: 'hire_date', prompt: 'Hire Date (YYYY-MM-DD). Defaults to today.' } ] }, items: mapEmployees(res, employees) }}; send_cj(200, req, res, c); break; default: res.send(406); } })); }); }; function parseBody(cb) { return function(req, res) { switch(req.headers['content-type']) { case 'application/vnd.collection+json': var s = ''; req.setEncoding('utf8'); req.on('data', function(string) { s += string; }).on('end', function() { try { cb(req, res, collection_json.fromObject(JSON.parse(s))); } catch(e) { send_text(res, 400, 'Unable to parse JSON: ' + e); } }); break; default: res.send(415, 'Sending "application/vnd.collection+json" is required.'); break; } } } var postEmployees = parseBody(function(req, res, body) { var data = body.template.toObject(); connect(res, function(dao) { var now = new Date(); var hire_date = data.hire_date; if(typeof hire_date !== 'string' || hire_date.trim().length == 0) { hire_date = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate(); } dao.insertEmployee(data.birth_date, data.first_name, data.last_name, data.gender, hire_date, after(res, function(emp_no) { res.writeHead(201, {'Location' : res.urlgenerator.employee(emp_no)}); res.end(); })); }); }); exports.employees = method({ GET: getEmployees, POST: postEmployees }); function send_employee_cj(req, res, employee) { var c; if(typeof employee == 'object') { c = {collection: { href: res.urlgenerator.employee(employee.emp_no), items: mapEmployees(res, [ employee ]) }}; } else { c = {collection: { error: { title: 'No employee with emp_no=' + emp_no + '.' } }}; } send_cj(typeof employee == 'object' ? 200 : 404, req, res, c); } function getEmployee(req, res) { connect(res, function(dao) { var emp_no = req.params.emp_no; dao.getEmployee(emp_no, after(res, function(employee) { switch(req.accept.types.getBestMatch(['text/html', 'application/vnd.collection+json', '*/*'])) { case 'text/html': if(typeof employee == 'object') { send_html(req, res, _.bind(res.render, res, 'employee', { title: 'Employee: #' + emp_no, urlgenerator: res.urlgenerator, employee: employee })); } else { send_text(res, 404, 'No employee with emp_no=' + emp_no + '.'); } break; case 'application/vnd.collection+json': case '*/*': send_employee_cj(req, res, employee); break; default: res.send(406); } })) }); } function deleteEmployee(req, res) { connect(res, function(dao) { parseEmpNo(req, res, function(emp_no) { dao.deleteEmployee(emp_no, after(res, function(rowCount) { if(rowCount == 0) { return send_text(res, 404, 'No employee with emp_no=' + emp_no + '.'); } res.send(204); })); }); }); } var postEmployee = parseBody(function(req, res, body) { connect(res, function(dao) { parseEmpNo(req, res, function(emp_no) { if(!body.isTemplate()) { return send_text(res, 400, 'You have to send a proper template. See http://amundsen.com/media-types/collection/format/, section 1.1.4'); } var o = body.template.toObject(); delete o.emp_no; dao.updateEmployee(emp_no, o, after(res, function(rowCount) { console.log('after updateEmployee'); console.log(arguments); if(rowCount == 0) { return send_text(res, 404, 'No employee with emp_no=' + emp_no + '.'); } dao.getEmployee(emp_no, after(res, function(employee) { send_employee_cj(req, res, employee); })); })); }); }); }); exports.employee = method({ GET: getEmployee, POST: postEmployee, DELETE: deleteEmployee });