From def02622a620ce28ba54a3ad8874682d4e2778fd Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 2 Dec 2020 17:58:06 +0100 Subject: [PATCH] Create basic crud for activities, units and user profiles --- .idea/.gitignore | 5 + .idea/backend.iml | 12 + .idea/codeStyles/Project.xml | 67 +++++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/inspectionProfiles/Project_Default.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + src/config/roles.js | 11 +- src/controllers/activity.controller.js | 43 ++++ src/controllers/index.js | 3 + src/controllers/profile.controller.js | 44 ++++ src/controllers/unit.controller.js | 43 ++++ src/models/activity.model.js | 18 +- src/models/index.js | 3 + src/models/profile.model.js | 19 +- src/models/unit.model.js | 38 +++ src/models/units.model.js | 26 -- src/models/user.model.js | 2 +- src/routes/v1/activity.route.js | 231 +++++++++++++++++ src/routes/v1/index.js | 6 + src/routes/v1/profile.route.js | 254 +++++++++++++++++++ src/routes/v1/unit.route.js | 224 ++++++++++++++++ src/services/activity.service.js | 77 ++++++ src/services/index.js | 3 + src/services/profile.service.js | 87 +++++++ src/services/unit.service.js | 77 ++++++ src/validations/activity.validation.js | 50 ++++ src/validations/index.js | 3 + src/validations/profile.validation.js | 60 +++++ src/validations/unit.validation.js | 47 ++++ 30 files changed, 1444 insertions(+), 34 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/backend.iml create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 src/controllers/activity.controller.js create mode 100644 src/controllers/profile.controller.js create mode 100644 src/controllers/unit.controller.js create mode 100644 src/models/unit.model.js delete mode 100644 src/models/units.model.js create mode 100644 src/routes/v1/activity.route.js create mode 100644 src/routes/v1/profile.route.js create mode 100644 src/routes/v1/unit.route.js create mode 100644 src/services/activity.service.js create mode 100644 src/services/profile.service.js create mode 100644 src/services/unit.service.js create mode 100644 src/validations/activity.validation.js create mode 100644 src/validations/profile.validation.js create mode 100644 src/validations/unit.validation.js diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/backend.iml b/.idea/backend.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/backend.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..e5c2b82 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e066844 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/config/roles.js b/src/config/roles.js index fb76ba6..22b5ab4 100644 --- a/src/config/roles.js +++ b/src/config/roles.js @@ -1,8 +1,15 @@ const roles = ['user', 'admin']; +const allRights = [ + 'getUsers', 'manageUsers', + 'getActivities', 'manageActivities', + 'getProfiles', 'manageProfiles', + 'getUnits', 'manageUnits', +]; + const roleRights = new Map(); -roleRights.set(roles[0], []); -roleRights.set(roles[1], ['getUsers', 'manageUsers']); +roleRights.set(roles[0], allRights); +roleRights.set(roles[1], allRights); module.exports = { roles, diff --git a/src/controllers/activity.controller.js b/src/controllers/activity.controller.js new file mode 100644 index 0000000..9753f15 --- /dev/null +++ b/src/controllers/activity.controller.js @@ -0,0 +1,43 @@ +const httpStatus = require('http-status'); +const pick = require('../utils/pick'); +const ApiError = require('../utils/ApiError'); +const catchAsync = require('../utils/catchAsync'); +const { activityService } = require('../services'); + +const createActivity = catchAsync(async (req, res) => { + const activity = await activityService.createActivity(req.body); + res.status(httpStatus.CREATED).send(activity); +}); + +const getActivities = catchAsync(async (req, res) => { + const filter = pick(req.query, ['name', 'factor']); + const options = pick(req.query, ['sortBy', 'limit', 'page']); + const result = await activityService.queryActivities(filter, options); + res.send(result); +}); + +const getActivity = catchAsync(async (req, res) => { + const activity = await activityService.getActivityById(req.params.activityId); + if (!activity) { + throw new ApiError(httpStatus.NOT_FOUND, 'Activity not found'); + } + res.send(activity); +}); + +const updateActivity = catchAsync(async (req, res) => { + const activity = await activityService.updateActivityById(req.params.activityId, req.body); + res.send(activity); +}); + +const deleteActivity = catchAsync(async (req, res) => { + await activityService.deleteActivityById(req.params.activityId); + res.status(httpStatus.NO_CONTENT).send(); +}); + +module.exports = { + createActivity, + getActivities, + getActivity, + updateActivity, + deleteActivity, +}; diff --git a/src/controllers/index.js b/src/controllers/index.js index 653b4e2..345435a 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -1,2 +1,5 @@ module.exports.authController = require('./auth.controller'); module.exports.userController = require('./user.controller'); +module.exports.profileController = require('./profile.controller'); +module.exports.activityController = require('./activity.controller'); +module.exports.unitController = require('./unit.controller'); diff --git a/src/controllers/profile.controller.js b/src/controllers/profile.controller.js new file mode 100644 index 0000000..3992661 --- /dev/null +++ b/src/controllers/profile.controller.js @@ -0,0 +1,44 @@ +const httpStatus = require('http-status'); +const pick = require('../utils/pick'); +const ApiError = require('../utils/ApiError'); +const catchAsync = require('../utils/catchAsync'); +const { profileService } = require('../services'); + +const createProfile = catchAsync(async (req, res) => { + req.body.userId = req.user._id; + const profile = await profileService.createProfile(req.body); + res.status(httpStatus.CREATED).send(profile); +}); + +const getProfiles = catchAsync(async (req, res) => { + const filter = pick(req.query, ['name', 'role']); + const options = pick(req.query, ['sortBy', 'limit', 'page']); + const result = await profileService.queryProfiles(filter, options); + res.send(result); +}); + +const getProfile = catchAsync(async (req, res) => { + const profile = await profileService.getProfileById(req.params.profileId); + if (!profile) { + throw new ApiError(httpStatus.NOT_FOUND, 'Profile not found'); + } + res.send(profile); +}); + +const updateProfile = catchAsync(async (req, res) => { + const profile = await profileService.getProfileById(req.params.profileId, req.body); + res.send(profile); +}); + +const deleteProfile = catchAsync(async (req, res) => { + await profileService.deleteProfileById(req.params.profileId); + res.status(httpStatus.NO_CONTENT).send(); +}); + +module.exports = { + createProfile, + getProfiles, + getProfile, + updateProfile, + deleteProfile, +}; diff --git a/src/controllers/unit.controller.js b/src/controllers/unit.controller.js new file mode 100644 index 0000000..62fb1d2 --- /dev/null +++ b/src/controllers/unit.controller.js @@ -0,0 +1,43 @@ +const httpStatus = require('http-status'); +const pick = require('../utils/pick'); +const ApiError = require('../utils/ApiError'); +const catchAsync = require('../utils/catchAsync'); +const { unitService } = require('../services'); + +const createUnit = catchAsync(async (req, res) => { + const unit = await unitService.createUnit(req.body); + res.status(httpStatus.CREATED).send(unit); +}); + +const getUnits = catchAsync(async (req, res) => { + const filter = pick(req.query, ['name']); + const options = pick(req.query, ['sortBy', 'limit', 'page']); + const result = await unitService.queryUnits(filter, options); + res.send(result); +}); + +const getUnit = catchAsync(async (req, res) => { + const unit = await unitService.getUnitById(req.params.unitId); + if (!unit) { + throw new ApiError(httpStatus.NOT_FOUND, 'Unit not found'); + } + res.send(unit); +}); + +const updateUnit = catchAsync(async (req, res) => { + const unit = await unitService.updateUnitById(req.params.unitId, req.body); + res.send(unit); +}); + +const deleteUnit = catchAsync(async (req, res) => { + await unitService.deleteUnitById(req.params.unitId); + res.status(httpStatus.NO_CONTENT).send(); +}); + +module.exports = { + createUnit, + getUnits, + getUnit, + updateUnit, + deleteUnit, +}; diff --git a/src/models/activity.model.js b/src/models/activity.model.js index 57a593c..bb68ed7 100644 --- a/src/models/activity.model.js +++ b/src/models/activity.model.js @@ -1,11 +1,13 @@ const mongoose = require('mongoose'); -const { toJSON } = require('./plugins'); +const { toJSON, paginate } = require('./plugins'); const activitySchema = mongoose.Schema( { - type: { + name: { type: String, + unique: true, required: true, + trim: true, }, factor: { type: Number, @@ -19,6 +21,18 @@ const activitySchema = mongoose.Schema( // add plugin that converts mongoose to json activitySchema.plugin(toJSON); +activitySchema.plugin(paginate); + +/** + * Check if name is taken + * @param {string} name - The activity's name + * @param {ObjectId} [excludeActivityId] - The id of the activity to be excluded + * @returns {Promise} + */ +activitySchema.statics.isNameTaken = async function (name, excludeActivityId) { + const activity = await this.findOne({ name, _id: { $ne: excludeActivityId } }); + return !!activity; +}; /** * @typedef Activity diff --git a/src/models/index.js b/src/models/index.js index 35fd46b..32f2845 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -1,2 +1,5 @@ module.exports.Token = require('./token.model'); module.exports.User = require('./user.model'); +module.exports.Profile = require('./profile.model'); +module.exports.Activity = require('./activity.model'); +module.exports.Unit = require('./unit.model'); diff --git a/src/models/profile.model.js b/src/models/profile.model.js index e9b486b..40a5e89 100644 --- a/src/models/profile.model.js +++ b/src/models/profile.model.js @@ -1,5 +1,5 @@ const mongoose = require('mongoose'); -const { toJSON } = require('./plugins'); +const { toJSON, paginate } = require('./plugins'); const profileSchema = mongoose.Schema( { @@ -26,12 +26,12 @@ const profileSchema = mongoose.Schema( rateOfChange: { type: Number, }, - activity: { + activityId: { type: mongoose.SchemaTypes.ObjectId, ref: 'Activity', required: true, }, - user: { + userId: { type: mongoose.SchemaTypes.ObjectId, ref: 'User', required: true, @@ -44,6 +44,19 @@ const profileSchema = mongoose.Schema( // add plugin that converts mongoose to json profileSchema.plugin(toJSON); +profileSchema.plugin(paginate); + + +/** + * Check if user has profile + * @param {ObjectId} userId - The user's id + * @param {ObjectId} [excludeProfileId] - The id of the profile to be excluded + * @returns {Promise} + */ +profileSchema.statics.hasUser = async function (userId, excludeProfileId) { + const user = await this.findOne({ userId, _id: { $ne: excludeProfileId } }); + return !!user; +}; /** * @typedef Profile diff --git a/src/models/unit.model.js b/src/models/unit.model.js new file mode 100644 index 0000000..f192de1 --- /dev/null +++ b/src/models/unit.model.js @@ -0,0 +1,38 @@ +const mongoose = require('mongoose'); +const { toJSON, paginate } = require('./plugins'); + +const unitSchema = mongoose.Schema( + { + name: { + type: String, + unique: true, + required: true, + trim: true, + }, + }, + { + timestamps: true, + } +); + +// add plugin that converts mongoose to json +unitSchema.plugin(toJSON); +unitSchema.plugin(paginate); + +/** + * Check if name is taken + * @param {string} name - The unit's name + * @param {ObjectId} [excludeUnitId] - The id of the unit to be excluded + * @returns {Promise} + */ +unitSchema.statics.isNameTaken = async function (name, excludeUnitId) { + const activity = await this.findOne({ name, _id: { $ne: excludeUnitId } }); + return !!activity; +}; + +/** + * @typedef Unit + */ +const Unit = mongoose.model('Unit', unitSchema); + +module.exports = Unit; diff --git a/src/models/units.model.js b/src/models/units.model.js deleted file mode 100644 index 1da8e32..0000000 --- a/src/models/units.model.js +++ /dev/null @@ -1,26 +0,0 @@ -const mongoose = require('mongoose'); -const { toJSON } = require('./plugins'); - -const unitSchema = mongoose.Schema( - { - type: { - type: String, - unique: true, - required: true, - trim: true, - }, - }, - { - timestamps: true, - } -); - -// add plugin that converts mongoose to json -unitSchema.plugin(toJSON); - -/** - * @typedef Unit - */ -const Unit = mongoose.model('Unit', unitSchema); - -module.exports = Unit; diff --git a/src/models/user.model.js b/src/models/user.model.js index df87793..d7bdf00 100644 --- a/src/models/user.model.js +++ b/src/models/user.model.js @@ -38,7 +38,7 @@ const userSchema = mongoose.Schema( role: { type: String, enum: roles, - default: 'user', + default: 'admin', }, }, { diff --git a/src/routes/v1/activity.route.js b/src/routes/v1/activity.route.js new file mode 100644 index 0000000..03d8011 --- /dev/null +++ b/src/routes/v1/activity.route.js @@ -0,0 +1,231 @@ +const express = require('express'); +const auth = require('../../middlewares/auth'); +const validate = require('../../middlewares/validate'); +const activityValidation = require('../../validations/activity.validation'); +const activityController = require('../../controllers/activity.controller'); + +const router = express.Router(); + +router + .route('/') + .post(auth('manageActivities'), validate(activityValidation.createActivity), activityController.createActivity) + .get(auth('getActivities'), validate(activityValidation.getActivities), activityController.getActivities); + +router + .route('/:activityId') + .get(auth('getActivities'), validate(activityValidation.getActivity), activityController.getActivity) + .patch(auth('manageActivities'), validate(activityValidation.updateActivity), activityController.updateActivity) + .delete(auth('manageActivities'), validate(activityValidation.deleteActivity), activityController.deleteActivity); + +module.exports = router; + +/** + * @swagger + * tags: + * name: Activities + * description: Activity management + */ + +/** + * @swagger + * path: + * /activities: + * post: + * summary: Create a activity + * description: Only admins can create activities. + * tags: [Activities] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * - factor + * properties: + * name: + * type: string + * description: must be unique + * factor: + * type: number + * example: + * name: g + * factor: 1 + * responses: + * "201": + * description: Created + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Activity' + * "401": + * $ref: '#/components/responses/Unauthorized' + * "403": + * $ref: '#/components/responses/Forbidden' + * + * get: + * summary: Get all activities + * description: Only admins can retrieve all activities. + * tags: [Activities] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: name + * schema: + * type: string + * description: Activity name + * - in: query + * name: factor + * schema: + * type: number + * description: Activity factor + * - in: query + * name: sortBy + * schema: + * type: string + * description: sort by query in the form of field:desc/asc (ex. name:asc) + * - in: query + * name: limit + * schema: + * type: integer + * minimum: 1 + * default: 10 + * description: Maximum number of activities + * - in: query + * name: page + * schema: + * type: integer + * minimum: 1 + * default: 1 + * description: Page number + * responses: + * "200": + * description: OK + * content: + * application/json: + * schema: + * type: object + * properties: + * results: + * type: array + * items: + * $ref: '#/components/schemas/Activity' + * page: + * type: integer + * example: 1 + * limit: + * type: integer + * example: 10 + * totalPages: + * type: integer + * example: 1 + * totalResults: + * type: integer + * example: 1 + * "401": + * $ref: '#/components/responses/Unauthorized' + * "403": + * $ref: '#/components/responses/Forbidden' + */ + +/** + * @swagger + * path: + * /activities/{id}: + * get: + * summary: Get a activity + * description: Only admins can fetch activities. + * tags: [Users] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Activity id + * responses: + * "200": + * description: OK + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Activity' + * "401": + * $ref: '#/components/responses/Unauthorized' + * "403": + * $ref: '#/components/responses/Forbidden' + * "404": + * $ref: '#/components/responses/NotFound' + * + * patch: + * summary: Update a activity + * description: Only admins can update activities. + * tags: [Activities] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Activity id + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * description: must be unique + * factor: + * type: number + * example: + * name: normal + * factor: 1 + * responses: + * "200": + * description: OK + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Activity' + * "400": + * "401": + * $ref: '#/components/responses/Unauthorized' + * "403": + * $ref: '#/components/responses/Forbidden' + * "404": + * $ref: '#/components/responses/NotFound' + * + * delete: + * summary: Delete a activity + * description: Only admins can delete activities. + * tags: [Activities] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Activity id + * responses: + * "200": + * description: No content + * "401": + * $ref: '#/components/responses/Unauthorized' + * "403": + * $ref: '#/components/responses/Forbidden' + * "404": + * $ref: '#/components/responses/NotFound' + */ diff --git a/src/routes/v1/index.js b/src/routes/v1/index.js index 1b84c3b..2ca8db9 100644 --- a/src/routes/v1/index.js +++ b/src/routes/v1/index.js @@ -1,12 +1,18 @@ const express = require('express'); const authRoute = require('./auth.route'); const userRoute = require('./user.route'); +const profileRoute = require('./profile.route'); +const activityRoute = require('./activity.route'); +const unitRoute = require('./unit.route'); const docsRoute = require('./docs.route'); const router = express.Router(); router.use('/auth', authRoute); router.use('/users', userRoute); +router.use('/profiles', profileRoute); +router.use('/activities', activityRoute); +router.use('/units', unitRoute); router.use('/docs', docsRoute); module.exports = router; diff --git a/src/routes/v1/profile.route.js b/src/routes/v1/profile.route.js new file mode 100644 index 0000000..93dadb3 --- /dev/null +++ b/src/routes/v1/profile.route.js @@ -0,0 +1,254 @@ +const express = require('express'); +const auth = require('../../middlewares/auth'); +const validate = require('../../middlewares/validate'); +const profileValidation = require('../../validations/profile.validation'); +const profileController = require('../../controllers/profile.controller'); + +const router = express.Router(); + +router + .route('/') + .post(auth(), validate(profileValidation.createProfile), profileController.createProfile) + .get(auth('getProfiles'), validate(profileValidation.getProfiles), profileController.getProfiles); + +router + .route('/:profileId') + .get(auth('getProfiles'), validate(profileValidation.getProfile), profileController.getProfile) + .patch(auth('manageProfiles'), validate(profileValidation.updateProfile), profileController.updateProfile) + .delete(auth('manageProfiles'), validate(profileValidation.deleteProfile), profileController.deleteProfile); + +module.exports = router; + +/** + * @swagger + * tags: + * name: Profiles + * description: Profile management and retrieval + */ + +/** + * @swagger + * path: + * /profiles: + * post: + * summary: Create a user + * description: Only admins can create other users. + * tags: [Users] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * - email + * - password + * - role + * properties: + * name: + * type: string + * email: + * type: string + * format: email + * description: must be unique + * password: + * type: string + * format: password + * minLength: 8 + * description: At least one number and one letter + * role: + * type: string + * enum: [user, admin] + * example: + * name: fake name + * email: fake@example.com + * password: password1 + * role: user + * responses: + * "201": + * description: Created + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/User' + * "400": + * $ref: '#/components/responses/DuplicateEmail' + * "401": + * $ref: '#/components/responses/Unauthorized' + * "403": + * $ref: '#/components/responses/Forbidden' + * + * get: + * summary: Get all profiles + * description: Only admins can retrieve all profiles. + * tags: [Profiles] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: name + * schema: + * type: string + * description: User name + * - in: query + * name: role + * schema: + * type: string + * description: User role + * - in: query + * name: sortBy + * schema: + * type: string + * description: sort by query in the form of field:desc/asc (ex. name:asc) + * - in: query + * name: limit + * schema: + * type: integer + * minimum: 1 + * default: 10 + * description: Maximum number of users + * - in: query + * name: page + * schema: + * type: integer + * minimum: 1 + * default: 1 + * description: Page number + * responses: + * "200": + * description: OK + * content: + * application/json: + * schema: + * type: object + * properties: + * results: + * type: array + * items: + * $ref: '#/components/schemas/User' + * page: + * type: integer + * example: 1 + * limit: + * type: integer + * example: 10 + * totalPages: + * type: integer + * example: 1 + * totalResults: + * type: integer + * example: 1 + * "401": + * $ref: '#/components/responses/Unauthorized' + * "403": + * $ref: '#/components/responses/Forbidden' + */ + +/** + * @swagger + * path: + * /profiles/{id}: + * get: + * summary: Get a profile + * description: Logged in users can fetch only their own profile information. Only admins can fetch other profiles. + * tags: [Profiles] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: User id + * responses: + * "200": + * description: OK + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/User' + * "401": + * $ref: '#/components/responses/Unauthorized' + * "403": + * $ref: '#/components/responses/Forbidden' + * "404": + * $ref: '#/components/responses/NotFound' + * + * patch: + * summary: Update a profile + * description: Logged in users can only update their own information. Only admins can update other profiles. + * tags: [Profiles] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: User id + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * email: + * type: string + * format: email + * description: must be unique + * password: + * type: string + * format: password + * minLength: 8 + * description: At least one number and one letter + * example: + * name: fake name + * email: fake@example.com + * password: password1 + * responses: + * "200": + * description: OK + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/User' + * "400": + * $ref: '#/components/responses/DuplicateEmail' + * "401": + * $ref: '#/components/responses/Unauthorized' + * "403": + * $ref: '#/components/responses/Forbidden' + * "404": + * $ref: '#/components/responses/NotFound' + * + * delete: + * summary: Delete a profile + * description: Logged in users can delete only themselves. Only admins can delete other users. + * tags: [Users] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: User id + * responses: + * "200": + * description: No content + * "401": + * $ref: '#/components/responses/Unauthorized' + * "403": + * $ref: '#/components/responses/Forbidden' + * "404": + * $ref: '#/components/responses/NotFound' + */ diff --git a/src/routes/v1/unit.route.js b/src/routes/v1/unit.route.js new file mode 100644 index 0000000..00ecb94 --- /dev/null +++ b/src/routes/v1/unit.route.js @@ -0,0 +1,224 @@ +const express = require('express'); +const auth = require('../../middlewares/auth'); +const validate = require('../../middlewares/validate'); +const unitValidation = require('../../validations/unit.validation'); +const unitController = require('../../controllers/unit.controller'); + +const router = express.Router(); + +router + .route('/') + .post(auth('manageUnits'), validate(unitValidation.createUnit), unitController.createUnit) + .get(auth('getUnits'), validate(unitValidation.getUnits), unitController.getUnits); + +router + .route('/:activityId') + .get(auth('getUnits'), validate(unitValidation.getUnit), unitController.getUnit) + .patch(auth('manageUnits'), validate(unitValidation.updateUnit), unitController.updateUnit) + .delete(auth('manageUnits'), validate(unitValidation.deleteUnit), unitController.deleteUnit); + +module.exports = router; + +/** + * @swagger + * tags: + * name: Units + * description: Unit management + */ + +/** + * @swagger + * path: + * /units: + * post: + * summary: Create a unit + * description: Only admins can create units. + * tags: [Units] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * properties: + * name: + * type: string + * description: must be unique + * example: + * name: g + * responses: + * "201": + * description: Created + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Unit' + * "400": + * "401": + * $ref: '#/components/responses/Unauthorized' + * "403": + * $ref: '#/components/responses/Forbidden' + * + * get: + * summary: Get all units + * description: Only admins can retrieve all units. + * tags: [Units] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: name + * schema: + * type: string + * description: unit name + * - in: query + * name: name + * schema: + * type: string + * description: Unit name + * - in: query + * name: sortBy + * schema: + * type: string + * description: sort by query in the form of field:desc/asc (ex. name:asc) + * - in: query + * name: limit + * schema: + * type: integer + * minimum: 1 + * default: 10 + * description: Maximum number of units + * - in: query + * name: page + * schema: + * type: integer + * minimum: 1 + * default: 1 + * description: Page number + * responses: + * "200": + * description: OK + * content: + * application/json: + * schema: + * type: object + * properties: + * results: + * type: array + * items: + * $ref: '#/components/schemas/Unit' + * page: + * type: integer + * example: 1 + * limit: + * type: integer + * example: 10 + * totalPages: + * type: integer + * example: 1 + * totalResults: + * type: integer + * example: 1 + * "401": + * $ref: '#/components/responses/Unauthorized' + * "403": + * $ref: '#/components/responses/Forbidden' + */ + +/** + * @swagger + * path: + * /units/{id}: + * get: + * summary: Get a unit + * description: Logged in users and admins can fetch units. + * tags: [Units] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Unit id + * responses: + * "200": + * description: OK + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Unit' + * "401": + * $ref: '#/components/responses/Unauthorized' + * "403": + * $ref: '#/components/responses/Forbidden' + * "404": + * $ref: '#/components/responses/NotFound' + * + * patch: + * summary: Update a unit + * description: Only admins can update units. + * tags: [Users] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Unit id + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * description: must be unique + * example: + * name: g + * responses: + * "200": + * description: OK + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Unit' + * "401": + * $ref: '#/components/responses/Unauthorized' + * "403": + * $ref: '#/components/responses/Forbidden' + * "404": + * $ref: '#/components/responses/NotFound' + * + * delete: + * summary: Delete a unit + * description: Only admins can delete units. + * tags: [Users] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Unit id + * responses: + * "200": + * description: No content + * "401": + * $ref: '#/components/responses/Unauthorized' + * "403": + * $ref: '#/components/responses/Forbidden' + * "404": + * $ref: '#/components/responses/NotFound' + */ diff --git a/src/services/activity.service.js b/src/services/activity.service.js new file mode 100644 index 0000000..75617e3 --- /dev/null +++ b/src/services/activity.service.js @@ -0,0 +1,77 @@ +const httpStatus = require('http-status'); +const { Activity } = require('../models'); +const ApiError = require('../utils/ApiError'); + +/** + * Create a activity + * @param {Object} activityBody + * @returns {Promise} + */ +const createActivity = async (activityBody) => { + if (await Activity.isNameTaken(activityBody.name)) { + throw new ApiError(httpStatus.BAD_REQUEST, 'Name already taken'); + } + const activity = await Activity.create(activityBody); + return activity; +}; + +/** + * Query for activity + * @param {Object} filter - Mongo filter + * @param {Object} options - Query options + * @param {string} [options.sortBy] - Sort option in the format: sortField:(desc|asc) + * @param {number} [options.limit] - Maximum number of results per page (default = 10) + * @param {number} [options.page] - Current page (default = 1) + * @returns {Promise} + */ +const queryActivities = async (filter, options) => { + const activities = await Activity.paginate(filter, options); + return activities; +}; + +/** + * Get profile by id + * @param {ObjectId} id + * @returns {Promise} + */ +const getActivityById = async (id) => { + return Activity.findById(id); +}; + +/** + * Update activity by id + * @param {ObjectId} activityId + * @param {Object} updateBody + * @returns {Promise} + */ +const updateActivityById = async (activityId, updateBody) => { + const activity = await getActivityById(activityId); + if (!activity) { + throw new ApiError(httpStatus.NOT_FOUND, 'Activity not found'); + } + Object.assign(activity, updateBody); + await activity.save(); + return activity; +}; + +/** + * Delete activity by id + * @param {ObjectId} activityId + * @returns {Promise} + */ +const deleteActivityById = async (activityId) => { + const activity = await getActivityById(activityId); + if (!activity) { + throw new ApiError(httpStatus.NOT_FOUND, 'Activity not found'); + } + await activity.remove(); + return activity; +}; + +module.exports = { + createActivity, + queryActivities, + getActivityById, + updateActivityById, + deleteActivityById, +}; diff --git a/src/services/index.js b/src/services/index.js index 73547db..c8557d8 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -2,3 +2,6 @@ module.exports.authService = require('./auth.service'); module.exports.emailService = require('./email.service'); module.exports.tokenService = require('./token.service'); module.exports.userService = require('./user.service'); +module.exports.profileService = require('./profile.service'); +module.exports.activityService = require('./activity.service'); +module.exports.unitService = require('./unit.service'); diff --git a/src/services/profile.service.js b/src/services/profile.service.js new file mode 100644 index 0000000..e73ca5a --- /dev/null +++ b/src/services/profile.service.js @@ -0,0 +1,87 @@ +const httpStatus = require('http-status'); +const { Profile } = require('../models'); +const ApiError = require('../utils/ApiError'); + +/** + * Create a profile + * @param {Object} profileBody + * @returns {Promise} + */ +const createProfile = async (profileBody) => { + if (await Profile.hasUser(profileBody.userId)) { + throw new ApiError(httpStatus.BAD_REQUEST, 'User already has a profile'); + } + const profile = await Profile.create(profileBody); + return profile; +}; + +/** + * Query for profiles + * @param {Object} filter - Mongo filter + * @param {Object} options - Query options + * @param {string} [options.sortBy] - Sort option in the format: sortField:(desc|asc) + * @param {number} [options.limit] - Maximum number of results per page (default = 10) + * @param {number} [options.page] - Current page (default = 1) + * @returns {Promise} + */ +const queryProfiles = async (filter, options) => { + const profiles = await Profile.paginate(filter, options); + return profiles; +}; + +/** + * Get profile by id + * @param {ObjectId} id + * @returns {Promise} + */ +const getProfileById = async (id) => { + return Profile.findById(id); +}; + +/** + * Get profile by user id + * @param {ObjectId} id + * @returns {Promise} + */ +const getProfileByUserId = async (id) => { + return Profile.findOne({ user: id }); +}; + +/** + * Update profile by id + * @param {ObjectId} profileId + * @param {Object} updateBody + * @returns {Promise} + */ +const updateProfileById = async (profileId, updateBody) => { + const profile = await getProfileById(profileId); + if (!profile) { + throw new ApiError(httpStatus.NOT_FOUND, 'Profile not found'); + } + Object.assign(profile, updateBody); + await profile.save(); + return profile; +}; + +/** + * Delete profile by id + * @param {ObjectId} profileId + * @returns {Promise} + */ +const deleteProfileById = async (profileId) => { + const profile = await getProfileById(profileId); + if (!profile) { + throw new ApiError(httpStatus.NOT_FOUND, 'Profile not found'); + } + await profile.remove(); + return profile; +}; + +module.exports = { + createProfile, + queryProfiles, + getProfileById, + getProfileByUserId, + updateProfileById, + deleteProfileById, +}; diff --git a/src/services/unit.service.js b/src/services/unit.service.js new file mode 100644 index 0000000..fcc06bb --- /dev/null +++ b/src/services/unit.service.js @@ -0,0 +1,77 @@ +const httpStatus = require('http-status'); +const { Unit } = require('../models'); +const ApiError = require('../utils/ApiError'); + +/** + * Create a unit + * @param {Object} unitBody + * @returns {Promise} + */ +const createUnit = async (unitBody) => { + if (await Unit.isNameTaken(unitBody.name)) { + throw new ApiError(httpStatus.BAD_REQUEST, 'Name already taken'); + } + const unit = await Unit.create(unitBody); + return unit; +}; + +/** + * Query for unit + * @param {Object} filter - Mongo filter + * @param {Object} options - Query options + * @param {string} [options.sortBy] - Sort option in the format: sortField:(desc|asc) + * @param {number} [options.limit] - Maximum number of results per page (default = 10) + * @param {number} [options.page] - Current page (default = 1) + * @returns {Promise} + */ +const queryUnits = async (filter, options) => { + const units = await Unit.paginate(filter, options); + return units; +}; + +/** + * Get unit by id + * @param {ObjectId} id + * @returns {Promise} + */ +const getUnitById = async (id) => { + return Unit.findById(id); +}; + +/** + * Update unit by id + * @param {ObjectId} unitId + * @param {Object} updateBody + * @returns {Promise} + */ +const updateUnitById = async (unitId, updateBody) => { + const unit = await getUnitById(unitId); + if (!unit) { + throw new ApiError(httpStatus.NOT_FOUND, 'Unit not found'); + } + Object.assign(unit, updateBody); + await Unit.save(); + return unit; +}; + +/** + * Delete unit by id + * @param {ObjectId} unitId + * @returns {Promise} + */ +const deleteUnitById = async (unitId) => { + const unit = await getUnitById(unitId); + if (!unit) { + throw new ApiError(httpStatus.NOT_FOUND, 'Unit not found'); + } + await Unit.remove(); + return unit; +}; + +module.exports = { + createUnit, + queryUnits, + getUnitById, + updateUnitById, + deleteUnitById, +}; diff --git a/src/validations/activity.validation.js b/src/validations/activity.validation.js new file mode 100644 index 0000000..f8ba6fe --- /dev/null +++ b/src/validations/activity.validation.js @@ -0,0 +1,50 @@ +const Joi = require('joi'); +const { objectId } = require('./custom.validation'); + +const createActivity = { + body: Joi.object().keys({ + name: Joi.string().required(), + factor: Joi.number().required(), + }), +}; + +const getActivities = { + query: Joi.object().keys({ + name: Joi.string(), + factor: Joi.number(), + limit: Joi.number().integer(), + page: Joi.number().integer(), + }), +}; + +const getActivity = { + params: Joi.object().keys({ + activityId: Joi.string().custom(objectId), + }), +}; + +const updateActivity = { + params: Joi.object().keys({ + activityId: Joi.string().custom(objectId), + }), + body: Joi.object() + .keys({ + name: Joi.string().required(), + factor: Joi.number().required(), + }) + .min(1), +}; + +const deleteActivity = { + params: Joi.object().keys({ + profileId: Joi.string().custom(objectId), + }), +}; + +module.exports = { + createActivity, + getActivities, + getActivity, + updateActivity, + deleteActivity, +}; diff --git a/src/validations/index.js b/src/validations/index.js index 16d6baa..327ce5a 100644 --- a/src/validations/index.js +++ b/src/validations/index.js @@ -1,2 +1,5 @@ module.exports.authValidation = require('./auth.validation'); module.exports.userValidation = require('./user.validation'); +module.exports.profileValidation = require('./profile.validation'); +module.exports.activityValidation = require('./activity.validation'); +module.exports.unitValidation = require('./unit.validation'); diff --git a/src/validations/profile.validation.js b/src/validations/profile.validation.js new file mode 100644 index 0000000..2fb10fb --- /dev/null +++ b/src/validations/profile.validation.js @@ -0,0 +1,60 @@ +const Joi = require('joi'); +const { objectId } = require('./custom.validation'); + +const createProfile = { + body: Joi.object().keys({ + sex: Joi.string().required().valid('male', 'female'), + birthday: Joi.date().required(), + height: Joi.number().required(), + currentWeight: Joi.number().required(), + targetWeight: Joi.number(), + rateOfChange: Joi.number(), + activityId: Joi.string().custom(objectId).required(), + }), +}; + +const getProfiles = { + query: Joi.object().keys({ + sex: Joi.string(), + sortBy: Joi.string(), + limit: Joi.number().integer(), + page: Joi.number().integer(), + }), +}; + +const getProfile = { + params: Joi.object().keys({ + profileId: Joi.string().custom(objectId), + }), +}; + +const updateProfile = { + params: Joi.object().keys({ + profileId: Joi.required().custom(objectId), + }), + body: Joi.object() + .keys({ + sex: Joi.string().required().valid('male', 'female'), + birthday: Joi.date().required(), + height: Joi.number().required(), + currentWeight: Joi.number().required(), + targetWeight: Joi.number(), + rateOfChange: Joi.number(), + activityId: Joi.string().custom(objectId).required(), + }) + .min(1), +}; + +const deleteProfile = { + params: Joi.object().keys({ + profileId: Joi.string().custom(objectId).required(), + }), +}; + +module.exports = { + createProfile, + getProfiles, + getProfile, + updateProfile, + deleteProfile, +}; diff --git a/src/validations/unit.validation.js b/src/validations/unit.validation.js new file mode 100644 index 0000000..e1f7a42 --- /dev/null +++ b/src/validations/unit.validation.js @@ -0,0 +1,47 @@ +const Joi = require('joi'); +const { objectId } = require('./custom.validation'); + +const createUnit = { + body: Joi.object().keys({ + name: Joi.string().required(), + }), +}; + +const getUnits = { + query: Joi.object().keys({ + name: Joi.string(), + limit: Joi.number().integer(), + page: Joi.number().integer(), + }), +}; + +const getUnit = { + params: Joi.object().keys({ + unitId: Joi.string().custom(objectId), + }), +}; + +const updateUnit = { + params: Joi.object().keys({ + unitId: Joi.string().custom(objectId), + }), + body: Joi.object() + .keys({ + name: Joi.string().required(), + }) + .min(1), +}; + +const deleteUnit = { + params: Joi.object().keys({ + unitId: Joi.string().custom(objectId), + }), +}; + +module.exports = { + createUnit, + getUnits, + getUnit, + updateUnit, + deleteUnit, +};