diff --git a/app/index.js b/app/index.js index d71500b..67ae134 100644 --- a/app/index.js +++ b/app/index.js @@ -22,6 +22,7 @@ server.use(restify.bodyParser({mapParams: false, rejectUnknown: true})); server.use(middleware.verifyRequest()) server.use(middleware.attachResolvePath()) server.use(middleware.attachErrorLogger()) +server.use(middleware.attachPageData()) applyRoutes(server); diff --git a/app/lib/middleware.js b/app/lib/middleware.js index 39b7555..3f562f6 100644 --- a/app/lib/middleware.js +++ b/app/lib/middleware.js @@ -10,6 +10,7 @@ module.exports = { verifyRequest: verifyRequest, attachResolvePath: attachResolvePath, attachErrorLogger: attachErrorLogger, + attachPageData: attachPageData, } const jws = require('jws') @@ -107,6 +108,19 @@ function attachErrorLogger() { } } +function attachPageData() { + return function (req, res, next) { + const page = parseInt(req.query.page, 10) + const count = parseInt(req.query.count, 10) + + if (page > 0 && count > 0) { + req.pageData = { page: page, count: count } + } + + return next() + } +} + function verifyRequest() { if (process.env.NODE_ENV == 'test') { log.warn('In test environment, bypassing request verification') diff --git a/app/lib/send-paginated.js b/app/lib/send-paginated.js new file mode 100644 index 0000000..caa09f5 --- /dev/null +++ b/app/lib/send-paginated.js @@ -0,0 +1,11 @@ +module.exports = function sendPaginated(req, res, responseData, total) { + if (req.pageData) { + responseData.pageData = { + page: req.pageData.page, + count: req.pageData.count, + total: total + }; + } + + return res.send(200, responseData); +} diff --git a/app/routes/applications.js b/app/routes/applications.js index 8336d78..3229d81 100644 --- a/app/routes/applications.js +++ b/app/routes/applications.js @@ -5,6 +5,7 @@ const Badges = require('../models/badge'); const errorHelper = require('../lib/error-helper') const middleware = require('../lib/middleware') const hash = require('../lib/hash').hash +const sendPaginated = require('../lib/send-paginated'); const dbErrorHandler = errorHelper.makeDbHandler('application') @@ -55,13 +56,25 @@ exports = module.exports = function applyApplicationRoutes (server) { if (req.issuer) query.issuerId = req.issuer.id; if (req.program) query.programId = req.program.id; - Applications.get(query, options, function foundRows (error, rows) { + if (req.pageData) { + options.limit = req.pageData.count; + options.page = req.pageData.page; + options.includeTotal = true; + } + + Applications.get(query, options, function foundRows (error, result) { if (error) return dbErrorHandler(error, null, res, next); - res.send({applications: rows.map(function (application) { - return Applications.toResponse(application, req); - })}); + var total = 0; + var rows = result; + if (req.pageData) { + total = result.total; + rows = result.rows; + } + + var responseData = {applications: rows.map(function (application) { return Applications.toResponse(application, req); })} + sendPaginated(req, res, responseData, total); return next(); }); diff --git a/app/routes/badge-instances.js b/app/routes/badge-instances.js index 8aedbe7..7776748 100644 --- a/app/routes/badge-instances.js +++ b/app/routes/badge-instances.js @@ -3,6 +3,7 @@ const util = require('util') const unixtimeFromDate = require('../lib/unixtime-from-date') const sha256 = require('../lib/hash').sha256 const customError = require('../lib/custom-error') +const sendPaginated = require('../lib/send-paginated') const Badges = require('../models/badge') const ClaimCodes = require('../models/claim-codes') const Webhooks = require('../models/webhook') @@ -259,8 +260,23 @@ exports = module.exports = function applyBadgeRoutes (server) { server.get(prefix.program + getInstancesSuffix, findProgramBadge, getBadgeInstances) function getBadgeInstances(req, res, next) { - BadgeInstances.get({ badgeId: req.badge.id}, {relationships: true, relationshipsDepth: 2}).then(function (rows) { - return res.send({instances: rows.map(function (row) { return BadgeInstances.toResponse(row, req); })}); + var options = {relationships: true, relationshipsDepth: 2}; + + if (req.pageData) { + options.limit = req.pageData.count; + options.page = req.pageData.page; + options.includeTotal = true; + } + + BadgeInstances.get({ badgeId: req.badge.id}, options).then(function (result) { + var total = 0; + var rows = result; + if (req.pageData) { + total = result.total; + rows = result.rows; + } + var responseData = {instances: rows.map(function (row) { return BadgeInstances.toResponse(row, req); })} + return sendPaginated(req, res, responseData, total); }) .error(function (err) { log.error(err, 'error fetching badge instances'); @@ -361,13 +377,28 @@ exports = module.exports = function applyBadgeRoutes (server) { BadgeInstances.get([query, queryParams]).then(function (rows) { var instanceIds = rows.map(function(row) { return row.id; }); if (instanceIds.length) { - return BadgeInstances.get( { id: instanceIds }, { relationships: true, relationshipsDepth: 2 }); + var options = { relationships: true, relationshipsDepth: 2 }; + if (req.pageData) { + options.limit = req.pageData.count; + options.page = req.pageData.page; + options.includeTotal = true; + } + + return BadgeInstances.get( { id: instanceIds }, options); } else { return Promise.resolve([]); } - }).then(function (rows) { - res.send({instances: rows.map(function (row) { return BadgeInstances.toResponse(row, req); })}); + }).then(function (result) { + var total = 0; + var rows = result; + if (req.pageData) { + total = result.total; + rows = result.rows; + } + + var responseData = {instances: rows.map(function (row) { return BadgeInstances.toResponse(row, req); })} + return sendPaginated(req, res, responseData, total); }).error(function (err) { if (!err.restCode) log.error(err, 'unknown error in getUserInstances route') diff --git a/app/routes/badges.js b/app/routes/badges.js index b298d4e..c8b3f76 100644 --- a/app/routes/badges.js +++ b/app/routes/badges.js @@ -7,6 +7,7 @@ const Milestones = require('../models/milestone'); const imageHelper = require('../lib/image-helper') const errorHelper = require('../lib/error-helper') const middleware = require('../lib/middleware') +const sendPaginated = require('../lib/send-paginated'); const putBadgeHelper = imageHelper.putModel(Badges) const dbErrorHandler = errorHelper.makeDbHandler('badge') @@ -61,11 +62,27 @@ exports = module.exports = function applyBadgeRoutes (server) { if (req.issuer) query.issuerId = req.issuer.id if (req.program) query.programId = req.program.id - Badges.get(query, options, function foundRows (error, rows) { + if (req.pageData) { + options.limit = req.pageData.count; + options.page = req.pageData.page; + options.includeTotal = true; + } + + Badges.get(query, options, function foundRows (error, result) { if (error) return dbErrorHandler(error, null, res, next); + + var total = 0; + var rows = result; + if (req.pageData) { + total = result.total; + rows = result.rows; + } + + var responseData = { badges: rows.map(Badges.toResponse) }; - res.send({badges: rows.map(Badges.toResponse)}); + sendPaginated(req, res, responseData, total); + return next(); }); } diff --git a/app/routes/claim-codes.js b/app/routes/claim-codes.js index 3627a43..93f8841 100644 --- a/app/routes/claim-codes.js +++ b/app/routes/claim-codes.js @@ -2,6 +2,7 @@ const Badges = require('../models/badge') const ClaimCodes = require('../models/claim-codes') const middleware = require('../lib/middleware') const errorHelper = require('../lib/error-helper') +const sendPaginated = require('../lib/send-paginated'); // #TODO: factor this out, see ./badge-instances.js @@ -107,13 +108,28 @@ exports = module.exports = function applyClaimCodesRoutes (server) { const query = {badgeId: req.badge.id} const options = {relationships: false} + if (req.pageData) { + options.limit = req.pageData.count; + options.page = req.pageData.page; + options.includeTotal = true; + } + ClaimCodes .get(query, options) - .then(function (claimCodes) { - res.send(200, { - claimCodes: claimCodes.map(ClaimCodes.toResponse), + .then(function (result) { + var total = 0; + var rows = result; + if (req.pageData) { + total = result.total; + rows = result.rows; + } + + var responseData = { + claimCodes: rows.map(ClaimCodes.toResponse), badge: req.badge.toResponse(), - }) + }; + + sendPaginated(req, res, responseData, total); }) .error(req.error('Error getting claim code list')) } diff --git a/app/routes/issuers.js b/app/routes/issuers.js index c2d830b..9ae8f51 100644 --- a/app/routes/issuers.js +++ b/app/routes/issuers.js @@ -4,6 +4,7 @@ const Issuers = require('../models/issuer'); const imageHelper = require('../lib/image-helper') const errorHelper = require('../lib/error-helper') const middleware = require('../lib/middleware') +const sendPaginated = require('../lib/send-paginated'); const putIssuer = imageHelper.putModel(Issuers) const dbErrorHandler = errorHelper.makeDbHandler('issuer') @@ -16,10 +17,26 @@ exports = module.exports = function applyIssuerRoutes (server) { function showAllIssuers(req, res, next) { const options = {relationships: true} const query = {systemId: req.system.id} - Issuers.get(query, options, function foundRows(error, rows) { + + if (req.pageData) { + options.limit = req.pageData.count; + options.page = req.pageData.page; + options.includeTotal = true; + } + + Issuers.get(query, options, function foundRows(error, result) { if (error) return dbErrorHandler(error, null, res, next) - return res.send({issuers: rows.map(Issuers.toResponse)}); + + var total = 0; + var rows = result; + if (req.pageData) { + total = result.total; + rows = result.rows; + } + + var responseData = {issuers: rows.map(Issuers.toResponse)} + return sendPaginated(req, res, responseData, total) }); } diff --git a/app/routes/milestones.js b/app/routes/milestones.js index 14f3e4e..f2fa216 100644 --- a/app/routes/milestones.js +++ b/app/routes/milestones.js @@ -5,6 +5,7 @@ const Milestones = require('../models/milestone'); const MilestoneBadges = require('../models/milestone-badge') const middleware = require('../lib/middleware'); const errorHelper = require('../lib/error-helper') +const sendPaginated = require('../lib/send-paginated'); exports = module.exports = function applyBadgeRoutes(server) { server.get('/systems/:systemSlug/milestones', [ @@ -14,11 +15,24 @@ exports = module.exports = function applyBadgeRoutes(server) { function showAllMilestones(req, res, next) { const query = { systemId: req.system.id }; const options = { relationships: true }; + + if (req.pageData) { + options.limit = req.pageData.count; + options.page = req.pageData.page; + options.includeTotal = true; + } + Milestones.get(query, options) - .then(function (milestones) { - return res.send(200, { - milestones: milestones.map(Milestones.toResponse) - }); + .then(function (result) { + var total = 0; + var rows = result; + if (req.pageData) { + total = result.total; + rows = result.rows; + } + + var responseData = { milestones: rows.map(Milestones.toResponse) }; + return sendPaginated(req, res, responseData, total); }) .catch(handleError(req, next)); } diff --git a/app/routes/programs.js b/app/routes/programs.js index 4785148..4bbbfb8 100644 --- a/app/routes/programs.js +++ b/app/routes/programs.js @@ -4,6 +4,7 @@ const Programs = require('../models/program'); const imageHelper = require('../lib/image-helper') const errorHelper = require('../lib/error-helper') const middleware = require('../lib/middleware') +const sendPaginated = require('../lib/send-paginated'); const putProgram = imageHelper.putModel(Programs) const dbErrorHandler = errorHelper.makeDbHandler('program') @@ -17,11 +18,26 @@ exports = module.exports = function applyProgramRoutes (server) { function showAllPrograms(req, res, next) { const options = {relationships: true} const query = {issuerId: req.issuer.id} - Programs.get(query, options, function foundRows(error, rows) { + + if (req.pageData) { + options.limit = req.pageData.count; + options.page = req.pageData.page; + options.includeTotal = true; + } + + Programs.get(query, options, function foundRows(error, result) { if (error) return dbErrorHandler(error, null, res, next) - res.send({programs: rows.map(Programs.toResponse)}); + var total = 0; + var rows = result; + if (req.pageData) { + total = result.total; + rows = result.rows; + } + + var responseData = {programs: rows.map(Programs.toResponse)}; + sendPaginated(req, res, responseData, total); return next(); }); } diff --git a/app/routes/reviews.js b/app/routes/reviews.js index 12485a0..86c0477 100644 --- a/app/routes/reviews.js +++ b/app/routes/reviews.js @@ -9,6 +9,7 @@ const middleware = require('../lib/middleware') const hash = require('../lib/hash').hash const request = require('request') const log = require('../lib/logger') +const sendPaginated = require('../lib/send-paginated'); const _ = require('underscore'); const dbErrorHandler = errorHelper.makeDbHandler('application') @@ -39,11 +40,25 @@ exports = module.exports = function applyReviewRoutes (server) { var query = { applicationId : req.application.id }; var options = {relationships: true}; - Reviews.get(query, options, function foundRows (error, rows) { + if (req.pageData) { + options.limit = req.pageData.count; + options.page = req.pageData.page; + options.includeTotal = true; + } + + Reviews.get(query, options, function foundRows (error, result) { if (error) return dbErrorHandler(error, null, res, next); + + var total = 0; + var rows = result; + if (req.pageData) { + total = result.total; + rows = result.rows; + } - res.send({reviews: rows.map(Reviews.toResponse)}); + var responseData = {reviews: rows.map(Reviews.toResponse)}; + sendPaginated(req, res, responseData, total); return next(); }); } diff --git a/app/routes/systems.js b/app/routes/systems.js index 24f218c..fd796d3 100644 --- a/app/routes/systems.js +++ b/app/routes/systems.js @@ -4,6 +4,7 @@ const Systems = require('../models/system'); const imageHelper = require('../lib/image-helper') const errorHelper = require('../lib/error-helper') const middleware = require('../lib/middleware') +const sendPaginated = require('../lib/send-paginated'); const putSystem = imageHelper.putModel(Systems) const dbErrorHandler = errorHelper.makeDbHandler('system') @@ -13,11 +14,26 @@ exports = module.exports = function applySystemRoutes (server) { function showAllSystems(req, res, next) { const query = {} const options = {relationships: true}; - Systems.get(query, options, function foundRows(error, rows) { + + if (req.pageData) { + options.limit = req.pageData.count; + options.page = req.pageData.page; + options.includeTotal = true; + } + + Systems.get(query, options, function foundRows(error, result) { if (error) return dbErrorHandler(error, null, res, next) - return res.send({systems: rows.map(Systems.toResponse)}); + var total = 0; + var rows = result; + if (req.pageData) { + total = result.total; + rows = result.rows; + } + + var responseData = {systems: rows.map(Systems.toResponse)}; + return sendPaginated(req, res, responseData, total); }); } diff --git a/package.json b/package.json index b40fdb9..81a9e60 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dependencies": { "mysql": "~2.0.0-alpha9", "restify": "~2.6.1", - "streamsql": "~0.8.0", + "streamsql": "~0.8.3", "validator": "~2.0.0", "xtend": "~2.1.1", "bluebird": "~1.0.8", diff --git a/test/applications.test.js b/test/applications.test.js index cf48f1c..a01016f 100644 --- a/test/applications.test.js +++ b/test/applications.test.js @@ -12,13 +12,16 @@ spawn(app).then(function (api) { return body.applications.map(prop('slug')).sort() } - t.plan(23); + t.plan(26); - api.get('/systems/chicago/applications').then(function (res) { + api.get('/systems/chicago/applications?page=1&count=2').then(function (res) { const slugs = getSlugs(res.body) active = res.body.applications t.same(res.statusCode, 200, 'should have HTTP 200') t.same(slugs.length, 2) + t.same(res.body.pageData.total, 2) + t.same(res.body.pageData.page, 1) + t.same(res.body.pageData.count, 2) t.same(slugs.indexOf('app-pittsburgh'), -1, 'should not have pittsburgh system application') }).catch(api.fail(t)) diff --git a/test/badge-instances.test.js b/test/badge-instances.test.js index b725aaf..7324599 100644 --- a/test/badge-instances.test.js +++ b/test/badge-instances.test.js @@ -63,10 +63,13 @@ spawn(app).then(function (api) { t.same(issuerOrg.url, 'http://cityofchicago.org') t.same(issuerOrg.description, 'The City of Chicago') t.same(issuerOrg.email, 'mayor-emanuel@cityofchicago.org') - return api.get('/systems/chicago/instances/' + email) + return api.get('/systems/chicago/instances/' + email + '?page=1&count=1') }).then(function(res) { t.same(res.statusCode, 200, 'should be found') t.ok(res.body.instances && res.body.instances.length === 1, 'should find one instance') + t.same(res.body.pageData.total, 1) + t.same(res.body.pageData.page, 1) + t.same(res.body.pageData.count, 1) const instance = res.body.instances[0] t.same(instance.email, email, 'should be correct email') t.same(instance.badge.slug, 'chicago-badge', 'should be instance of chicago-badge') diff --git a/test/badges.test.js b/test/badges.test.js index 54f522a..ee05a2a 100644 --- a/test/badges.test.js +++ b/test/badges.test.js @@ -12,7 +12,7 @@ spawn(app).then(function (api) { return body.badges.map(prop('slug')).sort() } - t.plan(22) + t.plan(33) api.get('/systems/chicago/badges').then(function (res) { const slugs = getSlugs(res.body) @@ -23,6 +23,28 @@ spawn(app).then(function (api) { t.same(slugs.indexOf('pittsburgh-badge'), -1, 'should not find pittsburgh badge') }).catch(api.fail(t)) + var pageOneBadgeSlugs + + api.get('/systems/chicago/badges?page=1&count=2').then(function (res) { + const slugs = pageOneBadgeSlugs = getSlugs(res.body) + t.same(res.statusCode, 200, 'should have HTTP 200') + t.same(slugs.length, 2) + t.same(res.body.pageData.page, 1, 'page data should indicate page 1') + t.same(res.body.pageData.count, 2, 'page data should indicate a count of 2') + t.same(res.body.pageData.total, 4, 'page data should indicate a total of 4') + }).catch(api.fail(t)) + + api.get('/systems/chicago/badges?page=2&count=2').then(function (res) { + const slugs = getSlugs(res.body) + t.same(res.statusCode, 200, 'should have HTTP 200') + t.same(slugs.length, 2) + t.same(res.body.pageData.page, 2, 'page data should indicate page 2') + t.same(res.body.pageData.count, 2, 'page data should indicate a count of 2') + t.same(res.body.pageData.total, 4, 'page data should indicate a total of 4') + const sharedSlugs = slugs.filter(function(slug) { return (pageOneBadgeSlugs.indexOf(slug) != -1 ) }); + t.same(sharedSlugs.length, 0, 'should not have any slugs in common with first page') + }).catch(api.fail(t)) + api.get('/systems/chicago/issuers/chicago-library/badges').then(function (res) { const slugs = getSlugs(res.body) t.same(res.statusCode, 200, 'should have HTTP 200') diff --git a/test/claim-codes.test.js b/test/claim-codes.test.js index 7bbedfa..9616702 100644 --- a/test/claim-codes.test.js +++ b/test/claim-codes.test.js @@ -24,9 +24,12 @@ spawn(app).then(function (api) { }) test('List all claimcodes', function (t) { - const url = '/systems/chicago/badges/chicago-badge/codes' + const url = '/systems/chicago/badges/chicago-badge/codes?page=1&count=5' api.get(url).then(function (res) { t.same(res.body.claimCodes.length, 4, 'should have right amount of codes') + t.same(res.body.pageData.page, 1, 'page data should indicate page 1') + t.same(res.body.pageData.count, 5, 'page data should indicate a count of 5') + t.same(res.body.pageData.total, 4, 'page data should indicate a total of 4') t.same(res.body.badge.slug, 'chicago-badge', 'should get badge back as well') t.end() }).catch(api.fail(t)) diff --git a/test/issuers.test.js b/test/issuers.test.js index 5b3d2c0..e2fa1a5 100644 --- a/test/issuers.test.js +++ b/test/issuers.test.js @@ -7,8 +7,11 @@ const spawn = require('./spawn') spawn(app).then(function (api) { test('get issuer list', function (t) { - api.get('/systems/chicago/issuers').then(function (res) { + api.get('/systems/chicago/issuers?page=1&count=5').then(function (res) { t.ok(res.body.issuers, 'should have issuers') + t.same(res.body.pageData.page, 1, 'page data should indicate page 1') + t.same(res.body.pageData.count, 5, 'page data should indicate a count of 5') + t.same(res.body.pageData.total, 1, 'page data should indicate a total of 1') t.same(res.body.issuers[0].slug, 'chicago-library') return api.get('/systems/bogus/issuers') }).then(function(res){ diff --git a/test/milestones.test.js b/test/milestones.test.js index c0791eb..b430978 100644 --- a/test/milestones.test.js +++ b/test/milestones.test.js @@ -22,10 +22,13 @@ spawn(app).then(function (api) { }) test('Get all milestones', function (t) { - api.get('/systems/chicago/milestones') + api.get('/systems/chicago/milestones?page=1&count=5') .then(function (res) { t.same(res.statusCode, 200, '200 OK') t.ok(res.body.milestones.length >= 1, 'at least one milestone') + t.same(res.body.pageData.page, 1, 'page data should indicate page 1') + t.same(res.body.pageData.count, 5, 'page data should indicate a count of 5') + t.same(res.body.pageData.total, 2, 'page data should indicate a total of 2') t.end() }) .catch(api.fail(t)) diff --git a/test/programs.test.js b/test/programs.test.js index 34561cf..2c3d073 100644 --- a/test/programs.test.js +++ b/test/programs.test.js @@ -7,10 +7,13 @@ const spawn = require('./spawn') spawn(app).then(function (api) { test('get program list', function (t) { - api.get('/systems/chicago/issuers/chicago-library/programs').then(function (res) { + api.get('/systems/chicago/issuers/chicago-library/programs?page=1&count=5').then(function (res) { t.ok(res.body.programs, 'should have programs') t.same(res.body.programs[0].id, 1) t.same(res.body.programs[0].slug, 'mit-scratch') + t.same(res.body.pageData.page, 1, 'page data should indicate page 1') + t.same(res.body.pageData.count, 5, 'page data should indicate a count of 5') + t.same(res.body.pageData.total, 1, 'page data should indicate a total of 1') t.end() }).catch(api.fail(t)) }) diff --git a/test/reviews.test.js b/test/reviews.test.js index 82b30bb..328a23b 100644 --- a/test/reviews.test.js +++ b/test/reviews.test.js @@ -12,13 +12,16 @@ spawn(app).then(function (api) { return body.reviews.map(prop('slug')).sort() } - t.plan(9); + t.plan(12); - api.get('/systems/chicago/badges/chicago-scratch-badge/applications/app-scratch/reviews').then(function (res) { + api.get('/systems/chicago/badges/chicago-scratch-badge/applications/app-scratch/reviews?page=1&count=1').then(function (res) { const slugs = getSlugs(res.body) active = res.body.reviews t.same(res.statusCode, 200, 'should have HTTP 200') t.same(slugs.length, 1) + t.same(res.body.pageData.total, 1) + t.same(res.body.pageData.page, 1) + t.same(res.body.pageData.count, 1) t.same(slugs.indexOf('review-archived'), -1, 'should not have archived badge review)') }).catch(api.fail(t)) diff --git a/test/systems.test.js b/test/systems.test.js index 790c42b..3e43148 100644 --- a/test/systems.test.js +++ b/test/systems.test.js @@ -7,9 +7,12 @@ const spawn = require('./spawn') spawn(app).then(function (api) { test('get system list', function (t) { - api.get('/systems').then(function (res) { + api.get('/systems?page=1&count=5').then(function (res) { t.ok(res.body.systems, 'should have systems') t.same(res.body.systems[0].slug, 'chicago') + t.same(res.body.pageData.page, 1, 'page data should indicate page 1') + t.same(res.body.pageData.count, 5, 'page data should indicate a count of 5') + t.same(res.body.pageData.total, 2, 'page data should indicate a total of 2') t.end() }).catch(api.fail(t)) })