diff --git a/api/middleware/filter-handler-mongo.js b/api/middleware/filter-handler-mongo.js index 5dd9ad0..9ae5878 100644 --- a/api/middleware/filter-handler-mongo.js +++ b/api/middleware/filter-handler-mongo.js @@ -22,7 +22,7 @@ "use strict"; const express = require('express'); -const logger = require('debug')('me2:filterware'); +const logger = require('debug')('middleware:filterware'); /** * private helper function to filter Objects by given keys diff --git a/api/middleware/limitoffset-middleware-mongo.js b/api/middleware/limitoffset-middleware-mongo.js new file mode 100644 index 0000000..944ee3a --- /dev/null +++ b/api/middleware/limitoffset-middleware-mongo.js @@ -0,0 +1,69 @@ +/** This module defines a express.Router() instance + * - supporting offset= and limit=* + * - calls next with error if a impossible offset and/or limit value is given + * + * Note: it expects to be called BEFORE any data fetched from DB + * Note: it sets an object { limit: 0, skip: 0 } with the proper number values in req.locals.limitskip + * Note: it sets an Error-Object to next with error.status set to HTTP status code 400 + * + * @author Johannes Konert + * @licence CC BY-SA 4.0 + * + * @module restapi/limitoffset-middleware-mongo + * @type {Router} + */ + +// remember: in modules you have 3 variables given by CommonJS +// 1.) require() function +// 2.) module.exports +// 3.) exports (which is module.exports) +"use strict"; + +const router = require('express').Router(); +const logger = require('debug')('middleware:offsetlimit'); + + +// the exported router with handler +router.use((req, res, next) => { + let offset = undefined; + let limit = undefined; + const offsetString = req.query.offset; + const limitString = req.query.limit; + let err = null; + + + if (offsetString) { + if (!isNaN(offsetString)) { + offset = parseInt(offsetString); + if (offset < 0) { + err = new Error('offset is negative') + } + } + else { + err = new Error('given offset is not a valid number ' + offsetString); + } + } + if (limitString) { + if (!isNaN(limitString)) { + limit = parseInt(limitString); + if (limit < 1) { + err = new Error('limit is zero or negative') + } + } + else { + err = new Error('given limit is not a valid number ' + limitString); + } + } + if (err) { + logger('problem occurred with limit/offset values'); + err.status = 400; + next(err) + } else { + res.locals.limitskip = {}; // mongoDB uses parameter object for skip/limit + if (limit) res.locals.limitskip.limit = limit; + if (offset) res.locals.limitskip.skip = offset; + next() + } +}); + +module.exports = router; diff --git a/api/routes/users.js b/api/routes/users.js index 948b879..5e77d30 100644 --- a/api/routes/users.js +++ b/api/routes/users.js @@ -10,89 +10,57 @@ const codes = require('./http-codes'); const apiAuthenticationMiddleware = require('../middleware/auth-middleware'); const checkHl = require('../middleware/permission-check').checkHl; -const sortCollectionBy = require('../middleware/util').sortCollection; +const offsetlimitMiddleware = require('../middleware/limitoffset-middleware-mongo'); +const filterHandlerCreator = require('../middleware/filter-handler-mongo'); const routerHandling = require('../middleware/router-handling'); // Mongoose Model using mongoDB const UserModel = require('../models/user'); -const RankModel = require('../models/rank'); +const SquadModel = require('../models/squad'); const AwardingModel = require('../models/awarding'); -const resultSet = {'__v': 0, 'updatedAt': 0, 'timestamp': 0}; - - const users = express.Router(); +users.get('/', filterHandlerCreator(UserModel.schema.paths)); +users.get('/', offsetlimitMiddleware); + // routes ********************** users.route('/') .get((req, res, next) => { - if (req.query.simple) { - UserModel.find({}, res.locals.filter, res.locals.limitskip, (err, items) => { - if (err) { - err.status = codes.servererror; - return next(err); - } - // if the collection is empty we do not send empty arrays back. - - res.locals.items = items; - res.locals.processed = true; - next(); - }) - } - else { - const nameQuery = req.query.q; - const fractionFilter = req.query.fractFilter; - const squadFilter = req.query.squadId; - - UserModel.find({}, (err, users) => { - if (err) return next(err); - if (users.length === 0) { - res.locals.items = users; - res.locals.processed = true; - next(); - } - let resUsers = []; - let rowsLength = users.length; - users.forEach((user) => { - // filter by name - if (!nameQuery || (nameQuery && user.username.toLowerCase().includes(nameQuery.toLowerCase()))) { - getExtendedUser(user, next, (extUser) => { - - // filter by squad - if (squadFilter) { - if (extUser.squad && extUser.squad._id.toString() === squadFilter) { - resUsers.push(extUser); - } - else { - rowsLength -= 1; - } - } - // filter by fraction - else if (!fractionFilter || - (fractionFilter && extUser.squad && extUser.squad.fraction.toLowerCase() === fractionFilter) || - (fractionFilter && fractionFilter === 'unassigned' && !extUser.squad)) { - resUsers.push(extUser); - } else { - rowsLength -= 1; - } - if (resUsers.length === rowsLength) { - resUsers = sortCollectionBy(resUsers, 'username'); - res.locals.items = resUsers; - res.locals.processed = true; - return next(); - } - }); - } else { - rowsLength -= 1; - // no user matching query - return empty [] - if (rowsLength === 0) { - res.locals.items = resUsers; - res.locals.processed = true; - next(); - } + const userQuery = () => { + UserModel.find(dbFilter, res.locals.filter, res.locals.limitskip) + .populate('squadId') + .collation({locale: "en", strength: 2}) // case insensitive order + .sort('username').exec((err, users) => { + if (err) return next(err); + if (users.length === 0) { + res.locals.items = users; + res.locals.processed = true; + return next(); } + UserModel.count(dbFilter, (err, totalCount) => { + res.set('x-total-count', totalCount); + res.locals.items = users; + res.locals.processed = true; + return next(); + }) }) + }; + + if (!req.query.q) req.query.q = ''; + const dbFilter = {username: {"$regex": req.query.q, "$options": "i"}}; + if (req.query.squadId) dbFilter["squadId"] = {"$eq": req.query.squadId}; + // squad / fraction filter setup + if (req.query.fractFilter && req.query.fractFilter !== 'UNASSIGNED' && !req.query.squadId) { + SquadModel.find({'fraction': req.query.fractFilter}, {_id: 1}, (err, squads) => { + dbFilter['squadId'] = {$in: squads.map(squad => squad.id)}; + userQuery(); }) + } else { + if (req.query.fractFilter === 'UNASSIGNED') { + dbFilter['squadId'] = {$eq: null}; + } + userQuery(); } }) @@ -105,7 +73,8 @@ users.route('/') return next(err); } res.status(codes.created); - getExtendedUser(user, next, (extUser) => { + + UserModel.populate(user, {path: 'squadId'}, (err, extUser) => { res.locals.items = extUser; res.locals.processed = true; return next(); @@ -118,26 +87,19 @@ users.route('/') users.route('/:id') .get((req, res, next) => { - - UserModel.findById(req.params.id, (err, item) => { + UserModel.findById(req.params.id).populate('squadId').exec((err, user) => { if (err) { err.status = codes.servererror; return next(err); } - else if (!item) { + else if (!user) { err = new Error("item not found"); err.status = codes.notfound; return next(err); - } else if (req.query.simple) { - res.locals.items = item; - next(); } - getExtendedUser(item, next, (extUser) => { - res.locals.items = extUser; - res.locals.processed = true; - return next(); - }) - + res.locals.items = user; + res.locals.processed = true; + return next(); }); }) @@ -162,29 +124,21 @@ users.route('/:id') else if (!item) { err = new Error("item not found"); err.status = codes.notfound; - } else if (req.query.simple) { - res.locals.items = item; + } + UserModel.populate(item, {path: 'squadId'}, (err, extUser) => { + if (err) { + err.status = codes.servererror; + return next(err); + } + if (!user) { + res.locals.items = {}; + res.locals.processed = true; + return next(); + } + res.locals.items = extUser; res.locals.processed = true; return next(); - } - else { - UserModel.findById(item._id, (err, user) => { - if (err) { - err.status = codes.servererror; - return next(err); - } - if (!user) { - res.locals.items = {}; - res.locals.processed = true; - return next(); - } - getExtendedUser(user, next, (extUser) => { - res.locals.items = extUser; - res.locals.processed = true; - return next(); - }) - }) - } + }) }) }) @@ -198,7 +152,7 @@ users.route('/:id') return; // prevent node to process this function further after next() has finished. } // main difference of PUT and PATCH is that PUT expects all data in request: checked by using the schema - var user = new UserModel(req.body); + const user = new UserModel(req.body); UserModel.findById(req.params.id, req.body, {new: true}, function (err, item) { // with parameter {new: true} the TweetNModel will return the new and changed object from the DB and not the old one. if (err) { @@ -210,7 +164,7 @@ users.route('/:id') err.status = codes.notfound; return next(err); } - // optional task 3b: check that version is still accurate + // check that version is still accurate else if (user.__v !== item.__v) { err = new Error("version outdated. Meanwhile update on item happened. Please GET resource again") err.status = codes.conflict; @@ -224,7 +178,7 @@ users.route('/:id') } } - // optional task 3: update updatedAt and increase version + // update updatedAt and increase version item.updatedAt = new Date(); item.increment(); // this sets __v++ item.save(function (err) { @@ -234,7 +188,8 @@ users.route('/:id') err.status = codes.wrongrequest; err.message += ' in fields: ' + Object.getOwnPropertyNames(err.errors); } - getExtendedUser(item, next, (extUser) => { + + UserModel.populate(item, {path: 'squadId'}, (err, extUser) => { res.locals.items = extUser; res.locals.processed = true; return next(); @@ -283,56 +238,4 @@ users.route('/:id') // it looks for object(s) in res.locals.items and if they exist, they are send to the client as json users.use(routerHandling.emptyResponse); -/** - * Create model for single extended user and - * return via callback - */ -let getExtendedUser = (user, next, callback) => { - let extUser = {}; - UserModel.findById(user._id, resultSet) - .populate('squadId', resultSet).exec((err, member) => { - if (err) { - err.status = codes.servererror; - return next(err); - } - extUser._id = user._id; - extUser.username = user.username; - extUser.squad = member.squadId; - - if (extUser.squad) { - RankModel.findOne({ - level: member.rankLvl, - fraction: member.squadId.fraction - }, resultSet, (err, rank) => { - if (err) { - err.status = codes.servererror; - return next(err); - } - extUser.rank = rank; - }).then(() => { - addAwards(extUser).then(() => { - callback(extUser); - }) - }) - } else { - extUser.rank = {level: user.rankLvl}; - addAwards(extUser).then(() => { - callback(extUser); - }) - } - }) -}; - -let addAwards = (extUser) => { - return AwardingModel.find({userId: extUser._id}, resultSet, {sort: {date: 'desc'}}) - .populate('decorationId', resultSet) - .exec((err, awards) => { - if (err) { - err.status = codes.servererror; - return next(err); - } - extUser.awards = awards; - }) -}; - module.exports = users; diff --git a/package.json b/package.json index 3976222..2ef491a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opt-cc", - "version": "1.5.3", + "version": "1.5.4", "license": "MIT", "private": true, "scripts": { @@ -15,8 +15,7 @@ "start-e2e": "npm run deploy-static && npm run e2e --prefix ./api", "test-e2e": "npm run e2e --prefix ./static" }, - "dependencies": { - }, + "dependencies": {}, "devDependencies": { "concurrently": "^3.4.0", "wait-on": "^2.0.2" diff --git a/static/package.json b/static/package.json index 7791630..b3ceb71 100644 --- a/static/package.json +++ b/static/package.json @@ -33,6 +33,7 @@ "ngx-bootstrap": "^2.0.0-beta.6", "ngx-clipboard": "^8.1.0", "ngx-cookie-service": "^1.0.9", + "ngx-infinite-scroll": "^0.5.2", "rxjs": "^5.2.0", "ts-helpers": "^1.1.1", "typescript": "^2.3.4", diff --git a/static/src/app/app.component.css b/static/src/app/app.component.css index 73039f4..2a8381f 100644 --- a/static/src/app/app.component.css +++ b/static/src/app/app.component.css @@ -29,6 +29,15 @@ li { overflow-x: hidden; } +.version-label { + display:block; + position: fixed; + top: 32px; + left: 106px; + font-size: 12px; + color: #7e7d64; +} + .unprocessed { -webkit-animation-name: color-blink; /* Safari 4.0 - 8.0 */ -webkit-animation-duration: 4s; /* Safari 4.0 - 8.0 */ diff --git a/static/src/app/app.component.html b/static/src/app/app.component.html index bb60e51..c3cc9d9 100644 --- a/static/src/app/app.component.html +++ b/static/src/app/app.component.html @@ -11,6 +11,7 @@ + {{version}}