diff --git a/.circleci/config.yml b/.circleci/config.yml
index 8b2a88c..2297526 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -102,7 +102,7 @@ workflows:
context : org-global
filters:
branches:
- only: [dev, 'feature/general-purpose-notifications-usage']
+ only: [dev, 'hotfix/V5-API-Standards']
- "build-prod":
context : org-global
filters:
diff --git a/config/default.js b/config/default.js
index 750802c..bbcc8c5 100644
--- a/config/default.js
+++ b/config/default.js
@@ -32,6 +32,7 @@ module.exports = {
TC_API_V4_BASE_URL: process.env.TC_API_V4_BASE_URL || '',
TC_API_V5_BASE_URL: process.env.TC_API_V5_BASE_URL || '',
API_CONTEXT_PATH: process.env.API_CONTEXT_PATH || '/v5/notifications',
+ TC_API_BASE_URL: process.env.TC_API_BASE_URL || '',
// Configuration for generating machine to machine auth0 token.
// The token will be used for calling another internal API.
@@ -45,22 +46,23 @@ module.exports = {
AUTH0_PROXY_SERVER_URL: process.env.AUTH0_PROXY_SERVER_URL,
KAFKA_CONSUMER_RULESETS: {
- // key is Kafka topic name, value is array of ruleset which have key as handler function name defined in src/processors/index.js
+ // key is Kafka topic name, value is array of ruleset which have key as
+ // handler function name defined in src/processors/index.js
'challenge.notification.events': [
{
handleChallenge: /** topic handler name */
{
type: 'UPDATE_DRAFT_CHALLENGE',
- roles: ["Submitter" /** Competitor */, "Copilot", "Reviewer"],
+ roles: ['Submitter' /** Competitor */, 'Copilot', 'Reviewer'],
notification:
{
id: 0, /** challengeid or projectid */
name: '', /** challenge name */
group: 'Challenge',
- title: 'Challenge specification is modified.'
- }
- }
- }
+ title: 'Challenge specification is modified.',
+ },
+ },
+ },
],
'notifications.autopilot.events': [
{
@@ -68,33 +70,33 @@ module.exports = {
{
phaseTypeName: 'Checkpoint Screening',
state: 'START',
- roles: ["Copilot", "Reviewer"],
+ roles: ['Copilot', 'Reviewer'],
notification:
{
id: 0, /** challengeid or projectid */
name: '', /** challenge name */
group: 'Challenge',
- title: 'Challenge checkpoint review.'
- }
- }
- }
+ title: 'Challenge checkpoint review.',
+ },
+ },
+ },
],
'submission.notification.create': [
{
handleSubmission:
{
resource: 'submission',
- roles: ["Copilot", "Reviewer"],
+ roles: ['Copilot', 'Reviewer'],
selfOnly: true /** Submitter only */,
notification:
{
id: 0, /** challengeid or projectid */
name: '', /** challenge name */
group: 'Submission',
- title: 'A new submission is uploaded.'
- }
- }
- }
+ title: 'A new submission is uploaded.',
+ },
+ },
+ },
],
//'notifications.community.challenge.created': ['handleChallengeCreated'],
//'notifications.community.challenge.phasewarning': ['handleChallengePhaseWarning'],
diff --git a/connect/connectNotificationServer.js b/connect/connectNotificationServer.js
index 8bcbc3a..4cc98be 100644
--- a/connect/connectNotificationServer.js
+++ b/connect/connectNotificationServer.js
@@ -9,6 +9,7 @@ const config = require('./config');
const notificationServer = require('../index');
const _ = require('lodash');
const service = require('./service');
+const helpers = require('./helpers');
const { BUS_API_EVENT } = require('./constants');
const EVENTS = require('./events-config').EVENTS;
const PROJECT_ROLE_RULES = require('./events-config').PROJECT_ROLE_RULES;
@@ -85,27 +86,28 @@ const getNotificationsForMentionedUser = (logger, eventConfig, content) => {
// only one per userHandle
notifications = _.uniqBy(notifications, 'userHandle');
- return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars
- const handles = _.map(notifications, 'userHandle');
- if (handles.length > 0) {
- service.getUsersByHandle(handles).then((users) => {
- _.forEach(notifications, (notification) => {
- const mentionedUser = _.find(users, { handle: notification.userHandle });
- notification.userId = mentionedUser ? mentionedUser.userId.toString() : notification.userHandle;
- });
- resolve(notifications);
- }).catch((error) => {
- if (logger) {
- logger.error(error);
- logger.info('Unable to send notification to mentioned user')
+ const handles = _.map(notifications, 'userHandle');
+ if (handles.length > 0) {
+ return service.getUsersByHandle(handles).then((users) => {
+ _.forEach(notifications, (notification) => {
+ const mentionedUser = _.find(users, { handle: notification.userHandle });
+ notification.userId = mentionedUser ? mentionedUser.userId.toString() : null;
+ if (!notification.userId && logger) {// such notifications would be discarded later after aggregation
+ logger.info(`Unable to find user with handle ${notification.userHandle}`);
}
- //resolves with empty notification which essentially means we are unable to send notification to mentioned user
- resolve([]);
});
- } else {
- resolve([]);
- }
- });
+ return Promise.resolve(notifications);
+ }).catch((error) => {
+ if (logger) {
+ logger.error(error);
+ logger.info('Unable to send notification to mentioned user');
+ }
+ //resolves with empty notification which essentially means we are unable to send notification to mentioned user
+ return Promise.resolve([]);
+ });
+ } else {
+ return Promise.resolve([]);
+ }
};
/**
@@ -150,7 +152,7 @@ const getProjectMembersNotifications = (eventConfig, project) => {
return Promise.resolve([]);
}
- return new Promise((resolve) => {
+ return Promise.promisify((callback) => {
let notifications = [];
const projectMembers = _.get(project, 'members', []);
@@ -185,8 +187,8 @@ const getProjectMembersNotifications = (eventConfig, project) => {
// only one per userId
notifications = _.uniqBy(notifications, 'userId');
- resolve(notifications);
- });
+ callback(null, notifications);
+ })();
};
/**
@@ -254,6 +256,83 @@ const getNotificationsForTopicStarter = (eventConfig, topicId) => {
});
};
+/**
+ * Filter members by project roles
+ *
+ * @params {Array} List of project roles
+ * @params {Array} List of project members
+ *
+ * @returns {Array} List of objects with user ids
+ */
+const filterMembersByRoles = (roles, members) => {
+ let result = [];
+
+ roles.forEach(projectRole => {
+ result = result.concat(
+ _.filter(members, PROJECT_ROLE_RULES[projectRole])
+ .map(projectMember => ({
+ userId: projectMember.userId.toString(),
+ }))
+ );
+ });
+
+ return result;
+};
+
+/**
+ * Exclude private posts notification
+ *
+ * @param {Object} eventConfig event configuration
+ * @param {Object} project project details
+ * @param {Array} tags list of message tags
+ *
+ * @return {Promise} resolves to a list of notifications
+ */
+const getExcludedPrivatePostNotifications = (eventConfig, project, tags) => {
+ // skip if message is not private or exclusion rule is not configured
+ if (!_.includes(tags, 'MESSAGES') || !eventConfig.privatePostsForProjectRoles) {
+ return Promise.resolve([]);
+ }
+
+ const members = _.get(project, 'members', []);
+ const notifications = filterMembersByRoles(eventConfig.privatePostsForProjectRoles, members);
+
+ return Promise.resolve(notifications);
+};
+
+/**
+ * Exclude notifications about posts inside draft phases
+ *
+ * @param {Object} eventConfig event configuration
+ * @param {Object} project project details
+ * @param {Array} tags list of message tags
+ *
+ * @return {Promise} resolves to a list of notifications
+ */
+const getExcludeDraftPhasesNotifications = (eventConfig, project, tags) => {
+ // skip is no exclusion rule is configured
+ if (!eventConfig.draftPhasesForProjectRoles) {
+ return Promise.resolve([]);
+ }
+
+ const phaseId = helpers.extractPhaseId(tags);
+ // skip if it is not phase notification
+ if (!phaseId) {
+ return Promise.resolve([]);
+ }
+
+ // exclude all user with configured roles if phase is in draft state
+ return service.getPhase(project.id, phaseId)
+ .then((phase) => {
+ if (phase.status === 'draft') {
+ const members = _.get(project, 'members', []);
+ const notifications = filterMembersByRoles(eventConfig.draftPhasesForProjectRoles, members);
+
+ return Promise.resolve(notifications);
+ }
+ });
+};
+
/**
* Exclude notifications using exclude rules of the event config
*
@@ -281,12 +360,17 @@ const excludeNotifications = (logger, notifications, eventConfig, message, data)
// and after filter out such notifications from the notifications list
// TODO move this promise all together with `_.uniqBy` to one function
// and reuse it here and in `handler` function
+ const tags = _.get(message, 'tags', []);
+
return Promise.all([
getNotificationsForTopicStarter(excludeEventConfig, message.topicId),
getNotificationsForUserId(excludeEventConfig, message.userId),
getNotificationsForMentionedUser(logger, excludeEventConfig, message.postContent),
getProjectMembersNotifications(excludeEventConfig, project),
getTopCoderMembersNotifications(excludeEventConfig),
+ // these are special exclude rules which are only working for excluding notifications but not including
+ getExcludedPrivatePostNotifications(excludeEventConfig, project, tags),
+ getExcludeDraftPhasesNotifications(excludeEventConfig, project, tags),
]).then((notificationsPerSource) => (
_.uniqBy(_.flatten(notificationsPerSource), 'userId')
)).then((excludedNotifications) => {
@@ -332,10 +416,10 @@ const handler = (topic, message, logger, callback) => {
}
// get project details
- service.getProject(projectId).then(project => {
+ return service.getProject(projectId).then(project => {
let allNotifications = [];
- Promise.all([
+ return Promise.all([
// the order in this list defines the priority of notification for the SAME user
// upper in this list - higher priority
// NOTE: always add all handles here, they have to check by themselves:
@@ -357,7 +441,7 @@ const handler = (topic, message, logger, callback) => {
project,
})
)).then((notifications) => {
- allNotifications = _.filter(notifications, notification => notification.userId !== `${message.initiatorUserId}`);
+ allNotifications = _.filter(notifications, n => n.userId && n.userId !== `${message.initiatorUserId}`);
if (eventConfig.includeUsers && message[eventConfig.includeUsers] &&
message[eventConfig.includeUsers].length > 0) {
diff --git a/connect/events-config.js b/connect/events-config.js
index acf92bc..460cd47 100644
--- a/connect/events-config.js
+++ b/connect/events-config.js
@@ -5,6 +5,7 @@ const { BUS_API_EVENT } = require('./constants');
// project member role names
const PROJECT_ROLE_OWNER = 'owner';
+const PROJECT_ROLE_CUSTOMER = 'customer';
const PROJECT_ROLE_COPILOT = 'copilot';
const PROJECT_ROLE_MANAGER = 'manager';
const PROJECT_ROLE_MEMBER = 'member';
@@ -13,6 +14,7 @@ const PROJECT_ROLE_ACCOUNT_MANAGER = 'account_manager';
// project member role rules
const PROJECT_ROLE_RULES = {
[PROJECT_ROLE_OWNER]: { role: 'customer', isPrimary: true },
+ [PROJECT_ROLE_CUSTOMER]: { role: 'customer' },
[PROJECT_ROLE_COPILOT]: { role: 'copilot' },
[PROJECT_ROLE_MANAGER]: { role: 'manager' },
[PROJECT_ROLE_ACCOUNT_MANAGER]: { role: 'account_manager' },
@@ -20,6 +22,7 @@ const PROJECT_ROLE_RULES = {
};
// TopCoder roles
+// eslint-disable-next-line no-unused-vars
const ROLE_CONNECT_COPILOT = 'Connect Copilot';
const ROLE_CONNECT_MANAGER = 'Connect Manager';
const ROLE_CONNECT_COPILOT_MANAGER = 'Connect Copilot Manager';
@@ -123,31 +126,53 @@ const EVENTS = [
version: 2,
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
toMentionedUsers: true,
+ exclude: {
+ privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
+ },
}, {
type: BUS_API_EVENT.CONNECT.POST.CREATED,
version: 2,
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
toTopicStarter: true,
toMentionedUsers: true,
+ exclude: {
+ draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER],
+ privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
+ },
}, {
type: BUS_API_EVENT.CONNECT.POST.UPDATED,
version: 2,
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
toTopicStarter: true,
toMentionedUsers: true,
+ exclude: {
+ draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER],
+ privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
+ },
}, {
type: BUS_API_EVENT.CONNECT.POST.MENTION,
+ exclude: {
+ draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER],
+ privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
+ },
},
{
type: BUS_API_EVENT.CONNECT.TOPIC.DELETED,
version: 2,
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
toTopicStarter: false,
+ exclude: {
+ privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
+ },
},
{
type: BUS_API_EVENT.CONNECT.POST.DELETED,
version: 2,
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
+ exclude: {
+ draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER],
+ privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
+ },
},
{
type: BUS_API_EVENT.CONNECT.PROJECT.LINK_CREATED,
diff --git a/connect/helpers.js b/connect/helpers.js
index 3092323..94691fd 100644
--- a/connect/helpers.js
+++ b/connect/helpers.js
@@ -2,6 +2,9 @@
* Helper functions
*/
const Remarkable = require('remarkable');
+const _ = require('lodash');
+
+const PHASE_ID_REGEXP = /phase#(\d+)/;
/**
* Convert markdown into raw draftjs state
@@ -42,7 +45,20 @@ const sanitizeEmail = (email) => {
return '';
};
+/**
+ * Helper method to extract phaseId from tag
+ *
+ * @param {Array} tags list of message tags
+ *
+ * @returns {String} phase id
+ */
+const extractPhaseId = (tags) => {
+ const phaseIds = tags.map((tag) => _.get(tag.match(PHASE_ID_REGEXP), '1', null));
+ return _.find(phaseIds, (phaseId) => phaseId !== null);
+};
+
module.exports = {
markdownToHTML,
sanitizeEmail,
+ extractPhaseId,
};
diff --git a/connect/notificationServices/email.js b/connect/notificationServices/email.js
index 40a64a5..cdcdaa2 100644
--- a/connect/notificationServices/email.js
+++ b/connect/notificationServices/email.js
@@ -4,8 +4,6 @@
const _ = require('lodash');
const jwt = require('jsonwebtoken');
const co = require('co');
-const fs = require('fs');
-const path = require('path');
const { logger, busService, eventScheduler, notificationService } = require('../../index');
const { createEventScheduler, SCHEDULED_EVENT_STATUS } = eventScheduler;
@@ -16,29 +14,43 @@ const {
SETTINGS_EMAIL_SERVICE_ID,
ACTIVE_USER_STATUSES,
} = require('../constants');
-const { EVENTS, EVENT_BUNDLES } = require('../events-config');
+const { EVENT_BUNDLES } = require('../events-config');
const helpers = require('../helpers');
const service = require('../service');
function replacePlaceholders(term, data) {
- let placeholders = term.match(/<[a-zA-Z]+>/g);
+ const placeholders = term.match(/<[a-zA-Z]+>/g);
let ret = term;
if (placeholders && placeholders.length) {
_(placeholders).each(p => {
- let values = _.map(data, p.slice(1, -1));
- const total = values.length;
- let replacement = values.length < 3 ?
- values.join(', ') :
- values.slice(0, 2).join(', ') + ' and ' + (total - 3) + 'others';
+ const values = _.map(data, p.slice(1, -1));
+ // TODO remove this code if possible.
+ // This code appears to be not in use causing lint errors.
+ // For now I'm commenting it, in case it contains some valuable logic.
+ // But after confirmation that it's redundant it has to be removed.
+ //
+ // const total = values.length;
+ // const replacement = values.length < 3 ?
+ // values.join(', ') :
+ // values.slice(0, 2).join(', ') + ' and ' + (total - 3) + 'others';
ret = ret.replace(p, values.join(', '));
});
}
return ret;
}
+function getEventGroupKey(value) {
+ const key = _.chain(EVENT_BUNDLES)
+ .keys()
+ .find(k => _.includes(_.get(EVENT_BUNDLES, `${k}.types`), _.get(value, 'data.data.type')))
+ .value();
+ if (!key) return 'DEFAULT';
+ return key;
+}
+
function getSections(projectUserEvents) {
- let sections = [];
+ const sections = [];
_.chain(projectUserEvents)
.groupBy(value => getEventGroupKey(value))
.forIn((value, key) => {
@@ -49,7 +61,7 @@ function getSections(projectUserEvents) {
notifications: _(value).map(v => v.data.data).value(),
});
} else {
- _.chain(value).groupBy(n => n.data.data[EVENT_BUNDLES[key].groupBy]).forIn((groupValue, groupKey) => {
+ _.chain(value).groupBy(n => n.data.data[EVENT_BUNDLES[key].groupBy]).forIn((groupValue) => {
let title = EVENT_BUNDLES[key].title;
title = replacePlaceholders(title, _(groupValue).map(g => g.data.data).value());
sections.push({
@@ -140,15 +152,6 @@ const scheduler = createEventScheduler(
handleScheduledEvents
);
-function getEventGroupKey(value) {
- let key = _.chain(EVENT_BUNDLES)
- .keys()
- .find(key => _.includes(_.get(EVENT_BUNDLES, `${key}.types`), _.get(value, 'data.data.type')))
- .value();
- if (!key) return 'DEFAULT';
- return key;
-}
-
/**
* Prepares data to be provided to the template to render a single notification.
*
@@ -245,30 +248,31 @@ function handler(topicName, messageJSON, notification) {
};
eventMessage.data[eventMessage.data.type] = true;
_.assign(eventMessage.data, notification.contents);
-
+
// message service may return tags
// to understand if post notification is regarding phases or no, we will try to get phaseId from the tags
- const tags = _.get(notification.contents, 'tags', [])
- const PHASE_ID_REGEXP = /phase#(\d+)/
- const phaseIds = tags.map((tag) => _.get(tag.match(PHASE_ID_REGEXP), '1', null))
- const phaseId = _.find(phaseIds, (phaseId) => phaseId !== null)
+ const tags = _.get(notification.contents, 'tags', []);
+ const phaseId = helpers.extractPhaseId(tags);
if (phaseId) {
eventMessage.data.phaseId = phaseId;
}
- // if the notification is regarding topic: dashboard topic, dashboard post or phase post
+ // if the notification is regarding topic: dashboard topic, dashboard post or phase post
// we build a link to the post
if (eventMessage.data.topicId) {
// phase post
if (eventMessage.data.phaseId) {
+ // eslint-disable-next-line max-len
eventMessage.data.postURL = `${config.CONNECT_URL}/projects/${eventMessage.data.projectId}/plan#phase-${eventMessage.data.phaseId}-posts-${eventMessage.data.postId}`;
// dashboard post
} else if (eventMessage.data.postId) {
+ // eslint-disable-next-line max-len
eventMessage.data.postURL = `${config.CONNECT_URL}/projects/${eventMessage.data.projectId}#comment-${eventMessage.data.postId}`;
// dashboard topic
} else {
+ // eslint-disable-next-line max-len
eventMessage.data.postURL = `${config.CONNECT_URL}/projects/${eventMessage.data.projectId}#feed-${eventMessage.data.topicId}`;
}
}
@@ -319,19 +323,21 @@ function handler(topicName, messageJSON, notification) {
}
// if notifications has to be bundled
- let bundlePeriod = _.get(settings, `notifications['${notificationType}'].${SETTINGS_EMAIL_SERVICE_ID}.bundlePeriod`);
+ let bundlePeriod = _.get(settings,
+ `notifications['${notificationType}'].${SETTINGS_EMAIL_SERVICE_ID}.bundlePeriod`);
bundlePeriod = bundlePeriod && bundlePeriod.trim().length > 0 ? bundlePeriod : null;
// if bundling is not explicitly set and the event is not a messaging event, assume bundling enabled
if (!bundlePeriod && !messagingEvent) {
// finds the event category for the notification type
- let eventBundleCategory = _.findKey(EVENT_BUNDLES, b => b.types && b.types.indexOf(notificationType) !== -1);
+ const eventBundleCategory = _.findKey(EVENT_BUNDLES, b => b.types && b.types.indexOf(notificationType) !== -1);
if (eventBundleCategory) {
const eventBundle = EVENT_BUNDLES[eventBundleCategory];
// if we find the event category for the notification, use the bundle settings from the first event
if (eventBundle && eventBundle.types && eventBundle.types.length) {
const firstEvtInBundle = eventBundle.types[0];
- const firstEvtBundleSettingPath = `notifications['${firstEvtInBundle}'].${SETTINGS_EMAIL_SERVICE_ID}.bundlePeriod`;
- let firstEvtBundlePeriod = _.get(settings, firstEvtBundleSettingPath);
+ const firstEvtBundleSettingPath =
+ `notifications['${firstEvtInBundle}'].${SETTINGS_EMAIL_SERVICE_ID}.bundlePeriod`;
+ const firstEvtBundlePeriod = _.get(settings, firstEvtBundleSettingPath);
bundlePeriod = firstEvtBundlePeriod;
logger.debug('Assuming bundle period of first event in the event category=>', bundlePeriod);
}
@@ -341,7 +347,7 @@ function handler(topicName, messageJSON, notification) {
}
logger.debug('bundlePeriod=>', bundlePeriod);
- if (bundlePeriod && 'immediately' !== bundlePeriod && !requiresImmediateAttention) {
+ if (bundlePeriod && bundlePeriod !== 'immediately' && !requiresImmediateAttention) {
if (!SCHEDULED_EVENT_PERIOD[bundlePeriod]) {
throw new Error(`User's '${notification.userId}' setting for service`
+ ` '${SETTINGS_EMAIL_SERVICE_ID}' option 'bundlePeriod' has unsupported value '${bundlePeriod}'.`);
@@ -359,7 +365,7 @@ function handler(topicName, messageJSON, notification) {
} else {
// send single field "notificationsHTML" with the rendered template
eventMessage.data = wrapIndividualNotification({ data: eventMessage });
- //console.log(eventMessage.data.contents);
+ // console.log(eventMessage.data.contents);
// send event to bus api
return busService.postEvent({
@@ -374,7 +380,7 @@ function handler(topicName, messageJSON, notification) {
.catch((err) => {
logger.error(`Failed to send ${eventType} event`
+ `; error: ${err.message}`
- + `; with body ${JSON.stringify(eventMessage)} to bus api`);
+ + `; with body ${JSON.stringify(eventMessage)} to bus api`);
});
}
});
diff --git a/connect/service.js b/connect/service.js
index adff1db..aae9f38 100644
--- a/connect/service.js
+++ b/connect/service.js
@@ -232,10 +232,46 @@ const getTopic = (topicId, logger) => (
})
);
+/**
+ * Get phase details
+ *
+ * @param {String} projectId project id
+ * @param {String} phaseId phase id
+ *
+ * @return {Promise} promise resolved to phase details
+ */
+const getPhase = (projectId, phaseId) => (
+ M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET)
+ .then((token) => (
+ request
+ .get(`${config.TC_API_V4_BASE_URL}/projects/${projectId}/phases/${phaseId}`)
+ .set('accept', 'application/json')
+ .set('authorization', `Bearer ${token}`)
+ .then((res) => {
+ if (!_.get(res, 'body.result.success')) {
+ throw new Error(`Failed to get phase details of project id: ${projectId}, phase id: ${phaseId}`);
+ }
+ const project = _.get(res, 'body.result.content');
+ return project;
+ }).catch((err) => {
+ const errorDetails = _.get(err, 'response.body.result.content.message');
+ throw new Error(
+ `Failed to get phase details of project id: ${projectId}, phase id: ${phaseId}.` +
+ (errorDetails ? ' Server response: ' + errorDetails : '')
+ );
+ })
+ ))
+ .catch((err) => {
+ err.message = 'Error generating m2m token: ' + err.message;
+ throw err;
+ })
+);
+
module.exports = {
getProject,
getRoleMembers,
getUsersById,
getUsersByHandle,
getTopic,
+ getPhase,
};
diff --git a/consumer.js b/consumer.js
index 60676ae..5313f0a 100644
--- a/consumer.js
+++ b/consumer.js
@@ -8,7 +8,7 @@ const _ = require('lodash');
const Kafka = require('no-kafka');
const co = require('co');
global.Promise = require('bluebird');
-const healthcheck = require('topcoder-healthcheck-dropin')
+const healthcheck = require('topcoder-healthcheck-dropin');
const logger = require('./src/common/logger');
const models = require('./src/models');
@@ -63,13 +63,13 @@ function startKafkaConsumer() {
return co(function* () {
// run each handler
for (let i = 0; i < ruleSets.length; i += 1) {
- const rule = ruleSets[i]
- const handlerFuncArr = _.keys(rule)
- const handlerFuncName = _.get(handlerFuncArr, "0")
+ const rule = ruleSets[i];
+ const handlerFuncArr = _.keys(rule);
+ const handlerFuncName = _.get(handlerFuncArr, '0');
try {
- const handler = processors[handlerFuncName]
- const handlerRuleSets = rule[handlerFuncName]
+ const handler = processors[handlerFuncName];
+ const handlerRuleSets = rule[handlerFuncName];
if (!handler) {
logger.error(`Handler ${handlerFuncName} is not defined`);
continue;
@@ -79,7 +79,7 @@ function startKafkaConsumer() {
const notifications = yield handler(messageJSON, handlerRuleSets);
if (notifications && notifications.length > 0) {
// save notifications in bulk to improve performance
- logger.info(`Going to insert ${notifications.length} notifications in database.`)
+ logger.info(`Going to insert ${notifications.length} notifications in database.`);
yield models.Notification.bulkCreate(_.map(notifications, (n) => ({
userId: n.userId,
type: n.type || topic,
@@ -87,9 +87,9 @@ function startKafkaConsumer() {
read: false,
seen: false,
version: n.version || null,
- })))
+ })));
// logging
- logger.info(`Saved ${notifications.length} notifications`)
+ logger.info(`Saved ${notifications.length} notifications`);
/* logger.info(` for users: ${
_.map(notifications, (n) => n.userId).join(', ')
}`); */
@@ -112,15 +112,15 @@ function startKafkaConsumer() {
const check = function () {
if (!consumer.client.initialBrokers && !consumer.client.initialBrokers.length) {
- return false
+ return false;
}
- let connected = true
+ let connected = true;
consumer.client.initialBrokers.forEach(conn => {
- logger.debug(`url ${conn.server()} - connected=${conn.connected}`)
- connected = conn.connected & connected
- })
- return connected
- }
+ logger.debug(`url ${conn.server()} - connected=${conn.connected}`);
+ connected = conn.connected & connected;
+ });
+ return connected;
+ };
// Start kafka consumer
logger.info('Starting kafka consumer');
@@ -132,7 +132,7 @@ function startKafkaConsumer() {
}])
.then(() => {
logger.info('Kafka consumer initialized successfully');
- healthcheck.init([check])
+ healthcheck.init([check]);
})
.catch((err) => {
logger.error('Kafka consumer failed');
diff --git a/docs/swagger_api.yaml b/docs/swagger_api.yaml
index 1afa80f..1e77b61 100644
--- a/docs/swagger_api.yaml
+++ b/docs/swagger_api.yaml
@@ -1,275 +1,350 @@
-swagger: "2.0"
-info:
- title: "TOPCODER NOTIFICATIONS SERIES - NOTIFICATIONS SERVER"
- description: "TOPCODER NOTIFICATIONS SERIES - NOTIFICATIONS SERVER"
- version: "1.0.0"
-host: "localhost:4000"
-basePath: "/v5/notifications"
-schemes:
-- "http"
-securityDefinitions:
- jwt:
- type: apiKey
- name: Authorization
- in: header
- description: JWT Authentication. Provide API Key in the form 'Bearer <token>'.
-
-paths:
- /list:
- get:
- description:
- list notifications
- produces:
- - application/json
- security:
- - jwt: []
- parameters:
- - name: offset
- in: query
- description: The offset
- required: false
- type: integer
- format: int32
- - name: limit
- in: query
- description: The limit
- required: false
- type: integer
- format: int32
- - name: type
- in: query
- description: The type
- required: false
- type: string
- - name: read
- in: query
- description: The read flag, either 'true' or 'false'
- required: false
- type: string
- responses:
- 200:
- description: OK
- schema:
- type: object
- properties:
- items:
- type: array
- items:
- type: object
- properties:
- id:
- type: integer
- format: int64
- description: the notification id
- userId:
- type: integer
- format: int64
- description: user id
- type:
- type: string
- description: notification type
- read:
- type: boolean
- description: read flag
- seen:
- type: boolean
- description: seen flag
- contents:
- type: object
- description: the event message in JSON format
- createdAt:
- type: string
- description: created at date string
- updatedAt:
- type: string
- description: updated at date string
- offset:
- type: integer
- format: int32
- description: the offset
- limit:
- type: integer
- format: int32
- description: the limit
- totalCount:
- type: integer
- format: int32
- description: the total count
- 401:
- description: "Authentication failed."
- schema:
- $ref: "#/definitions/Error"
- 500:
- description: "Internal server error."
- schema:
- $ref: "#/definitions/Error"
- /{id}/read:
- put:
- description:
- mark notification(s) as read, id can be single id or '-' separated ids
- security:
- - jwt: []
- parameters:
- - in: path
- name: id
- description: notification id
- required: true
- type: integer
- format: int64
- responses:
- 200:
- description: OK, the notification(s) are marked as read
- 400:
- description: "Invalid input"
- schema:
- $ref: "#/definitions/Error"
- 401:
- description: "authentication failed"
- schema:
- $ref: "#/definitions/Error"
- 403:
- description: "Action not allowed."
- schema:
- $ref: "#/definitions/Error"
- 404:
- description: "Notification is not found"
- schema:
- $ref: "#/definitions/Error"
- 500:
- description: "Internal server error."
- schema:
- $ref: "#/definitions/Error"
- /read:
- put:
- description:
- mark all notifications as read
- security:
- - jwt: []
- responses:
- 200:
- description: OK, all notifications are marked as read
- 401:
- description: "authentication failed"
- schema:
- $ref: "#/definitions/Error"
- 500:
- description: "Internal server error."
- schema:
- $ref: "#/definitions/Error"
- /{id}/seen:
- put:
- description:
- mark notification(s) as seen, id can be single id or '-' separated ids
- security:
- - jwt: []
- parameters:
- - in: path
- name: id
- description: notification id
- required: true
- type: integer
- format: int64
- responses:
- 200:
- description: OK, the notification(s) are marked as seen
- 400:
- description: "Invalid input"
- schema:
- $ref: "#/definitions/Error"
- 401:
- description: "authentication failed"
- schema:
- $ref: "#/definitions/Error"
- 403:
- description: "Action not allowed."
- schema:
- $ref: "#/definitions/Error"
- 404:
- description: "Notification is not found"
- schema:
- $ref: "#/definitions/Error"
- 500:
- description: "Internal server error."
- schema:
- $ref: "#/definitions/Error"
- /settings:
- get:
- description:
- get notification settings
- produces:
- - application/json
- security:
- - jwt: []
- responses:
- 200:
- description: OK. Each key is topic name, value is object of deliveryMethod - value mappings for the topic
- schema:
- type: object
- 401:
- description: "Authentication failed."
- schema:
- $ref: "#/definitions/Error"
- 500:
- description: "Internal server error."
- schema:
- $ref: "#/definitions/Error"
- put:
- description:
- update notification settings
- consumes:
- - application/json
- security:
- - jwt: []
- parameters:
- - in: body
- name: body
- description: notification settings
- required: true
- schema:
- type: array
- items:
- type: object
- properties:
- topic:
- type: string
- description: the topic
- deliveryMethod:
- type: string
- description: the delivery method
- value:
- type: string
- description: the value for the delivery method
- responses:
- 200:
- description: OK
- 400:
- description: "Invalid input"
- schema:
- $ref: "#/definitions/Error"
- 401:
- description: "authentication failed"
- schema:
- $ref: "#/definitions/Error"
- 500:
- description: "Internal server error."
- schema:
- $ref: "#/definitions/Error"
-
-definitions:
- Error:
- properties:
- error:
- type: string
- details:
- type: array
- items:
- type: object
- properties:
- message:
- type: string
- path:
- type: string
- type:
- type: string
- context:
- type: object
+swagger: "2.0"
+info:
+ title: "TOPCODER NOTIFICATIONS SERIES - NOTIFICATIONS SERVER"
+ description: "TOPCODER NOTIFICATIONS SERIES - NOTIFICATIONS SERVER"
+ version: "1.0.0"
+host: "localhost:4000"
+basePath: "/v5/notifications"
+schemes:
+ - "http"
+securityDefinitions:
+ jwt:
+ type: apiKey
+ name: Authorization
+ in: header
+ description: JWT Authentication. Provide API Key in the form 'Bearer <token>'.
+
+paths:
+ /:
+ get:
+ description:
+ list notifications
+ produces:
+ - application/json
+ security:
+ - jwt: []
+ parameters:
+ - name: page
+ in: query
+ description: The page
+ required: false
+ type: integer
+ format: int32
+ - name: per_page
+ in: query
+ description: The number of rows served
+ required: false
+ type: integer
+ format: int32
+ - name: platform
+ in: query
+ description: The platform
+ required: false
+ type: string
+ - name: type
+ in: query
+ description: The type
+ required: false
+ type: string
+ - name: read
+ in: query
+ description: The read flag, either 'true' or 'false'
+ required: false
+ type: string
+ responses:
+ 200:
+ description: OK
+ schema:
+ type: object
+ properties:
+ items:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: integer
+ format: int64
+ description: the notification id
+ userId:
+ type: integer
+ format: int64
+ description: user id
+ type:
+ type: string
+ description: notification type
+ read:
+ type: boolean
+ description: read flag
+ seen:
+ type: boolean
+ description: seen flag
+ contents:
+ type: object
+ description: the event message in JSON format
+ createdAt:
+ type: string
+ description: created at date string
+ updatedAt:
+ type: string
+ description: updated at date string
+ page:
+ type: integer
+ format: int32
+ description: the page
+ per_page:
+ type: integer
+ format: int32
+ description: the per_page
+ totalCount:
+ type: integer
+ format: int32
+ description: the total count
+ 401:
+ description: "Authentication failed."
+ schema:
+ $ref: "#/definitions/Error"
+ 500:
+ description: "Internal server error."
+ schema:
+ $ref: "#/definitions/Error"
+ /{id}:
+ patch:
+ description:
+ update notification
+ security:
+ - jwt: []
+ parameters:
+ - in: path
+ name: id
+ description: notification id
+ required: true
+ type: integer
+ format: int64
+ - in: body
+ name: body
+ description: notification payload
+ required: true
+ schema:
+ $ref: "#/definitions/NotificationUpdatePayload"
+ responses:
+ 200:
+ description: OK, the notification(s) are updated
+ schema:
+ $ref: "#/definitions/Notification"
+ 400:
+ description: "Invalid input"
+ schema:
+ $ref: "#/definitions/Error"
+ 401:
+ description: "authentication failed"
+ schema:
+ $ref: "#/definitions/Error"
+ 403:
+ description: "Action not allowed."
+ schema:
+ $ref: "#/definitions/Error"
+ 404:
+ description: "Notification is not found"
+ schema:
+ $ref: "#/definitions/Error"
+ 500:
+ description: "Internal server error."
+ schema:
+ $ref: "#/definitions/Error"
+ /{id}/read:
+ put:
+ description:
+ mark notification(s) as read, id can be single id or '-' separated ids
+ security:
+ - jwt: []
+ parameters:
+ - in: path
+ name: id
+ description: notification id
+ required: true
+ type: integer
+ format: int64
+ responses:
+ 200:
+ description: OK, the notification(s) are marked as read
+ 400:
+ description: "Invalid input"
+ schema:
+ $ref: "#/definitions/Error"
+ 401:
+ description: "authentication failed"
+ schema:
+ $ref: "#/definitions/Error"
+ 403:
+ description: "Action not allowed."
+ schema:
+ $ref: "#/definitions/Error"
+ 404:
+ description: "Notification is not found"
+ schema:
+ $ref: "#/definitions/Error"
+ 500:
+ description: "Internal server error."
+ schema:
+ $ref: "#/definitions/Error"
+ /read:
+ put:
+ description:
+ mark all notifications as read
+ security:
+ - jwt: []
+ responses:
+ 200:
+ description: OK, all notifications are marked as read
+ 401:
+ description: "authentication failed"
+ schema:
+ $ref: "#/definitions/Error"
+ 500:
+ description: "Internal server error."
+ schema:
+ $ref: "#/definitions/Error"
+ /{id}/seen:
+ put:
+ description:
+ mark notification(s) as seen, id can be single id or '-' separated ids
+ security:
+ - jwt: []
+ parameters:
+ - in: path
+ name: id
+ description: notification id
+ required: true
+ type: integer
+ format: int64
+ responses:
+ 200:
+ description: OK, the notification(s) are marked as seen
+ 400:
+ description: "Invalid input"
+ schema:
+ $ref: "#/definitions/Error"
+ 401:
+ description: "authentication failed"
+ schema:
+ $ref: "#/definitions/Error"
+ 403:
+ description: "Action not allowed."
+ schema:
+ $ref: "#/definitions/Error"
+ 404:
+ description: "Notification is not found"
+ schema:
+ $ref: "#/definitions/Error"
+ 500:
+ description: "Internal server error."
+ schema:
+ $ref: "#/definitions/Error"
+ /settings:
+ get:
+ description:
+ get notification settings
+ produces:
+ - application/json
+ security:
+ - jwt: []
+ responses:
+ 200:
+ description: OK. Each key is topic name, value is object of deliveryMethod - value mappings for the topic
+ schema:
+ type: object
+ 401:
+ description: "Authentication failed."
+ schema:
+ $ref: "#/definitions/Error"
+ 500:
+ description: "Internal server error."
+ schema:
+ $ref: "#/definitions/Error"
+ put:
+ description:
+ update notification settings
+ consumes:
+ - application/json
+ security:
+ - jwt: []
+ parameters:
+ - in: body
+ name: body
+ description: notification settings
+ required: true
+ schema:
+ type: array
+ items:
+ type: object
+ properties:
+ topic:
+ type: string
+ description: the topic
+ deliveryMethod:
+ type: string
+ description: the delivery method
+ value:
+ type: string
+ description: the value for the delivery method
+ responses:
+ 200:
+ description: OK
+ 400:
+ description: "Invalid input"
+ schema:
+ $ref: "#/definitions/Error"
+ 401:
+ description: "authentication failed"
+ schema:
+ $ref: "#/definitions/Error"
+ 500:
+ description: "Internal server error."
+ schema:
+ $ref: "#/definitions/Error"
+
+definitions:
+ NotificationUpdatePayload:
+ properties:
+ read:
+ type: boolean
+ seen:
+ type: boolean
+ Notification:
+ properties:
+ id:
+ type: integer
+ userId:
+ type: integer
+ type:
+ type: string
+ contents:
+ type: object
+ version:
+ type: integer
+ read:
+ type: boolean
+ seen:
+ type: boolean
+ createdAt:
+ type: string
+ updatedAt:
+ type: string
+ Error:
+ properties:
+ error:
+ type: string
+ details:
+ type: array
+ items:
+ type: object
+ properties:
+ message:
+ type: string
+ path:
+ type: string
+ type:
+ type: string
+ context:
+ type: object
diff --git a/docs/tc-notification-server-api.postman_collection.json b/docs/tc-notification-server-api.postman_collection.json
index f7c65c0..7267bf8 100644
--- a/docs/tc-notification-server-api.postman_collection.json
+++ b/docs/tc-notification-server-api.postman_collection.json
@@ -1,83 +1,108 @@
{
- "id": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
+ "id": "8fe3fe1d-744f-4c0e-ac34-e2029e6308e9",
"name": "tc-notification-server-api",
"description": "",
"auth": null,
"events": null,
- "variables": null,
+ "variables": [],
"order": [
- "19332a51-03e8-4f5c-8f85-4d28d6dfe6f4",
- "543cab06-2c7d-4aed-8cf3-0808463254d5",
- "76779830-a8a4-4636-8c03-1801b3d1863d",
- "cb2299a5-dac7-4c40-80c4-7b1694138354",
- "d57ba947-a5e7-410a-b978-76882f33c86e",
- "fce69847-5bf8-4b07-bcaf-6352db4ba923"
+ "68de66b4-c2c6-4b3b-9df4-42f7cf4c4fa3",
+ "c31aa2cc-e377-4b59-a4ee-9e5181449996",
+ "4dc587b5-2da8-4055-8317-88f7a677eb34",
+ "e323baef-7406-4665-9bbe-3f64ce4a427c",
+ "af4744ee-ceba-4a35-a14a-eb38290139fb",
+ "0012295a-9139-47bb-91b3-1f53d94bd928",
+ "8981f01c-fd95-4d19-abcf-36265682a610"
],
"folders_order": [
- "dbebd550-6c33-4778-b467-d56decf16c91"
+ "060feceb-1658-4fee-9d71-0fedc75effe9"
],
"folders": [
{
- "id": "dbebd550-6c33-4778-b467-d56decf16c91",
+ "id": "060feceb-1658-4fee-9d71-0fedc75effe9",
"name": "failure",
"description": "",
"auth": null,
"events": null,
- "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
+ "collection": "8fe3fe1d-744f-4c0e-ac34-e2029e6308e9",
"folder": null,
"order": [
- "1b3b6480-ea94-4027-8898-f82f28e2bea6",
- "59fc9f2b-28c5-4cff-b21b-11ab51bf67d8",
- "cbc03cb1-6dfe-43fd-8e99-8c56923c2978",
- "d293d2c5-230d-4f34-8c97-1adc1f2f89b4",
- "da23d550-55b3-4f7d-9131-735956d62f6d",
- "f2246cf7-7aae-4ea0-9d92-1d932d340302",
- "f3f3a847-46f6-4059-b167-b436078fb112"
+ "47aaac74-b661-48d6-bed1-d61efd13b668",
+ "c2d28896-3208-4326-a65e-1718c1f891c5",
+ "c8bb53de-91fc-443c-a8fc-d13f503d0e5f",
+ "208a8afb-8287-4b93-88fd-67320d0a7f0f",
+ "021b87b0-caca-41e9-85ea-16a9d09bab22",
+ "334aae9e-38fc-425e-9649-e09c6188ddc2",
+ "9445564c-74bb-4599-8eca-ae292d5b37fc",
+ "88038b95-0a16-4a36-8835-0713c731e433"
],
"folders_order": []
}
],
"requests": [
{
- "id": "19332a51-03e8-4f5c-8f85-4d28d6dfe6f4",
- "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
- "name": "getSettings",
- "url": "{{URL}}/settings",
+ "id": "0012295a-9139-47bb-91b3-1f53d94bd928",
+ "name": "updateNotification",
+ "url": "{{URL}}/1",
"description": "",
"data": [],
"dataMode": "raw",
"headerData": [
{
- "key": "Content-Type",
- "value": "application/json",
"description": "",
- "enabled": true
+ "enabled": true,
+ "key": "Content-Type",
+ "value": "application/json"
},
{
- "key": "Authorization",
- "value": "Bearer {{TOKEN}}",
"description": "",
- "enabled": true
+ "enabled": true,
+ "key": "Authorization",
+ "value": "Bearer {{TOKEN}}"
}
],
- "method": "GET",
+ "method": "PATCH",
"pathVariableData": [],
"queryParams": [],
"auth": null,
"events": null,
"folder": null,
- "rawModeData": "",
+ "rawModeData": "{\n\t\"read\": true\n}",
"headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
"pathVariables": {}
},
{
- "id": "1b3b6480-ea94-4027-8898-f82f28e2bea6",
- "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
- "name": "listNotifications - invalid read filter",
- "url": "{{URL}}/list?offset=0&limit=20&type=notifications.connect.project.updated&read=yes",
+ "id": "021b87b0-caca-41e9-85ea-16a9d09bab22",
+ "name": "markAllRead - missing token",
+ "url": "{{URL}}/read",
"description": "",
"data": [],
"dataMode": "raw",
+ "headerData": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "enabled": false
+ }
+ ],
+ "method": "PUT",
+ "pathVariableData": [],
+ "queryParams": [],
+ "auth": null,
+ "events": null,
+ "folder": "060feceb-1658-4fee-9d71-0fedc75effe9",
+ "rawModeData": "",
+ "headers": "//Content-Type: application/json\n",
+ "pathVariables": {}
+ },
+ {
+ "id": "208a8afb-8287-4b93-88fd-67320d0a7f0f",
+ "name": "listNotifications - invalid limit",
+ "url": "{{URL}}/list?offset=0&limit=abc&type=notifications.connect.project.updated",
+ "description": "",
+ "data": null,
+ "dataMode": null,
"headerData": [
{
"key": "Content-Type",
@@ -104,7 +129,7 @@
},
{
"key": "limit",
- "value": "20",
+ "value": "abc",
"equals": true,
"description": "",
"enabled": true
@@ -118,24 +143,22 @@
},
{
"key": "read",
- "value": "yes",
+ "value": "false",
"equals": true,
"description": "",
- "enabled": true
+ "enabled": false
}
],
"auth": null,
"events": null,
- "folder": "dbebd550-6c33-4778-b467-d56decf16c91",
- "rawModeData": "",
+ "folder": "060feceb-1658-4fee-9d71-0fedc75effe9",
"headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
"pathVariables": {}
},
{
- "id": "543cab06-2c7d-4aed-8cf3-0808463254d5",
- "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
- "name": "markAllRead",
- "url": "{{URL}}/read",
+ "id": "334aae9e-38fc-425e-9649-e09c6188ddc2",
+ "name": "updateSettings - invalid body",
+ "url": "{{URL}}/settings",
"description": "",
"data": [],
"dataMode": "raw",
@@ -144,7 +167,7 @@
"key": "Content-Type",
"value": "application/json",
"description": "",
- "enabled": false
+ "enabled": true
},
{
"key": "Authorization",
@@ -158,19 +181,18 @@
"queryParams": [],
"auth": null,
"events": null,
- "folder": null,
- "rawModeData": "",
- "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "folder": "060feceb-1658-4fee-9d71-0fedc75effe9",
+ "rawModeData": "[\n\t{\n\t\t\"wrong\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": 123,\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"off\"\n\t},\n\t{\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"email\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"on\"\n\t}\n]",
+ "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
"pathVariables": {}
},
{
- "id": "59fc9f2b-28c5-4cff-b21b-11ab51bf67d8",
- "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
- "name": "getSettings - invalid token",
- "url": "{{URL}}/settings",
+ "id": "47aaac74-b661-48d6-bed1-d61efd13b668",
+ "name": "listNotifications - invalid read filter",
+ "url": "{{URL}}/list?offset=0&limit=20&type=notifications.connect.project.updated&read=yes",
"description": "",
- "data": [],
- "dataMode": "raw",
+ "data": null,
+ "dataMode": null,
"headerData": [
{
"key": "Content-Type",
@@ -180,24 +202,51 @@
},
{
"key": "Authorization",
- "value": "Bearer invalid",
+ "value": "Bearer {{TOKEN}}",
"description": "",
"enabled": true
}
],
"method": "GET",
"pathVariableData": [],
- "queryParams": [],
+ "queryParams": [
+ {
+ "key": "offset",
+ "value": "0",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "limit",
+ "value": "20",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "type",
+ "value": "notifications.connect.project.updated",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "read",
+ "value": "yes",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ }
+ ],
"auth": null,
"events": null,
- "folder": "dbebd550-6c33-4778-b467-d56decf16c91",
- "rawModeData": "",
- "headers": "Content-Type: application/json\nAuthorization: Bearer invalid\n",
+ "folder": "060feceb-1658-4fee-9d71-0fedc75effe9",
+ "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
"pathVariables": {}
},
{
- "id": "76779830-a8a4-4636-8c03-1801b3d1863d",
- "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
+ "id": "4dc587b5-2da8-4055-8317-88f7a677eb34",
"name": "markAsRead",
"url": "{{URL}}/1/read",
"description": "",
@@ -228,13 +277,12 @@
"pathVariables": {}
},
{
- "id": "cb2299a5-dac7-4c40-80c4-7b1694138354",
- "name": "TC API - get project",
- "url": "https://api.topcoder-dev.com/v4/projects/1936",
- "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
+ "id": "68de66b4-c2c6-4b3b-9df4-42f7cf4c4fa3",
+ "name": "getSettings",
+ "url": "{{URL}}/settings",
"description": "",
- "data": [],
- "dataMode": "raw",
+ "data": null,
+ "dataMode": null,
"headerData": [
{
"key": "Content-Type",
@@ -243,8 +291,8 @@
"enabled": true
},
{
- "key": "authorization",
- "value": "Bearer {{TC_ADMIN_TOKEN}}",
+ "key": "Authorization",
+ "value": "Bearer {{TOKEN}}",
"description": "",
"enabled": true
}
@@ -255,143 +303,47 @@
"auth": null,
"events": null,
"folder": null,
- "responses": [
- {
- "id": "ae658c70-e29d-4d49-aefd-944af0e4f811",
- "name": "test111",
- "status": "",
- "mime": "",
- "language": "json",
- "text": "{\"id\":\"95744bd2-2830-4014-8885-7182a6225953\",\"version\":\"v4\",\"result\":{\"success\":true,\"status\":200,\"content\":{\"id\":1936,\"directProjectId\":12147,\"billingAccountId\":null,\"name\":\"Test-prj\",\"description\":\"Test description\",\"external\":null,\"bookmarks\":[],\"estimatedPrice\":null,\"actualPrice\":null,\"terms\":[],\"type\":\"app_dev\",\"status\":\"draft\",\"details\":{\"products\":[\"api_dev\"],\"appDefinition\":{\"primaryTarget\":\"desktop\",\"goal\":{\"value\":\"Goal\"},\"users\":{\"value\":\"Developers\"},\"notes\":\"Notes\"},\"utm\":{},\"hideDiscussions\":true},\"challengeEligibility\":[],\"cancelReason\":null,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"members\":[{\"id\":2997,\"userId\":305384,\"role\":\"customer\",\"isPrimary\":true,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"projectId\":1936}],\"attachments\":[]},\"metadata\":{\"totalCount\":1}}}",
- "responseCode": {
- "code": 200,
- "name": "OK",
- "detail": "Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request the response will contain an entity describing or containing the result of the action."
- },
- "requestObject": null,
- "headers": [
- {
- "name": "access-control-allow-credentials",
- "key": "access-control-allow-credentials",
- "value": "true",
- "description": "Indicates whether or not the response to the request can be exposed when the credentials flag is true. When used as part of a response to a preflight request, this indicates whether or not the actual request can be made using credentials."
- },
- {
- "name": "access-control-allow-headers",
- "key": "access-control-allow-headers",
- "value": "Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since",
- "description": "Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request."
- },
- {
- "name": "access-control-allow-methods",
- "key": "access-control-allow-methods",
- "value": "GET, POST, OPTIONS, DELETE, PUT, PATCH",
- "description": "Specifies the method or methods allowed when accessing the resource. This is used in response to a preflight request."
- },
- {
- "name": "connection",
- "key": "connection",
- "value": "keep-alive",
- "description": "Options that are desired for the connection"
- },
- {
- "name": "content-encoding",
- "key": "content-encoding",
- "value": "gzip",
- "description": "The type of encoding used on the data."
- },
- {
- "name": "content-length",
- "key": "content-length",
- "value": "491",
- "description": "The length of the response body in octets (8-bit bytes)"
- },
- {
- "name": "content-type",
- "key": "content-type",
- "value": "application/json; charset=utf-8",
- "description": "The mime type of this content"
- },
- {
- "name": "date",
- "key": "date",
- "value": "Thu, 02 Nov 2017 04:28:20 GMT",
- "description": "The date and time that the message was sent"
- },
- {
- "name": "etag",
- "key": "etag",
- "value": "W/\"3a6-4pbtTNq19Shn10rc0k+HRsoAyMw\"",
- "description": "An identifier for a specific version of a resource, often a message digest"
- },
- {
- "name": "server",
- "key": "server",
- "value": "nginx/1.9.7",
- "description": "A name for the server"
- },
- {
- "name": "x-powered-by",
- "key": "x-powered-by",
- "value": "Express",
- "description": "Specifies the technology (ASP.NET, PHP, JBoss, e.g.) supporting the web application (version details are often in X-Runtime, X-Version, or X-AspNet-Version)"
- },
- {
- "name": "x-request-id",
- "key": "x-request-id",
- "value": "95744bd2-2830-4014-8885-7182a6225953",
- "description": "Custom header"
- }
- ],
- "cookies": [],
- "request": "cb2299a5-dac7-4c40-80c4-7b1694138354",
- "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452"
- }
- ],
- "rawModeData": "",
- "headers": "Content-Type: application/json\nauthorization: Bearer {{TC_ADMIN_TOKEN}}\n",
+ "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
"pathVariables": {}
},
{
- "id": "cbc03cb1-6dfe-43fd-8e99-8c56923c2978",
- "name": "markAsRead - not found",
- "url": "{{URL}}/1111111/read",
+ "id": "88038b95-0a16-4a36-8835-0713c731e433",
+ "name": "updateNotification - invalid action",
+ "url": "{{URL}}/1",
"description": "",
- "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
"data": [],
"dataMode": "raw",
"headerData": [
{
- "key": "Content-Type",
- "value": "application/json",
"description": "",
- "enabled": false
+ "enabled": true,
+ "key": "Content-Type",
+ "value": "application/json"
},
{
- "key": "Authorization",
- "value": "Bearer {{TOKEN}}",
"description": "",
- "enabled": true
+ "enabled": true,
+ "key": "Authorization",
+ "value": "Bearer {{TOKEN}}"
}
],
- "method": "PUT",
+ "method": "PATCH",
"pathVariableData": [],
"queryParams": [],
"auth": null,
"events": null,
- "folder": "dbebd550-6c33-4778-b467-d56decf16c91",
- "rawModeData": "",
- "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "folder": "060feceb-1658-4fee-9d71-0fedc75effe9",
+ "rawModeData": "{\n\t\"read\": true,\n\t\"seen\": false\n}",
+ "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
"pathVariables": {}
},
{
- "id": "d293d2c5-230d-4f34-8c97-1adc1f2f89b4",
- "name": "listNotifications - invalid limit",
- "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
- "url": "{{URL}}/list?offset=0&limit=abc&type=notifications.connect.project.updated",
+ "id": "8981f01c-fd95-4d19-abcf-36265682a610",
+ "name": "listNotifications",
+ "url": "{{URL}}?page=1&per_page=20&platform=connect",
"description": "",
- "data": [],
- "dataMode": "raw",
+ "data": null,
+ "dataMode": null,
"headerData": [
{
"key": "Content-Type",
@@ -410,22 +362,22 @@
"pathVariableData": [],
"queryParams": [
{
- "key": "offset",
- "value": "0",
+ "key": "page",
+ "value": "1",
"equals": true,
"description": "",
"enabled": true
},
{
- "key": "limit",
- "value": "abc",
+ "key": "per_page",
+ "value": "20",
"equals": true,
"description": "",
"enabled": true
},
{
- "key": "type",
- "value": "notifications.connect.project.updated",
+ "key": "platform",
+ "value": "connect",
"equals": true,
"description": "",
"enabled": true
@@ -440,48 +392,66 @@
],
"auth": null,
"events": null,
- "folder": "dbebd550-6c33-4778-b467-d56decf16c91",
- "rawModeData": "",
+ "folder": null,
"headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
"pathVariables": {}
},
{
- "id": "d57ba947-a5e7-410a-b978-76882f33c86e",
- "name": "updateSettings",
- "url": "{{URL}}/settings",
- "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
+ "id": "9445564c-74bb-4599-8eca-ae292d5b37fc",
+ "name": "listNotifications - invalid page",
+ "url": "{{URL}}/list?page=-1&per_page=20",
"description": "",
- "data": [],
- "dataMode": "raw",
+ "data": null,
+ "dataMode": null,
"headerData": [
{
+ "description": "",
+ "enabled": true,
"key": "Content-Type",
- "value": "application/json",
+ "value": "application/json"
+ },
+ {
+ "description": "",
+ "enabled": true,
+ "key": "Authorization",
+ "value": "Bearer {{TOKEN}}"
+ }
+ ],
+ "method": "GET",
+ "pathVariableData": [],
+ "queryParams": [
+ {
+ "key": "page",
+ "value": "-1",
+ "equals": true,
"description": "",
"enabled": true
},
{
- "key": "Authorization",
- "value": "Bearer {{TOKEN}}",
+ "key": "per_page",
+ "value": "20",
+ "equals": true,
"description": "",
"enabled": true
+ },
+ {
+ "key": "read",
+ "value": "false",
+ "equals": true,
+ "description": "",
+ "enabled": false
}
],
- "method": "PUT",
- "pathVariableData": [],
- "queryParams": [],
"auth": null,
"events": null,
- "folder": null,
- "rawModeData": "{\n \"notifications\": {\n \"notifications.connect.project.active\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.updated\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.left\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.paused\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.approved\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.fileUploaded\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.canceled\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.topic.created\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.copilotJoined\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.post.deleted\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.created\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.assignedAsOwner\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.completed\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.topic.deleted\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.post.created\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.joined\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.removed\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.specificationModified\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.managerJoined\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.submittedForReview\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.linkCreated\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.post.edited\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n }\n },\n \"services\": {\n \"email\": {\n \"bundlePeriod\": \"every10minutes\"\n }\n }\n}",
+ "folder": "060feceb-1658-4fee-9d71-0fedc75effe9",
"headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
"pathVariables": {}
},
{
- "id": "da23d550-55b3-4f7d-9131-735956d62f6d",
- "name": "markAllRead - missing token",
- "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
- "url": "{{URL}}/read",
+ "id": "af4744ee-ceba-4a35-a14a-eb38290139fb",
+ "name": "updateSettings",
+ "url": "{{URL}}/settings",
"description": "",
"data": [],
"dataMode": "raw",
@@ -490,7 +460,13 @@
"key": "Content-Type",
"value": "application/json",
"description": "",
- "enabled": false
+ "enabled": true
+ },
+ {
+ "key": "Authorization",
+ "value": "Bearer {{TOKEN}}",
+ "description": "",
+ "enabled": true
}
],
"method": "PUT",
@@ -498,19 +474,18 @@
"queryParams": [],
"auth": null,
"events": null,
- "folder": "dbebd550-6c33-4778-b467-d56decf16c91",
- "rawModeData": "",
- "headers": "//Content-Type: application/json\n",
+ "folder": null,
+ "rawModeData": "{\n \"notifications\": {\n \"notifications.connect.project.active\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.updated\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.left\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.paused\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.approved\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.fileUploaded\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.canceled\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.topic.created\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.copilotJoined\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.post.deleted\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.created\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.assignedAsOwner\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.completed\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.topic.deleted\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.post.created\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.joined\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.removed\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.specificationModified\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.managerJoined\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.submittedForReview\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.linkCreated\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.post.edited\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n }\n },\n \"services\": {\n \"email\": {\n \"bundlePeriod\": \"every10minutes\"\n }\n }\n}",
+ "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
"pathVariables": {}
},
{
- "id": "f2246cf7-7aae-4ea0-9d92-1d932d340302",
- "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
- "name": "updateSettings - invalid body",
+ "id": "c2d28896-3208-4326-a65e-1718c1f891c5",
+ "name": "getSettings - invalid token",
"url": "{{URL}}/settings",
"description": "",
- "data": [],
- "dataMode": "raw",
+ "data": null,
+ "dataMode": null,
"headerData": [
{
"key": "Content-Type",
@@ -520,26 +495,24 @@
},
{
"key": "Authorization",
- "value": "Bearer {{TOKEN}}",
+ "value": "Bearer invalid",
"description": "",
"enabled": true
}
],
- "method": "PUT",
+ "method": "GET",
"pathVariableData": [],
"queryParams": [],
"auth": null,
"events": null,
- "folder": "dbebd550-6c33-4778-b467-d56decf16c91",
- "rawModeData": "[\n\t{\n\t\t\"wrong\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": 123,\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"off\"\n\t},\n\t{\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"email\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"on\"\n\t}\n]",
- "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "folder": "060feceb-1658-4fee-9d71-0fedc75effe9",
+ "headers": "Content-Type: application/json\nAuthorization: Bearer invalid\n",
"pathVariables": {}
},
{
- "id": "f3f3a847-46f6-4059-b167-b436078fb112",
- "name": "listNotifications - invalid offset",
- "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
- "url": "{{URL}}/list?offset=-1&limit=20&type=notifications.connect.project.updated",
+ "id": "c31aa2cc-e377-4b59-a4ee-9e5181449996",
+ "name": "markAllRead",
+ "url": "{{URL}}/read",
"description": "",
"data": [],
"dataMode": "raw",
@@ -548,7 +521,7 @@
"key": "Content-Type",
"value": "application/json",
"description": "",
- "enabled": true
+ "enabled": false
},
{
"key": "Authorization",
@@ -557,50 +530,20 @@
"enabled": true
}
],
- "method": "GET",
+ "method": "PUT",
"pathVariableData": [],
- "queryParams": [
- {
- "key": "offset",
- "value": "-1",
- "equals": true,
- "description": "",
- "enabled": true
- },
- {
- "key": "limit",
- "value": "20",
- "equals": true,
- "description": "",
- "enabled": true
- },
- {
- "key": "type",
- "value": "notifications.connect.project.updated",
- "equals": true,
- "description": "",
- "enabled": true
- },
- {
- "key": "read",
- "value": "false",
- "equals": true,
- "description": "",
- "enabled": false
- }
- ],
+ "queryParams": [],
"auth": null,
"events": null,
- "folder": "dbebd550-6c33-4778-b467-d56decf16c91",
+ "folder": null,
"rawModeData": "",
- "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
"pathVariables": {}
},
{
- "id": "fce69847-5bf8-4b07-bcaf-6352db4ba923",
- "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
- "name": "listNotifications",
- "url": "{{URL}}/list?offset=0&limit=20",
+ "id": "c8bb53de-91fc-443c-a8fc-d13f503d0e5f",
+ "name": "markAsRead - not found",
+ "url": "{{URL}}/1111111/read",
"description": "",
"data": [],
"dataMode": "raw",
@@ -609,7 +552,7 @@
"key": "Content-Type",
"value": "application/json",
"description": "",
- "enabled": true
+ "enabled": false
},
{
"key": "Authorization",
@@ -618,44 +561,138 @@
"enabled": true
}
],
- "method": "GET",
+ "method": "PUT",
"pathVariableData": [],
- "queryParams": [
+ "queryParams": [],
+ "auth": null,
+ "events": null,
+ "folder": "060feceb-1658-4fee-9d71-0fedc75effe9",
+ "rawModeData": "",
+ "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "pathVariables": {}
+ },
+ {
+ "id": "e323baef-7406-4665-9bbe-3f64ce4a427c",
+ "name": "TC API - get project",
+ "url": "https://api.topcoder-dev.com/v4/projects/1936",
+ "description": "",
+ "data": null,
+ "dataMode": null,
+ "headerData": [
{
- "key": "offset",
- "value": "0",
- "equals": true,
+ "key": "Content-Type",
+ "value": "application/json",
"description": "",
"enabled": true
},
{
- "key": "limit",
- "value": "20",
- "equals": true,
+ "key": "authorization",
+ "value": "Bearer {{TC_ADMIN_TOKEN}}",
"description": "",
"enabled": true
- },
- {
- "key": "type",
- "value": "notifications.connect.project.updated",
- "equals": true,
- "description": "",
- "enabled": false
- },
- {
- "key": "read",
- "value": "false",
- "equals": true,
- "description": "",
- "enabled": false
}
],
+ "method": "GET",
+ "pathVariableData": [],
+ "queryParams": [],
"auth": null,
"events": null,
"folder": null,
- "rawModeData": "",
- "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "responses": [
+ {
+ "id": "3e0e5441-9c98-4a09-a256-9d825e2c76f8",
+ "name": "test111",
+ "status": "",
+ "mime": "",
+ "language": "json",
+ "text": "{\"id\":\"95744bd2-2830-4014-8885-7182a6225953\",\"version\":\"v4\",\"result\":{\"success\":true,\"status\":200,\"content\":{\"id\":1936,\"directProjectId\":12147,\"billingAccountId\":null,\"name\":\"Test-prj\",\"description\":\"Test description\",\"external\":null,\"bookmarks\":[],\"estimatedPrice\":null,\"actualPrice\":null,\"terms\":[],\"type\":\"app_dev\",\"status\":\"draft\",\"details\":{\"products\":[\"api_dev\"],\"appDefinition\":{\"primaryTarget\":\"desktop\",\"goal\":{\"value\":\"Goal\"},\"users\":{\"value\":\"Developers\"},\"notes\":\"Notes\"},\"utm\":{},\"hideDiscussions\":true},\"challengeEligibility\":[],\"cancelReason\":null,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"members\":[{\"id\":2997,\"userId\":305384,\"role\":\"customer\",\"isPrimary\":true,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"projectId\":1936}],\"attachments\":[]},\"metadata\":{\"totalCount\":1}}}",
+ "responseCode": {
+ "code": 200,
+ "name": "OK",
+ "detail": "Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request the response will contain an entity describing or containing the result of the action."
+ },
+ "requestObject": null,
+ "headers": [
+ {
+ "key": "access-control-allow-credentials",
+ "value": "true",
+ "name": "access-control-allow-credentials",
+ "description": "Indicates whether or not the response to the request can be exposed when the credentials flag is true. When used as part of a response to a preflight request, this indicates whether or not the actual request can be made using credentials."
+ },
+ {
+ "key": "access-control-allow-headers",
+ "value": "Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since",
+ "name": "access-control-allow-headers",
+ "description": "Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request."
+ },
+ {
+ "key": "access-control-allow-methods",
+ "value": "GET, POST, OPTIONS, DELETE, PUT, PATCH",
+ "name": "access-control-allow-methods",
+ "description": "Specifies the method or methods allowed when accessing the resource. This is used in response to a preflight request."
+ },
+ {
+ "key": "connection",
+ "value": "keep-alive",
+ "name": "connection",
+ "description": "Options that are desired for the connection"
+ },
+ {
+ "key": "content-encoding",
+ "value": "gzip",
+ "name": "content-encoding",
+ "description": "The type of encoding used on the data."
+ },
+ {
+ "key": "content-length",
+ "value": "491",
+ "name": "content-length",
+ "description": "The length of the response body in octets (8-bit bytes)"
+ },
+ {
+ "key": "content-type",
+ "value": "application/json; charset=utf-8",
+ "name": "content-type",
+ "description": "The mime type of this content"
+ },
+ {
+ "key": "date",
+ "value": "Thu, 02 Nov 2017 04:28:20 GMT",
+ "name": "date",
+ "description": "The date and time that the message was sent"
+ },
+ {
+ "key": "etag",
+ "value": "W/\"3a6-4pbtTNq19Shn10rc0k+HRsoAyMw\"",
+ "name": "etag",
+ "description": "An identifier for a specific version of a resource, often a message digest"
+ },
+ {
+ "key": "server",
+ "value": "nginx/1.9.7",
+ "name": "server",
+ "description": "A name for the server"
+ },
+ {
+ "key": "x-powered-by",
+ "value": "Express",
+ "name": "x-powered-by",
+ "description": "Specifies the technology (ASP.NET, PHP, JBoss, e.g.) supporting the web application (version details are often in X-Runtime, X-Version, or X-AspNet-Version)"
+ },
+ {
+ "key": "x-request-id",
+ "value": "95744bd2-2830-4014-8885-7182a6225953",
+ "name": "x-request-id",
+ "description": "Custom header"
+ }
+ ],
+ "cookies": [],
+ "request": "e323baef-7406-4665-9bbe-3f64ce4a427c",
+ "collection": "8fe3fe1d-744f-4c0e-ac34-e2029e6308e9"
+ }
+ ],
+ "headers": "Content-Type: application/json\nauthorization: Bearer {{TC_ADMIN_TOKEN}}\n",
"pathVariables": {}
}
]
-}
+}
\ No newline at end of file
diff --git a/emails/src/partials/invites.html b/emails/src/partials/invites.html
index 4cd14e2..f616a14 100644
--- a/emails/src/partials/invites.html
+++ b/emails/src/partials/invites.html
@@ -7,7 +7,7 @@
|
-
+
|
{{title}}.
diff --git a/emails/src/partials/project-files.html b/emails/src/partials/project-files.html
index 821001c..6276b17 100644
--- a/emails/src/partials/project-files.html
+++ b/emails/src/partials/project-files.html
@@ -7,7 +7,7 @@
|
-
+
|
{{title}}
diff --git a/emails/src/partials/project-links.html b/emails/src/partials/project-links.html
index f70188a..7fb6d6a 100644
--- a/emails/src/partials/project-links.html
+++ b/emails/src/partials/project-links.html
@@ -7,7 +7,7 @@
|
-
+
|
{{title}}
diff --git a/emails/src/partials/project-plan.html b/emails/src/partials/project-plan.html
index 79a5d87..dcd772e 100644
--- a/emails/src/partials/project-plan.html
+++ b/emails/src/partials/project-plan.html
@@ -7,7 +7,7 @@
|
-
+
|
{{title}}
diff --git a/emails/src/partials/project-specification.html b/emails/src/partials/project-specification.html
index e6258c6..b46edfa 100644
--- a/emails/src/partials/project-specification.html
+++ b/emails/src/partials/project-specification.html
@@ -6,7 +6,7 @@
|
-
+
|
{{title}}
diff --git a/emails/src/partials/project-status.html b/emails/src/partials/project-status.html
index ff62ad4..4e058f8 100644
--- a/emails/src/partials/project-status.html
+++ b/emails/src/partials/project-status.html
@@ -7,7 +7,7 @@
|
-
+
|
{{title}}
diff --git a/emails/src/partials/project-team.html b/emails/src/partials/project-team.html
index 89deb86..bb250e3 100644
--- a/emails/src/partials/project-team.html
+++ b/emails/src/partials/project-team.html
@@ -7,7 +7,7 @@
|
-
+
|
{{title}}
diff --git a/emails/src/partials/topics_and_posts.html b/emails/src/partials/topics_and_posts.html
index f7b6751..73e1152 100644
--- a/emails/src/partials/topics_and_posts.html
+++ b/emails/src/partials/topics_and_posts.html
@@ -7,7 +7,7 @@
|
-
+
|
{{title}}.
diff --git a/emails/src/template.html b/emails/src/template.html
index 609cbff..14c2a4a 100644
--- a/emails/src/template.html
+++ b/emails/src/template.html
@@ -20,7 +20,7 @@
-  |
+  |
YOUR TOPCODER PROJECT UPDATES |
|
diff --git a/package.json b/package.json
index 7e802da..3789de8 100644
--- a/package.json
+++ b/package.json
@@ -7,8 +7,8 @@
"start": "node connect/connectNotificationServer",
"startAPI": "node index-api",
"startConsumer": "node consumer",
- "lint": "eslint *.js src config test connect || true",
- "lint:fix": "eslint *.js --fix src config test connect || true",
+ "lint": "eslint *.js src config test connect",
+ "lint:fix": "eslint *.js --fix src config test connect",
"postinstall": "npm run build",
"build": "gulp build",
"watch": "gulp watch"
@@ -53,8 +53,9 @@
"sequelize": "^4.21.0",
"superagent": "^3.8.0",
"tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.6",
- "winston": "^2.2.0",
- "topcoder-healthcheck-dropin": "^1.0.3"
+ "topcoder-healthcheck-dropin": "^1.0.3",
+ "urijs": "^1.19.1",
+ "winston": "^2.2.0"
},
"engines": {
"node": "6.x"
diff --git a/src/app.js b/src/app.js
index 3ee3e4a..1d7d4bc 100644
--- a/src/app.js
+++ b/src/app.js
@@ -15,7 +15,25 @@ const logger = require('./common/logger');
const errors = require('./common/errors');
const models = require('./models');
const Kafka = require('no-kafka');
-const healthcheck = require('topcoder-healthcheck-dropin')
+const healthcheck = require('topcoder-healthcheck-dropin');
+
+
+// helps in health checking in case of unhandled rejection of promises
+const unhandledRejections = [];
+process.on('unhandledRejection', (reason, promise) => {
+ console.log('Unhandled Rejection at:', promise, 'reason:', reason);
+ // aborts the process to let the HA of the container to restart the task
+ // process.abort();
+ unhandledRejections.push(promise);
+});
+
+// ideally any unhandled rejection is handled after more than one event loop, it should be removed
+// from the unhandledRejections array. We just remove the first element from the array as we only care
+// about the count every time an unhandled rejection promise is handled
+process.on('rejectionHandled', (promise) => {
+ console.log('Handled Rejection at:', promise);
+ unhandledRejections.shift();
+});
/**
* Start Kafka consumer for event bus events.
@@ -75,15 +93,39 @@ function startKafkaConsumer(handlers, notificationServiceHandlers) {
});
});
+ var latestSubscriptions = null;
+
const check = function () {
+ logger.debug("Checking health");
+ if (unhandledRejections && unhandledRejections.length > 0) {
+ logger.error('Found unhandled promises. Application is potentially in stalled state.');
+ return false;
+ }
if (!consumer.client.initialBrokers && !consumer.client.initialBrokers.length) {
- return false
+ logger.error('Found unhealthy Kafka Brokers...');
+ return false;
+ }
+ let connected = true;
+ let currentSubscriptions = consumer.subscriptions;
+ for(var sIdx in currentSubscriptions) {
+ // current subscription
+ let sub = currentSubscriptions[sIdx];
+ // previous subscription
+ let prevSub = latestSubscriptions ? latestSubscriptions[sIdx] : null;
+ // levarage the `paused` field (https://github.com/oleksiyk/kafka/blob/master/lib/base_consumer.js#L66) to
+ // determine if there was a possibility of an unhandled exception. If we find paused status for the same
+ // topic in two consecutive health checks, we assume it was stuck because of unhandled error
+ if (prevSub && prevSub.paused && sub.paused) {
+ logger.error(`Found subscription for ${sIdx} in paused state for consecutive health checks`);
+ return false;
+ }
}
- let connected = true
+ // stores the latest subscription status in global variable
+ latestSubscriptions = consumer.subscriptions;
consumer.client.initialBrokers.forEach(conn => {
logger.debug(`url ${conn.server()} - connected=${conn.connected}`)
connected = conn.connected & connected
- })
+ });
return connected
}
@@ -91,8 +133,8 @@ function startKafkaConsumer(handlers, notificationServiceHandlers) {
.init()
.then(() => {
_.each(_.keys(handlers),
- (topicName) => consumer.subscribe(topicName, dataHandler))
- healthcheck.init([check])
+ (topicName) => consumer.subscribe(topicName, dataHandler));
+ healthcheck.init([check]);
})
.catch((err) => {
logger.error('Kafka Consumer failed');
diff --git a/src/common/tcApiHelper.js b/src/common/tcApiHelper.js
index 37ef659..dcd7489 100644
--- a/src/common/tcApiHelper.js
+++ b/src/common/tcApiHelper.js
@@ -2,6 +2,7 @@
* Contains generic helper methods for TC API
*/
const _ = require('lodash');
+const URI = require('urijs');
const config = require('config');
const request = require('superagent');
const m2mAuth = require('tc-core-library-js').auth.m2m;
@@ -188,8 +189,8 @@ function* notifyUserViaEmail(user, message) {
*/
function* getChallenge(challengeId) {
// this is public API, M2M token is not needed
- const url = `${config.TC_API_V4_BASE_URL}/challenges/${challengeId}`
- logger.info(`calling public challenge api ${url}`)
+ const url = `${config.TC_API_V4_BASE_URL}/challenges/${challengeId}`;
+ logger.info(`calling public challenge api ${url}`);
const res = yield request.get(url);
if (!_.get(res, 'body.result.success')) {
throw new Error(`Failed to get challenge by id ${challengeId}`);
@@ -214,9 +215,9 @@ function* notifyUsersOfMessage(users, notification) {
for (let i = 0; i < users.length; i += 1) {
const user = users[i];
// construct notification, rest fields are set in consumer.js
- notifications.push({ userId: user.userId, notification: notification });
+ notifications.push({ userId: user.userId, notification });
- /* TODO Sachin disabled this code
+ /* TODO Sachin disabled this code
if (config.ENABLE_EMAILS) {
// notify user by email, ignore error in order not to block rest processing
try {
@@ -226,9 +227,8 @@ function* notifyUsersOfMessage(users, notification) {
logger.logFullError(e);
}
} */
-
}
- logger.info(`Total ${notifications.length} users would be notified.`)
+ logger.info(`Total ${notifications.length} users would be notified.`);
return notifications;
}
@@ -238,10 +238,10 @@ function* notifyUsersOfMessage(users, notification) {
* @returns {Array} the associated user's detail object
*/
function* getUsersInfoFromChallenge(challengeId) {
- const token = yield getM2MToken()
- let usersInfo = []
- const url = `${config.TC_API_V4_BASE_URL}/challenges/${challengeId}/resources`
- logger.info(`calling challenge api ${url} `)
+ const token = yield getM2MToken();
+ let usersInfo = [];
+ const url = `${config.TC_API_V4_BASE_URL}/challenges/${challengeId}/resources`;
+ logger.info(`calling challenge api ${url} `);
const res = yield request
.get(url)
.set('Authorization', `Bearer ${token}`)
@@ -250,71 +250,121 @@ function* getUsersInfoFromChallenge(challengeId) {
throw new Error(
`Error in call challenge api by id ${challengeId}` +
(errorDetails ? ' Server response: ' + errorDetails : '')
- )
- })
+ );
+ });
if (!_.get(res, 'body.result.success')) {
throw new Error(`Failed to get challenge by id ${challengeId}`);
}
usersInfo = _.get(res, 'body.result.content');
- logger.info(`Feteched ${usersInfo.length} records from challenge api`)
+ logger.info(`Feteched ${usersInfo.length} records from challenge api`);
return usersInfo;
}
-/**
- * Filter associated challenge's user based on criteria
+/**
+ * Filter associated challenge's user based on criteria
* @param {Array} usersInfo user object array
* @param {Array} filterOnRoles on roles
* @param {Array} filterOnUsers on user's ids
- *
- * @returns {Array} of user object
+ *
+ * @returns {Array} of user object
*/
function filterChallengeUsers(usersInfo, filterOnRoles = [], filterOnUsers = []) {
- const users = [] // filtered users
- const rolesAvailable = [] // available roles in challenge api response
+ const users = []; // filtered users
+ const rolesAvailable = []; // available roles in challenge api response
_.map(usersInfo, (user) => {
- const userId = parseInt(_.get(user, 'properties.External Reference ID'))
- const role = _.get(user, 'role')
+ const userId = parseInt(_.get(user, 'properties.External Reference ID'), 10);
+ const role = _.get(user, 'role');
- _.indexOf(rolesAvailable, role) == -1 ? rolesAvailable.push(role) : ''
+ if (_.indexOf(rolesAvailable, role) === -1) {
+ rolesAvailable.push(role);
+ }
if (filterOnRoles.length > 0 && _.indexOf(filterOnRoles, role) >= 0) {
- users.push({ userId: userId })
+ users.push({ userId });
} else if (filterOnUsers.length > 0 && _.indexOf(filterOnUsers, userId) >= 0) {
- users.push({ userId: userId }) /** Submitter only case */
- } else if (filterOnRoles.length == 0 && filterOnUsers.length == 0) {
- users.push({ userId: userId })
+ users.push({ userId }); /** Submitter only case */
+ } else if (filterOnRoles.length === 0 && filterOnUsers.length === 0) {
+ users.push({ userId });
}
- })
- logger.info(`Total roles available in this challenge are: ${rolesAvailable.join(',')}`)
- return users
+ });
+ logger.info(`Total roles available in this challenge are: ${rolesAvailable.join(',')}`);
+ return users;
}
-/**
- * modify notification template
+/**
+ * modify notification template
* @param {Object} ruleSet rule
- * @param {Object} data values to be filled
- *
+ * @param {Object} data values to be filled
+ *
* @returns {Object} notification node
*/
function* modifyNotificationNode(ruleSet, data) {
- const notification = _.get(ruleSet, "notification")
- const id = data.id || data.challengeId || 0
- const name = _.get(data, "name")
+ const notification = _.get(ruleSet, 'notification');
+ const id = data.id || data.challengeId || 0;
+ const name = _.get(data, 'name');
- notification.id = id
+ notification.id = id;
if (name) {
- notification.name = name
+ notification.name = name;
} else {
try {
- const challenge = yield getChallenge(id)
- notification.name = _.get(challenge, "challengeTitle")
+ const challenge = yield getChallenge(id);
+ notification.name = _.get(challenge, 'challengeTitle');
} catch (error) {
- notification.name = ''
- logger.error(`Error in fetching challenge detail : ${error}`)
+ notification.name = '';
+ logger.error(`Error in fetching challenge detail : ${error}`);
}
}
- return notification
+ return notification;
+}
+
+/**
+ * generate header based on v5 specification
+ * @param {String} url the api url to fetch
+ * @param {Number} perPage the number served in one page
+ * @param {Number} currentPage the current page number
+ * @param {Number} total the total number of rows/entities
+ *
+ * @returns {Object} the header response
+ */
+function generateV5Header({ url, perPage, currentPage, total }) {
+ const links = [];
+ const fullUrl = `${config.TC_API_BASE_URL}${url}`;
+ const generateUrl = (url_, page, rel) => {
+ const newUrl = new URI(url_);
+ newUrl.setQuery({
+ page,
+ });
+ links.push(`<${newUrl.toString()}>; rel="${rel}"`);
+ };
+
+ const totalPages = perPage ? Math.ceil(total / perPage) : 1;
+ const headers = {
+ 'X-Page': currentPage || 1,
+ 'X-Total': total,
+ 'X-Total-Pages': totalPages || 1,
+ };
+ if (perPage) {
+ headers['X-Per-Page'] = perPage;
+ }
+
+ if (currentPage > 1) {
+ headers['X-Prev-Page'] = currentPage - 1;
+ generateUrl(fullUrl, currentPage - 1, 'prev');
+ generateUrl(fullUrl, 1, 'first');
+ }
+
+ if (currentPage < totalPages) {
+ headers['X-Next-Page'] = currentPage + 1;
+
+ generateUrl(fullUrl, currentPage + 1, 'next');
+ generateUrl(fullUrl, totalPages, 'last');
+ }
+
+ headers.Link = links.join(',');
+
+ return headers;
}
module.exports = {
@@ -328,4 +378,5 @@ module.exports = {
getUsersInfoFromChallenge,
filterChallengeUsers,
modifyNotificationNode,
+ generateV5Header,
};
diff --git a/src/controllers/NotificationController.js b/src/controllers/NotificationController.js
index 4b7988b..5f91450 100644
--- a/src/controllers/NotificationController.js
+++ b/src/controllers/NotificationController.js
@@ -4,6 +4,7 @@
'use strict';
const NotificationService = require('../services/NotificationService');
+const tcApiHelper = require('../common/tcApiHelper');
/**
* List notifications.
@@ -11,7 +12,39 @@ const NotificationService = require('../services/NotificationService');
* @param res the response
*/
function* listNotifications(req, res) {
- res.json(yield NotificationService.listNotifications(req.query, req.user.userId));
+ const {
+ items,
+ perPage,
+ currentPage,
+ total,
+ } = yield NotificationService.listNotifications(req.query, req.user.userId);
+
+ const headers = tcApiHelper.generateV5Header({
+ url: req.originalUrl,
+ perPage,
+ currentPage,
+ total,
+ });
+
+ res.set(headers);
+
+ /**
+ * disabling v5 API feature temporarily for connect-app (backward compatibility)
+ */
+
+ //res.json(items);
+
+ // TODO disable this and revert to original
+ res.json({
+ items,
+ offset: currentPage,
+ limit: perPage,
+ totalCount: total
+ })
+}
+
+function* updateNotification(req, res) {
+ res.json(yield NotificationService.updateNotification(req.user.userId, req.params.id, req.body));
}
/**
@@ -71,4 +104,5 @@ module.exports = {
markAsSeen,
getSettings,
updateSettings,
+ updateNotification,
};
diff --git a/src/processors/challenge/AutoPilotHandler.js b/src/processors/challenge/AutoPilotHandler.js
index 47d6e7e..b3592ec 100644
--- a/src/processors/challenge/AutoPilotHandler.js
+++ b/src/processors/challenge/AutoPilotHandler.js
@@ -8,7 +8,7 @@ const service = require('../../services/AutoPilotService');
* Handle Kafka JSON message of autopilot.
*
* @param {Object} message the Kafka JSON message
- * @param {Object} ruleSets
+ * @param {Object} ruleSets
*
* @return {Promise} promise resolved to notifications
*/
diff --git a/src/processors/challenge/ChallengeHandler.js b/src/processors/challenge/ChallengeHandler.js
index 39e60ec..f2056a2 100644
--- a/src/processors/challenge/ChallengeHandler.js
+++ b/src/processors/challenge/ChallengeHandler.js
@@ -8,7 +8,7 @@ const service = require('../../services/ChallengeService');
* Handle Kafka JSON message of challenge created.
*
* @param {Object} message the Kafka JSON message
- * @param {Object} ruleSets
+ * @param {Object} ruleSets
*
* @return {Promise} promise resolved to notifications
*/
diff --git a/src/processors/challenge/SubmissionHandler.js b/src/processors/challenge/SubmissionHandler.js
index 9850563..78b048c 100644
--- a/src/processors/challenge/SubmissionHandler.js
+++ b/src/processors/challenge/SubmissionHandler.js
@@ -8,7 +8,7 @@ const service = require('../../services/SubmissionService');
* Handle Kafka JSON message of autopilot.
*
* @param {Object} message the Kafka JSON message
- * @param {Object} ruleSets
+ * @param {Object} ruleSets
*
* @return {Promise} promise resolved to notifications
*/
diff --git a/src/processors/index.js b/src/processors/index.js
index af3844c..70d8c6a 100644
--- a/src/processors/index.js
+++ b/src/processors/index.js
@@ -6,8 +6,8 @@
const ChallengeCreatedHandler = require('./challenge/ChallengeCreatedHandler');
const ChallengePhaseWarningHandler = require('./challenge/ChallengePhaseWarningHandler');
const ChallengeHandler = require('./challenge/ChallengeHandler');
-const AutoPilotHandler = require('./challenge/AutoPilotHandler')
-const SubmissionHandler = require('./challenge/SubmissionHandler')
+const AutoPilotHandler = require('./challenge/AutoPilotHandler');
+const SubmissionHandler = require('./challenge/SubmissionHandler');
// Exports
module.exports = {
diff --git a/src/routes.js b/src/routes.js
index cf1df2e..c3e2b97 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -7,6 +7,16 @@ module.exports = {
method: 'listNotifications',
},
},
+ '/:id': {
+ patch: {
+ controller: 'NotificationController',
+ method: 'updateNotification',
+ },
+ post: {
+ controller: 'NotificationController',
+ method: 'updateNotification',
+ },
+ },
'/:id/read': {
put: {
controller: 'NotificationController',
diff --git a/src/services/AutoPilotService.js b/src/services/AutoPilotService.js
index 6561daa..cd0ee9a 100644
--- a/src/services/AutoPilotService.js
+++ b/src/services/AutoPilotService.js
@@ -5,32 +5,31 @@
'use strict';
const joi = require('joi');
-const _ = require('lodash')
+const _ = require('lodash');
const logger = require('../common/logger');
const tcApiHelper = require('../common/tcApiHelper');
/**
* Handle autopilot message
* @param {Object} message the Kafka message
- * @param {Object} ruleSets
+ * @param {Object} ruleSets
* @returns {Array} the notifications
*/
function* handle(message, ruleSets) {
+ if ((message.payload.phaseTypeName === _.get(ruleSets, 'phaseTypeName'))
+ && (message.payload.state === _.get(ruleSets, 'state'))) {
+ const challengeId = message.payload.projectId;
+ const filerOnRoles = _.get(ruleSets, 'roles');
- if ((message.payload.phaseTypeName === _.get(ruleSets, "phaseTypeName"))
- && (message.payload.state === _.get(ruleSets, "state"))) {
- const challengeId = message.payload.projectId
- const filerOnRoles = _.get(ruleSets, "roles")
+ const notification = yield tcApiHelper.modifyNotificationNode(ruleSets, { id: challengeId });
+ const usersInfo = yield tcApiHelper.getUsersInfoFromChallenge(challengeId);
+ const users = tcApiHelper.filterChallengeUsers(usersInfo, filerOnRoles);
- const notification = yield tcApiHelper.modifyNotificationNode(ruleSets, { id: challengeId})
- const usersInfo = yield tcApiHelper.getUsersInfoFromChallenge(challengeId)
- const users = tcApiHelper.filterChallengeUsers(usersInfo, filerOnRoles)
-
- logger.info(`Successfully filetered ${users.length} users on rulesets ${JSON.stringify(filerOnRoles)} `)
+ logger.info(`Successfully filetered ${users.length} users on rulesets ${JSON.stringify(filerOnRoles)} `);
// notify users of message
return yield tcApiHelper.notifyUsersOfMessage(users, notification);
}
- return {}
+ return {};
}
handle.schema = {
@@ -42,15 +41,15 @@ handle.schema = {
payload: joi.object().keys({
phaseTypeName: joi.string().required(),
state: joi.string().required(),
- projectId: joi.number().integer().min(1)
+ projectId: joi.number().integer().min(1),
}).unknown(true).required(),
}).required(),
- ruleSets: joi.object()
-}
+ ruleSets: joi.object(),
+};
// Exports
module.exports = {
handle,
-}
+};
logger.buildService(module.exports);
diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js
index 38d6e37..8c3dbb3 100644
--- a/src/services/ChallengeService.js
+++ b/src/services/ChallengeService.js
@@ -5,31 +5,30 @@
'use strict';
const joi = require('joi');
-const _ = require('lodash')
+const _ = require('lodash');
const logger = require('../common/logger');
const tcApiHelper = require('../common/tcApiHelper');
/**
* Handle challenge message
* @param {Object} message the Kafka message
- * @param {Object} ruleSets
+ * @param {Object} ruleSets
* @returns {Array} the notifications
*/
function* handle(message, ruleSets) {
-
- if (message.payload.type === _.get(ruleSets, "type")) {
- const challengeId = message.payload.data.id
- const filterOnRoles = _.get(ruleSets, "roles")
- const challengeTitle = _.get(message.payload, "data.name")
-
- const notification = yield tcApiHelper.modifyNotificationNode(ruleSets, { id: challengeId, name: challengeTitle })
- const usersInfo = yield tcApiHelper.getUsersInfoFromChallenge(challengeId)
- const users = tcApiHelper.filterChallengeUsers(usersInfo, filterOnRoles)
- logger.info(`Successfully filetered ${users.length} users on rulesets ${JSON.stringify(filterOnRoles)} `)
+ if (message.payload.type === _.get(ruleSets, 'type')) {
+ const challengeId = message.payload.data.id;
+ const filterOnRoles = _.get(ruleSets, 'roles');
+ const challengeTitle = _.get(message.payload, 'data.name');
+
+ const notification = yield tcApiHelper.modifyNotificationNode(ruleSets, { id: challengeId, name: challengeTitle });
+ const usersInfo = yield tcApiHelper.getUsersInfoFromChallenge(challengeId);
+ const users = tcApiHelper.filterChallengeUsers(usersInfo, filterOnRoles);
+ logger.info(`Successfully filetered ${users.length} users on rulesets ${JSON.stringify(filterOnRoles)} `);
// notify users of message
return yield tcApiHelper.notifyUsersOfMessage(users, notification);
}
- return {}
+ return {};
}
handle.schema = {
@@ -40,15 +39,15 @@ handle.schema = {
'mime-type': joi.string().required(),
payload: joi.object().keys({
type: joi.string().required(),
- userId: joi.number().integer().min(1)
+ userId: joi.number().integer().min(1),
}).unknown(true).required(),
}).required(),
- ruleSets: joi.object()
-}
+ ruleSets: joi.object(),
+};
// Exports
module.exports = {
handle,
-}
+};
logger.buildService(module.exports);
diff --git a/src/services/NotificationService.js b/src/services/NotificationService.js
index 027116d..9b5ea52 100644
--- a/src/services/NotificationService.js
+++ b/src/services/NotificationService.js
@@ -1,313 +1,360 @@
-/**
- * Service for notification functinoalities.
- */
-
-'use strict';
-
-const _ = require('lodash');
-const Joi = require('joi');
-const errors = require('../common/errors');
+/**
+ * Service for notification functinoalities.
+ */
+
+'use strict';
+
+const _ = require('lodash');
+const Joi = require('joi');
+const errors = require('../common/errors');
const logger = require('../common/logger');
-const models = require('../models');
-
-const DEFAULT_LIMIT = 10;
-
-/**
- * Get notification settings.
- * @param {Number} userId the user id
- * @returns {Object} the notification settings
- */
-function* getSettings(userId) {
- const notificationSettings = yield models.NotificationSetting.findAll({ where: { userId } });
- const serviceSettings = yield models.ServiceSettings.findAll({ where: { userId } });
-
- // format settings per notification type
- const notifications = {};
- _.each(notificationSettings, (setting) => {
- if (!notifications[setting.topic]) {
- notifications[setting.topic] = {};
- }
- if (!notifications[setting.topic][setting.serviceId]) {
- notifications[setting.topic][setting.serviceId] = {};
- }
- notifications[setting.topic][setting.serviceId][setting.name] = setting.value;
- });
-
- // format settings per service
- const services = {};
- _.each(serviceSettings, (setting) => {
- if (!services[setting.serviceId]) {
- services[setting.serviceId] = {};
- }
- services[setting.serviceId][setting.name] = setting.value;
- });
- return {
- notifications,
- services,
- };
-}
-
-getSettings.schema = {
- userId: Joi.number().required(),
-};
-
-/**
- * Save notification setting entry. If the entry is not found, it will be created; otherwise it will be updated.
- * @param {Object} entry the notification setting entry
- * @param {Number} userId the user id
- */
-function* saveNotificationSetting(entry, userId) {
- const setting = yield models.NotificationSetting.findOne({ where: {
- userId, topic: entry.topic, serviceId: entry.serviceId, name: entry.name } });
- if (setting) {
- setting.value = entry.value;
- yield setting.save();
- } else {
- yield models.NotificationSetting.create({
- userId,
- topic: entry.topic,
- serviceId: entry.serviceId,
- name: entry.name,
- value: entry.value,
- });
- }
-}
-
-/**
- * Save service setting entry. If the entry is not found, it will be created; otherwise it will be updated.
- * @param {Object} entry the service setting entry
- * @param {Number} userId the user id
- */
-function* saveServiceSetting(entry, userId) {
- const setting = yield models.ServiceSettings.findOne({ where: {
- userId, serviceId: entry.serviceId, name: entry.name } });
- if (setting) {
- setting.value = entry.value;
- yield setting.save();
- } else {
- yield models.ServiceSettings.create({
- userId,
- serviceId: entry.serviceId,
- name: entry.name,
- value: entry.value,
- });
- }
-}
-
-/**
- * Update notification settings. Un-specified settings are not changed.
- * @param {Array} data the notification settings data
- * @param {Number} userId the user id
- */
-function* updateSettings(data, userId) {
- // convert notification settings object to the list of entries
- const notifications = [];
- _.forOwn(data.notifications, (notification, topic) => {
- _.forOwn(notification, (serviceSettings, serviceId) => {
- _.forOwn(serviceSettings, (value, name) => {
- notifications.push({
- topic,
- serviceId,
- name,
- value,
- });
- });
- });
- });
-
- // validation
- // there should be no duplicate (topic + serviceId + name)
- const triples = {};
- notifications.forEach((entry) => {
- const key = `${entry.topic} | ${entry.serviceId} | ${entry.name}`;
- if (triples[key]) {
- throw new errors.BadRequestError(`There are duplicate data for topic: ${
- entry.topic}, serviceId: ${entry.serviceId}, name: ${entry.name}`);
- }
- triples[key] = entry;
- });
-
- // save each entry in parallel
- yield _.map(notifications, (entry) => saveNotificationSetting(entry, userId));
-
- // convert services settings object the the list of entries
- const services = [];
- _.forOwn(data.services, (service, serviceId) => {
- _.forOwn(service, (value, name) => {
- services.push({
- serviceId,
- name,
- value,
- });
- });
- });
-
- // validation
- // there should be no duplicate (serviceId + name)
- const paris = {};
- services.forEach((entry) => {
- const key = `${entry.serviceId} | ${entry.name}`;
- if (paris[key]) {
- throw new errors.BadRequestError('There are duplicate data for'
- + ` serviceId: ${entry.serviceId}, name: ${entry.name}`);
- }
- paris[key] = entry;
- });
-
- yield _.map(services, (entry) => saveServiceSetting(entry, userId));
-}
-
-updateSettings.schema = {
- data: Joi.object().keys({
- notifications: Joi.object(),
- services: Joi.object(),
- }).required(),
- userId: Joi.number().required(),
-};
-
-/**
- * List notifications.
- *
- * This method returns only notifications for 'web'
- * Also this method filters notifications by the user and filters out notifications,
- * which user disabled in his settings.
- *
- * @param {Object} query the query parameters
- * @param {Number} userId the user id
- * @returns {Object} the search result
- */
-function* listNotifications(query, userId) {
- const settings = yield getSettings(userId);
- const notificationSettings = settings.notifications;
-
- const filter = { where: {
- userId,
- }, offset: query.offset, limit: query.limit, order: [['createdAt', 'DESC']] };
- if (_.keys(notificationSettings).length > 0) {
- // only filter out notifications types which were explicitly set to 'no' - so we return notification by default
- const notifications = _.keys(notificationSettings).filter((notificationType) =>
- !notificationSettings[notificationType] &&
- !notificationSettings[notificationType].web &&
- notificationSettings[notificationType].web.enabled === 'no'
- );
- filter.where.type = { $notIn: notifications };
- }
- if (query.type) {
- filter.where.type = query.type;
- }
- if (query.read) {
- filter.where.read = (query.read === 'true');
- }
- const docs = yield models.Notification.findAndCountAll(filter);
- const items = _.map(docs.rows, r => {
- const item = r.toJSON();
- // id and userId are BIGINT in database, sequelize maps them to string values,
- // convert them back to Number values
- item.id = Number(item.id);
- item.userId = Number(item.userId);
- return item;
- });
- return {
- items,
- offset: query.offset,
- limit: query.limit,
- totalCount: docs.count,
- };
-}
-
-listNotifications.schema = {
- query: Joi.object().keys({
- offset: Joi.number().integer().min(0).default(0),
- limit: Joi.number().integer().min(1).default(DEFAULT_LIMIT),
- type: Joi.string(),
- // when it is true, return only read notifications
- // when it is false, return only un-read notifications
- // when it is no provided, no read flag filtering
- read: Joi.string().valid('true', 'false'),
- }).required(),
- userId: Joi.number().required(),
-};
-
-/**
- * Mark notification(s) as read.
- * @param {Number} id the notification id or '-' separated ids
- * @param {Number} userId the user id
- */
-function* markAsRead(id, userId) {
- const ids = _.map(id.split('-'), (str) => {
- const idInt = Number(str);
- if (!_.isInteger(idInt)) {
- throw new errors.BadRequestError(`Notification id should be integer: ${str}`);
- }
- return idInt;
- });
- const entities = yield models.Notification.findAll({ where: { id: { $in: ids }, read: false } });
- if (!entities || entities.length === 0) {
- throw new errors.NotFoundError(`Cannot find un-read Notification where id = ${id}`);
- }
- _.each(entities, (entity) => {
- if (Number(entity.userId) !== userId) {
- throw new errors.ForbiddenError(`Cannot access Notification where id = ${entity.id}`);
- }
- });
- yield models.Notification.update({ read: true }, { where: { id: { $in: ids }, read: false } });
-}
-
-markAsRead.schema = {
- id: Joi.string().required(),
- userId: Joi.number().required(),
-};
-
-/**
- * Mark all notifications as read.
- * @param {Number} userId the user id
- */
-function* markAllRead(userId) {
- yield models.Notification.update({ read: true }, { where: { userId, read: false } });
-}
-
-markAllRead.schema = {
- userId: Joi.number().required(),
-};
-
-/**
- * Mark notification(s) as seen.
- * @param {Number} id the notification id or '-' separated ids
- * @param {Number} userId the user id
- */
-function* markAsSeen(id, userId) {
- const ids = _.map(id.split('-'), (str) => {
- const idInt = Number(str);
- if (!_.isInteger(idInt)) {
- throw new errors.BadRequestError(`Notification id should be integer: ${str}`);
- }
- return idInt;
- });
- const entities = yield models.Notification.findAll({ where: { id: { $in: ids }, seen: { $not: true } } });
- if (!entities || entities.length === 0) {
- throw new errors.NotFoundError(`Cannot find un-seen Notification where id = ${id}`);
- }
- _.each(entities, (entity) => {
- if (Number(entity.userId) !== userId) {
- throw new errors.ForbiddenError(`Cannot access Notification where id = ${entity.id}`);
- }
- });
- yield models.Notification.update({ seen: true }, { where: { id: { $in: ids }, seen: { $not: true } } });
-}
-
-markAsSeen.schema = {
- id: Joi.string().required(),
- userId: Joi.number().required(),
-};
-
-// Exports
-module.exports = {
- listNotifications,
- markAsRead,
- markAllRead,
- markAsSeen,
- getSettings,
- updateSettings,
-};
+const models = require('../models');
+
+const DEFAULT_LIMIT = 10;
+
+/**
+ * Get notification settings.
+ * @param {Number} userId the user id
+ * @returns {Object} the notification settings
+ */
+function* getSettings(userId) {
+ const notificationSettings = yield models.NotificationSetting.findAll({ where: { userId } });
+ const serviceSettings = yield models.ServiceSettings.findAll({ where: { userId } });
+
+ // format settings per notification type
+ const notifications = {};
+ _.each(notificationSettings, (setting) => {
+ if (!notifications[setting.topic]) {
+ notifications[setting.topic] = {};
+ }
+ if (!notifications[setting.topic][setting.serviceId]) {
+ notifications[setting.topic][setting.serviceId] = {};
+ }
+ notifications[setting.topic][setting.serviceId][setting.name] = setting.value;
+ });
+
+ // format settings per service
+ const services = {};
+ _.each(serviceSettings, (setting) => {
+ if (!services[setting.serviceId]) {
+ services[setting.serviceId] = {};
+ }
+ services[setting.serviceId][setting.name] = setting.value;
+ });
+ return {
+ notifications,
+ services,
+ };
+}
+
+getSettings.schema = {
+ userId: Joi.number().required(),
+};
+
+/**
+ * Save notification setting entry. If the entry is not found, it will be created; otherwise it will be updated.
+ * @param {Object} entry the notification setting entry
+ * @param {Number} userId the user id
+ */
+function* saveNotificationSetting(entry, userId) {
+ const setting = yield models.NotificationSetting.findOne({ where: {
+ userId, topic: entry.topic, serviceId: entry.serviceId, name: entry.name } });
+ if (setting) {
+ setting.value = entry.value;
+ yield setting.save();
+ } else {
+ yield models.NotificationSetting.create({
+ userId,
+ topic: entry.topic,
+ serviceId: entry.serviceId,
+ name: entry.name,
+ value: entry.value,
+ });
+ }
+}
+
+/**
+ * Save service setting entry. If the entry is not found, it will be created; otherwise it will be updated.
+ * @param {Object} entry the service setting entry
+ * @param {Number} userId the user id
+ */
+function* saveServiceSetting(entry, userId) {
+ const setting = yield models.ServiceSettings.findOne({ where: {
+ userId, serviceId: entry.serviceId, name: entry.name } });
+ if (setting) {
+ setting.value = entry.value;
+ yield setting.save();
+ } else {
+ yield models.ServiceSettings.create({
+ userId,
+ serviceId: entry.serviceId,
+ name: entry.name,
+ value: entry.value,
+ });
+ }
+}
+
+/**
+ * Update notification settings. Un-specified settings are not changed.
+ * @param {Array} data the notification settings data
+ * @param {Number} userId the user id
+ */
+function* updateSettings(data, userId) {
+ // convert notification settings object to the list of entries
+ const notifications = [];
+ _.forOwn(data.notifications, (notification, topic) => {
+ _.forOwn(notification, (serviceSettings, serviceId) => {
+ _.forOwn(serviceSettings, (value, name) => {
+ notifications.push({
+ topic,
+ serviceId,
+ name,
+ value,
+ });
+ });
+ });
+ });
+
+ // validation
+ // there should be no duplicate (topic + serviceId + name)
+ const triples = {};
+ notifications.forEach((entry) => {
+ const key = `${entry.topic} | ${entry.serviceId} | ${entry.name}`;
+ if (triples[key]) {
+ throw new errors.BadRequestError(`There are duplicate data for topic: ${
+ entry.topic}, serviceId: ${entry.serviceId}, name: ${entry.name}`);
+ }
+ triples[key] = entry;
+ });
+
+ // save each entry in parallel
+ yield _.map(notifications, (entry) => saveNotificationSetting(entry, userId));
+
+ // convert services settings object the the list of entries
+ const services = [];
+ _.forOwn(data.services, (service, serviceId) => {
+ _.forOwn(service, (value, name) => {
+ services.push({
+ serviceId,
+ name,
+ value,
+ });
+ });
+ });
+
+ // validation
+ // there should be no duplicate (serviceId + name)
+ const paris = {};
+ services.forEach((entry) => {
+ const key = `${entry.serviceId} | ${entry.name}`;
+ if (paris[key]) {
+ throw new errors.BadRequestError('There are duplicate data for'
+ + ` serviceId: ${entry.serviceId}, name: ${entry.name}`);
+ }
+ paris[key] = entry;
+ });
+
+ yield _.map(services, (entry) => saveServiceSetting(entry, userId));
+}
+
+updateSettings.schema = {
+ data: Joi.object().keys({
+ notifications: Joi.object(),
+ services: Joi.object(),
+ }).required(),
+ userId: Joi.number().required(),
+};
+
+/**
+ * List notifications.
+ *
+ * This method returns only notifications for 'web'
+ * Also this method filters notifications by the user and filters out notifications,
+ * which user disabled in his settings.
+ *
+ * @param {Object} query the query parameters
+ * @param {Number} userId the user id
+ * @returns {Object} the search result
+ */
+function* listNotifications(query, userId) {
+ const settings = yield getSettings(userId);
+ const notificationSettings = settings.notifications;
+ const limit = query.limit || query.per_page;
+ const offset = (query.page - 1) * limit;
+ const filter = { where: {
+ userId,
+ }, offset, limit, order: [['createdAt', 'DESC']] };
+ if (query.platform) {
+ filter.where.type = { $like: `notifications\.${query.platform}\.%` };
+ }
+ if (_.keys(notificationSettings).length > 0) {
+ // only filter out notifications types which were explicitly set to 'no' - so we return notification by default
+ const notifications = _.keys(notificationSettings).filter((notificationType) =>
+ !notificationSettings[notificationType] &&
+ !notificationSettings[notificationType].web &&
+ notificationSettings[notificationType].web.enabled === 'no'
+ );
+ filter.where.type = Object.assign(filter.where.type || {}, { $notIn: notifications });
+ }
+ if (query.type) {
+ filter.where.type = Object.assign(filter.where.type || {}, { $eq: query.type });
+ }
+ if (query.read) {
+ filter.where.read = (query.read === 'true');
+ }
+ const docs = yield models.Notification.findAndCountAll(filter);
+ const items = _.map(docs.rows, r => {
+ const item = r.toJSON();
+ // id and userId are BIGINT in database, sequelize maps them to string values,
+ // convert them back to Number values
+ item.id = Number(item.id);
+ item.userId = Number(item.userId);
+ return item;
+ });
+ return {
+ items,
+ perPage: limit,
+ currentPage: query.page,
+ total: docs.count,
+ };
+}
+
+listNotifications.schema = {
+ query: Joi.object().keys({
+ page: Joi.number().integer().min(1).default(1),
+ per_page: Joi.number().integer().min(1).default(DEFAULT_LIMIT),
+ // supporting limit field temporarily
+ limit: Joi.number().integer().min(1),
+ type: Joi.string(),
+ platform: Joi.string(),
+ // when it is true, return only read notifications
+ // when it is false, return only un-read notifications
+ // when it is no provided, no read flag filtering
+ read: Joi.string().valid('true', 'false'),
+ }).required(),
+ userId: Joi.number().required(),
+};
+
+/**
+ * Update notification.
+ *
+ * Update notification based on notification id
+ *
+ * @param {Number} userId the user id
+ * @param {Number} notificationId the notification id
+ * @param {Object} payload the update notification payload
+ * @returns {Object} the updated notification
+ */
+function* updateNotification(userId, notificationId, payload) {
+ if (payload.read === false) {
+ throw new errors.ValidationError('Cannot set notification to be unread');
+ }
+ if (payload.seen === false) {
+ throw new errors.ValidationError('Cannot set notification to be unseen');
+ }
+
+ const entity = yield models.Notification.findOne({ where: { id: Number(notificationId) } });
+ if (!entity) {
+ throw new errors.NotFoundError(`Cannot find Notification where id = ${notificationId}`);
+ }
+ if (Number(entity.userId) !== userId) {
+ throw new errors.ForbiddenError(`Cannot access Notification where id = ${entity.id}`);
+ }
+ yield models.Notification.update(payload, { where: { id: Number(notificationId), userId: Number(userId) } });
+
+ return Object.assign(entity, payload);
+}
+
+updateNotification.schema = {
+ userId: Joi.number().required(),
+ notificationId: Joi.number().required(),
+ payload: Joi.object().keys({
+ read: Joi.boolean(),
+ seen: Joi.boolean(),
+ }),
+};
+
+/**
+ * Mark notification(s) as read.
+ * @param {Number} id the notification id or '-' separated ids
+ * @param {Number} userId the user id
+ */
+function* markAsRead(id, userId) {
+ const ids = _.map(id.split('-'), (str) => {
+ const idInt = Number(str);
+ if (!_.isInteger(idInt)) {
+ throw new errors.BadRequestError(`Notification id should be integer: ${str}`);
+ }
+ return idInt;
+ });
+ const entities = yield models.Notification.findAll({ where: { id: { $in: ids }, read: false } });
+ if (!entities || entities.length === 0) {
+ throw new errors.NotFoundError(`Cannot find un-read Notification where id = ${id}`);
+ }
+ _.each(entities, (entity) => {
+ if (Number(entity.userId) !== userId) {
+ throw new errors.ForbiddenError(`Cannot access Notification where id = ${entity.id}`);
+ }
+ });
+ yield models.Notification.update({ read: true }, { where: { id: { $in: ids }, read: false } });
+}
+
+markAsRead.schema = {
+ id: Joi.string().required(),
+ userId: Joi.number().required(),
+};
+
+/**
+ * Mark all notifications as read.
+ * @param {Number} userId the user id
+ */
+function* markAllRead(userId) {
+ yield models.Notification.update({ read: true }, { where: { userId, read: false } });
+}
+
+markAllRead.schema = {
+ userId: Joi.number().required(),
+};
+
+/**
+ * Mark notification(s) as seen.
+ * @param {Number} id the notification id or '-' separated ids
+ * @param {Number} userId the user id
+ */
+function* markAsSeen(id, userId) {
+ const ids = _.map(id.split('-'), (str) => {
+ const idInt = Number(str);
+ if (!_.isInteger(idInt)) {
+ throw new errors.BadRequestError(`Notification id should be integer: ${str}`);
+ }
+ return idInt;
+ });
+ const entities = yield models.Notification.findAll({ where: { id: { $in: ids }, seen: { $not: true } } });
+ if (!entities || entities.length === 0) {
+ throw new errors.NotFoundError(`Cannot find un-seen Notification where id = ${id}`);
+ }
+ _.each(entities, (entity) => {
+ if (Number(entity.userId) !== userId) {
+ throw new errors.ForbiddenError(`Cannot access Notification where id = ${entity.id}`);
+ }
+ });
+ yield models.Notification.update({ seen: true }, { where: { id: { $in: ids }, seen: { $not: true } } });
+}
+
+markAsSeen.schema = {
+ id: Joi.string().required(),
+ userId: Joi.number().required(),
+};
+
+// Exports
+module.exports = {
+ listNotifications,
+ markAsRead,
+ markAllRead,
+ markAsSeen,
+ getSettings,
+ updateSettings,
+ updateNotification,
+};
logger.buildService(module.exports);
diff --git a/src/services/SubmissionService.js b/src/services/SubmissionService.js
index b3737dd..14f5e71 100644
--- a/src/services/SubmissionService.js
+++ b/src/services/SubmissionService.js
@@ -5,36 +5,35 @@
'use strict';
const joi = require('joi');
-const _ = require('lodash')
+const _ = require('lodash');
const logger = require('../common/logger');
const tcApiHelper = require('../common/tcApiHelper');
/**
* Handle submission message
* @param {Object} message the Kafka message
- * @param {Object} ruleSets
+ * @param {Object} ruleSets
* @returns {Array} the notifications
*/
function* handle(message, ruleSets) {
+ if (message.payload.resource === _.get(ruleSets, 'resource')) {
+ const challengeId = message.payload.challengeId;
+ const filterOnRoles = _.get(ruleSets, 'roles');
- if (message.payload.resource === _.get(ruleSets, "resource")) {
- const challengeId = message.payload.challengeId
- const filterOnRoles = _.get(ruleSets, "roles")
-
- const filterOnUsers = []
+ const filterOnUsers = [];
if (_.get(ruleSets, 'selfOnly')) {
- const memberId = _.get(message.payload, "memberId")
- filterOnUsers.push(memberId)
+ const memberId = _.get(message.payload, 'memberId');
+ filterOnUsers.push(memberId);
}
- const usersInfo = yield tcApiHelper.getUsersInfoFromChallenge(challengeId)
- const users = tcApiHelper.filterChallengeUsers(usersInfo, filterOnRoles, filterOnUsers)
- const notification = yield tcApiHelper.modifyNotificationNode(ruleSets, { id: challengeId})
- logger.info(`Successfully filetered ${users.length} users on rulesets ${JSON.stringify(filterOnRoles)} `)
+ const usersInfo = yield tcApiHelper.getUsersInfoFromChallenge(challengeId);
+ const users = tcApiHelper.filterChallengeUsers(usersInfo, filterOnRoles, filterOnUsers);
+ const notification = yield tcApiHelper.modifyNotificationNode(ruleSets, { id: challengeId });
+ logger.info(`Successfully filetered ${users.length} users on rulesets ${JSON.stringify(filterOnRoles)} `);
// notify users of message
return yield tcApiHelper.notifyUsersOfMessage(users, notification);
}
- return {}
+ return {};
}
handle.schema = {
@@ -44,15 +43,15 @@ handle.schema = {
timestamp: joi.date().required(),
'mime-type': joi.string().required(),
payload: joi.object().keys({
- resource: joi.string().required()
+ resource: joi.string().required(),
}).unknown(true).required(),
}).required(),
- ruleSets: joi.object()
-}
+ ruleSets: joi.object(),
+};
// Exports
module.exports = {
handle,
-}
+};
logger.buildService(module.exports);
| | | | | | | | |