From 6e5ba717dd0fe3696de7395b5ecd8ce8237a8c76 Mon Sep 17 00:00:00 2001 From: Bernardo Vieira Date: Fri, 21 Jul 2023 17:00:57 +0100 Subject: [PATCH 1/2] fix get notifications + update endpoints --- packages/api/src/controllers/v2/user.ts | 11 ++- packages/api/src/routes/v2/user.ts | 81 +++++++------------- packages/api/src/validators/user.ts | 32 +++++++- packages/core/src/services/app/user/index.ts | 75 +++++++++++++----- 4 files changed, 123 insertions(+), 76 deletions(-) diff --git a/packages/api/src/controllers/v2/user.ts b/packages/api/src/controllers/v2/user.ts index 607a52af3..720a6fa23 100644 --- a/packages/api/src/controllers/v2/user.ts +++ b/packages/api/src/controllers/v2/user.ts @@ -2,8 +2,10 @@ import { Request, Response } from 'express'; import { getAddress } from '@ethersproject/address'; import { services } from '@impactmarket/core'; -import { RequestWithUser } from '../../middlewares/core'; -import { standardResponse } from '../../utils/api'; +import { ListUserNotificationsRequestSchema } from '~validators/user'; +import { RequestWithUser } from '~middlewares/core'; +import { ValidatedRequest } from '~utils/queryValidator'; +import { standardResponse } from '~utils/api'; class UserController { private userService: services.app.UserServiceV2; @@ -281,7 +283,10 @@ class UserController { .catch(e => standardResponse(res, 400, false, '', { error: e })); }; - public getNotifications = (req: RequestWithUser, res: Response) => { + public getNotifications = ( + req: RequestWithUser & ValidatedRequest, + res: Response + ) => { if (req.user === undefined) { standardResponse(res, 401, false, '', { error: { diff --git a/packages/api/src/routes/v2/user.ts b/packages/api/src/routes/v2/user.ts index 29ef884c9..b00ce05cb 100644 --- a/packages/api/src/routes/v2/user.ts +++ b/packages/api/src/routes/v2/user.ts @@ -1,9 +1,16 @@ import { Router } from 'express'; import timeout from 'connect-timeout'; -import { adminAuthentication, authenticateToken, verifySignature } from '../../middlewares'; -import UserController from '../../controllers/v2/user'; -import userValidators from '../../validators/user'; +import { adminAuthentication, authenticateToken, verifySignature } from '~middlewares/index'; +import { + queryListUserNotificationsValidator, + create as userCreate, + readNotifications as userReadNotifications, + report as userReport, + sendPushNotifications as userSendPushNotifications, + update as userUpdate +} from '~validators/user'; +import UserController from '~controllers/v2/user'; export default (app: Router): void => { const route = Router(); @@ -79,7 +86,7 @@ export default (app: Router): void => { * "403": * description: "Invalid input" */ - route.post('/', userValidators.create, userController.create); + route.post('/', userCreate, userController.create); /** * @swagger @@ -171,7 +178,7 @@ export default (app: Router): void => { * - SignatureMessage: [] * - Signature: [] */ - route.put('/', authenticateToken, verifySignature, userValidators.update, userController.update); + route.put('/', authenticateToken, verifySignature, userUpdate, userController.update); /** * @swagger @@ -254,7 +261,7 @@ export default (app: Router): void => { * security: * - BearerToken: [] */ - route.post('/report', authenticateToken, userValidators.report, userController.report); + route.post('/report', authenticateToken, userReport, userController.report); /** * @swagger @@ -347,43 +354,6 @@ export default (app: Router): void => { */ route.get('/presigned/:query?', authenticateToken, timeout('3s'), userController.getPresignedUrlMedia); - /** - * @swagger - * - * /users/notifications/unread: - * get: - * tags: - * - "users" - * summary: Get the number of unread notifications from a user - * parameters: - * - in: query - * name: isWebApp - * schema: - * type: boolean - * required: false - * - in: query - * name: isWallet - * schema: - * type: boolean - * required: false - * responses: - * "200": - * description: OK - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * data: - * type: integer - * description: number of unread notifications - * security: - * - BearerToken: [] - */ - route.get('/notifications/unread/:query?', authenticateToken, timeout('3s'), userController.getUnreadNotifications); - /** * @swagger * @@ -415,6 +385,12 @@ export default (app: Router): void => { * schema: * type: boolean * required: false + * - in: query + * name: unreadOnly + * schema: + * type: boolean + * required: false + * description: filter by unread notifications only. If undefined, it will return all notifications * responses: * "200": * description: OK @@ -430,12 +406,18 @@ export default (app: Router): void => { * security: * - BearerToken: [] */ - route.get('/notifications/:query?', authenticateToken, timeout('3s'), userController.getNotifications); + route.get( + '/notifications/:query?', + authenticateToken, + queryListUserNotificationsValidator, + timeout('3s'), + userController.getNotifications + ); /** * @swagger * - * /users/notifications/read: + * /users/notifications: * put: * tags: * - "users" @@ -467,12 +449,7 @@ export default (app: Router): void => { * security: * - BearerToken: [] */ - route.put( - '/notifications/read', - authenticateToken, - userValidators.readNotifications, - userController.readNotifications - ); + route.put('/notifications', authenticateToken, userReadNotifications, userController.readNotifications); /** * @swagger @@ -502,7 +479,7 @@ export default (app: Router): void => { route.post( '/push-notifications', adminAuthentication, - userValidators.sendPushNotifications, + userSendPushNotifications, userController.sendPushNotifications ); }; diff --git a/packages/api/src/validators/user.ts b/packages/api/src/validators/user.ts index d95408e09..7e92d5f43 100644 --- a/packages/api/src/validators/user.ts +++ b/packages/api/src/validators/user.ts @@ -1,6 +1,10 @@ import { Joi, celebrate } from 'celebrate'; +import { ContainerTypes, ValidatedRequestSchema, createValidator } from '~utils/queryValidator'; import { defaultSchema } from './defaultSchema'; +import config from '~config/index'; + +const validator = createValidator(); const create = celebrate({ body: defaultSchema.object({ @@ -68,10 +72,34 @@ const sendPushNotifications = celebrate({ }) }); -export default { +type ListUserNotificationsType = { + offset?: number; + limit?: number; + unreadOnly?: boolean; + isWallet?: boolean; + isWebApp?: boolean; +}; + +const queryListUserNotificationsSchema = defaultSchema.object({ + offset: Joi.number().optional().default(0), + limit: Joi.number().optional().max(20).default(config.defaultLimit), + unreadOnly: Joi.boolean().optional(), + isWallet: Joi.boolean().optional(), + isWebApp: Joi.boolean().optional() +}); + +interface ListUserNotificationsRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Query]: ListUserNotificationsType; +} + +const queryListUserNotificationsValidator = validator.query(queryListUserNotificationsSchema); + +export { create, update, report, readNotifications, - sendPushNotifications + sendPushNotifications, + queryListUserNotificationsValidator, + ListUserNotificationsRequestSchema }; diff --git a/packages/core/src/services/app/user/index.ts b/packages/core/src/services/app/user/index.ts index 0d1b84213..90eee54c1 100644 --- a/packages/core/src/services/app/user/index.ts +++ b/packages/core/src/services/app/user/index.ts @@ -1,4 +1,4 @@ -import { Op } from 'sequelize'; +import { Op, WhereOptions } from 'sequelize'; import { ethers } from 'ethers'; import { getAddress } from '@ethersproject/address'; @@ -335,33 +335,70 @@ export default class UserService { public async getNotifications( query: { - offset?: string; - limit?: string; - isWallet?: string; - isWebApp?: string; + offset?: number; + limit?: number; + unreadOnly?: boolean; + isWallet?: boolean; + isWebApp?: boolean; }, userId: number ): Promise<{ count: number; rows: AppNotification[]; }> { - const notifications = await models.appNotification.findAndCountAll({ - where: { - userId, - isWebApp: query.isWebApp === 'true', - isWallet: query.isWallet === 'true' - }, - offset: query.offset ? parseInt(query.offset, 10) : config.defaultOffset, - limit: query.limit ? parseInt(query.limit, 10) : config.defaultLimit, - order: [['createdAt', 'DESC']] - }); + const { isWallet, isWebApp, unreadOnly, offset, limit } = query; + let where: WhereOptions = { userId }; + if (isWebApp !== undefined) { + where = { + ...where, + isWebApp + }; + } + if (isWallet !== undefined) { + where = { + ...where, + isWallet + }; + } + + let count = 0; + let rows: AppNotification[] = []; + + if (unreadOnly !== undefined) { + const notifications = await models.appNotification.findAndCountAll({ + where: { + ...where, + read: !unreadOnly + }, + offset, + limit, + order: [['createdAt', 'DESC']] + }); + + count = notifications.count; + rows = notifications.rows as AppNotification[]; + } else { + count = await models.appNotification.count({ + where: { + ...where, + read: false + } + }); + rows = await models.appNotification.findAll({ + where, + offset, + limit, + order: [['createdAt', 'DESC']] + }); + } + return { - count: notifications.count, - rows: notifications.rows as AppNotification[] + count, + rows }; } - public async readNotifications(userId: number, notifications?: number[]): Promise { + public async readNotifications(userId: number, notificationsId: number[]): Promise { const updated = await models.appNotification.update( { read: true @@ -371,7 +408,7 @@ export default class UserService { where: { userId, id: { - [Op.in]: notifications + [Op.in]: notificationsId } } } From b77254ef17316600ed67a2864887857a8100ce91 Mon Sep 17 00:00:00 2001 From: Bernardo Vieira Date: Fri, 21 Jul 2023 17:34:54 +0100 Subject: [PATCH 2/2] fix tests --- packages/core/tests/integration/v2/user.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/tests/integration/v2/user.test.ts b/packages/core/tests/integration/v2/user.test.ts index e1a8897a0..11711af89 100644 --- a/packages/core/tests/integration/v2/user.test.ts +++ b/packages/core/tests/integration/v2/user.test.ts @@ -566,7 +566,7 @@ describe('user service v2', () => { const notifications = await userService.getNotifications( { - isWebApp: 'true' + isWebApp: true }, user.id ); @@ -585,7 +585,7 @@ describe('user service v2', () => { }); const notifications = await userService.getNotifications( { - isWebApp: 'true' + isWebApp: true }, user.id );