Skip to content

Commit 0d3325f

Browse files
committed
Add phase notification flag to resources
1 parent 3ba41c7 commit 0d3325f

File tree

12 files changed

+282
-5
lines changed

12 files changed

+282
-5
lines changed

docs/swagger.yaml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,51 @@ paths:
228228
$ref: '#/definitions/NotFound'
229229
'500':
230230
$ref: '#/definitions/ServerError'
231+
'/resources/{resourceId}/phase-change-notifications':
232+
put:
233+
description: |
234+
Update the phase change notification setting for a specific resource. The preference can only be changed by the resource owner, an administrator, or an authorized machine token.
235+
236+
### Authentication
237+
- JWT roles: `administrator`, `copilot`, `Connect Manager`, `Topcoder User`
238+
- M2M scopes: `update:resources`, `all:resources`
239+
tags:
240+
- Resources
241+
security:
242+
- Bearer: []
243+
parameters:
244+
- name: resourceId
245+
in: path
246+
required: true
247+
type: string
248+
format: UUID
249+
description: The resource identifier.
250+
- in: body
251+
name: payload
252+
required: true
253+
schema:
254+
type: object
255+
required:
256+
- phaseChangeNotifications
257+
properties:
258+
phaseChangeNotifications:
259+
type: boolean
260+
description: Set to `true` to receive notifications when a challenge phase changes; `false` to opt out.
261+
responses:
262+
'200':
263+
description: OK - the preference was updated successfully.
264+
schema:
265+
$ref: '#/definitions/Resource'
266+
'400':
267+
$ref: '#/definitions/BadRequest'
268+
'401':
269+
$ref: '#/definitions/Unauthorized'
270+
'403':
271+
$ref: '#/definitions/Forbidden'
272+
'404':
273+
$ref: '#/definitions/NotFound'
274+
'500':
275+
$ref: '#/definitions/ServerError'
231276
/resources/internal/jobs/clean:
232277
post:
233278
description: |
@@ -592,6 +637,7 @@ definitions:
592637
- memberId
593638
- memberHandle
594639
- roleId
640+
- phaseChangeNotifications
595641
- created
596642
properties:
597643
id:
@@ -609,6 +655,9 @@ definitions:
609655
type: string
610656
format: UUID
611657
description: The resource role ID
658+
phaseChangeNotifications:
659+
type: boolean
660+
description: Indicates whether the member receives notifications when challenge phases change.
612661
created:
613662
type: string
614663
format: date-time

migrator/src/migrators/migrateResource.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ async function migrateResource(filePath) {
2727
const jsonLine = JSON.parse(line);
2828
const data = jsonLine._source;
2929
const createdBy = data.createdBy || process.env.CREATED_BY;
30+
const phaseChangeNotifications = Object.prototype.hasOwnProperty.call(data, 'phaseChangeNotifications')
31+
? data.phaseChangeNotifications
32+
: null;
3033

3134
await prisma.resource.upsert({
3235
where: { id: data.id },
@@ -38,7 +41,8 @@ async function migrateResource(filePath) {
3841
createdAt: data.created ? new Date(data.created) : new Date(),
3942
createdBy,
4043
updatedAt: data.updatedAt ? new Date(data.updatedAt) : null,
41-
updatedBy: data.updatedBy || null
44+
updatedBy: data.updatedBy || null,
45+
phaseChangeNotifications
4246
},
4347
create: {
4448
id: data.id,
@@ -49,7 +53,8 @@ async function migrateResource(filePath) {
4953
createdAt: data.created ? new Date(data.created) : new Date(),
5054
createdBy,
5155
updatedAt: data.updatedAt ? new Date(data.updatedAt) : null,
52-
updatedBy: data.updatedBy || null
56+
updatedBy: data.updatedBy || null,
57+
phaseChangeNotifications
5358
}
5459
});
5560

migrator/src/migrators/migrateResourceBatch.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ async function migrateResource(filePath) {
3333
const results = await Promise.allSettled(
3434
batch.map(data => {
3535
const createdBy = data.createdBy || process.env.CREATED_BY;
36+
const phaseChangeNotifications = Object.prototype.hasOwnProperty.call(data, 'phaseChangeNotifications')
37+
? data.phaseChangeNotifications
38+
: null;
3639

3740
return prisma.resource.upsert({
3841
where: { id: data.id },
@@ -44,7 +47,8 @@ async function migrateResource(filePath) {
4447
createdAt: data.created ? new Date(data.created) : new Date(),
4548
createdBy,
4649
updatedAt: data.updatedAt ? new Date(data.updatedAt) : null,
47-
updatedBy: data.updatedBy || null
50+
updatedBy: data.updatedBy || null,
51+
phaseChangeNotifications
4852
},
4953
create: {
5054
id: data.id,
@@ -55,7 +59,8 @@ async function migrateResource(filePath) {
5559
createdAt: data.created ? new Date(data.created) : new Date(),
5660
createdBy,
5761
updatedAt: data.updatedAt ? new Date(data.updatedAt) : null,
58-
updatedBy: data.updatedBy || null
62+
updatedBy: data.updatedBy || null,
63+
phaseChangeNotifications
5964
}
6065
}).catch(err => {
6166
failCount++;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- Add phaseChangeNotifications flag to Resource
2+
ALTER TABLE "Resource" ADD COLUMN "phaseChangeNotifications" BOOLEAN;
3+
ALTER TABLE "Resource" ALTER COLUMN "phaseChangeNotifications" SET DEFAULT TRUE;

prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ model Resource {
3535
memberHandle String
3636
roleId String
3737
legacyId Int?
38+
phaseChangeNotifications Boolean? @default(true)
3839
createdAt DateTime @default(now())
3940
createdBy String
4041
updatedAt DateTime? @updatedAt

src/controllers/ResourceController.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ async function deleteResource (req, res) {
3636
res.send(result)
3737
}
3838

39+
/**
40+
* Update the phase change notifications preference for a resource.
41+
* @param {Object} req the request
42+
* @param {Object} res the response
43+
*/
44+
async function updatePhaseChangeNotifications (req, res) {
45+
const result = await service.updatePhaseChangeNotifications(req.authUser, req.params.resourceId, req.body)
46+
res.send(result)
47+
}
48+
3949
/**
4050
* List all challenge ids that given member has access to.
4151
* @param {Object} req the request
@@ -61,6 +71,7 @@ module.exports = {
6171
getResources,
6272
createResource,
6373
deleteResource,
74+
updatePhaseChangeNotifications,
6475
listChallengesByMember,
6576
getResourceCount
6677
}

src/routes.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ module.exports = {
4242
scopes: [READ, ALL]
4343
}
4444
},
45+
'/resources/:resourceId/phase-change-notifications': {
46+
put: {
47+
controller: 'ResourceController',
48+
method: 'updatePhaseChangeNotifications',
49+
auth: 'jwt',
50+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager, constants.UserRoles.User],
51+
scopes: [UPDATE, ALL]
52+
}
53+
},
4554
'/resources/internal/jobs/clean': {
4655
post: {
4756
controller: 'CleanUpController',

src/services/ResourceService.js

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const errors = require('../common/errors')
1414
const constants = require('../../app-constants')
1515
const prisma = require('../common/prisma').getClient()
1616

17-
const payloadFields = ['id', 'challengeId', 'memberId', 'memberHandle', 'roleId', 'created', 'createdBy', 'updated', 'updatedBy']
17+
const payloadFields = ['id', 'challengeId', 'memberId', 'memberHandle', 'roleId', 'phaseChangeNotifications', 'created', 'createdBy', 'updated', 'updatedBy']
1818

1919
let copilotResourceRoleIdsCache
2020

@@ -170,6 +170,7 @@ async function getResources (currentUser, challengeId, roleId, memberId, memberH
170170
resources = _.map(resources, item => {
171171
const ret = _.omit(item, 'updatedBy', 'updatedAt', 'createdAt')
172172
ret.created = item.createdAt
173+
ret.phaseChangeNotifications = Boolean(item.phaseChangeNotifications)
173174
return ret
174175
})
175176

@@ -423,6 +424,7 @@ async function createResource (currentUser, resource) {
423424
let ret = _.pick(createdResource, payloadFields)
424425
ret.created = createdResource.createdAt
425426
ret.updated = createdResource.updatedAt
427+
ret.phaseChangeNotifications = Boolean(createdResource.phaseChangeNotifications)
426428

427429
logger.debug(`Created resource: ${JSON.stringify(ret)}`)
428430
await helper.postEvent(config.RESOURCE_CREATE_TOPIC, ret)
@@ -525,6 +527,7 @@ async function deleteResource (currentUser, resource) {
525527
created: ret.createdAt,
526528
updated: ret.updatedAt
527529
}
530+
ret.phaseChangeNotifications = Boolean(ret.phaseChangeNotifications)
528531
await prisma.resource.deleteMany({ where: { id: ret.id } })
529532

530533
logger.debug(`Deleted resource, posting to Bus API: ${JSON.stringify(ret)}`)
@@ -568,6 +571,59 @@ deleteResource.schema = {
568571
)
569572
}
570573

574+
/**
575+
* Update the phase change notifications preference for a resource.
576+
* @param {Object} currentUser the current user
577+
* @param {String} resourceId the resource id
578+
* @param {Object} payload the incoming payload
579+
* @returns {Object} the updated resource
580+
*/
581+
async function updatePhaseChangeNotifications (currentUser, resourceId, payload) {
582+
logger.debug(`updatePhaseChangeNotifications ${JSON.stringify([resourceId, payload])}`)
583+
584+
const resource = await prisma.resource.findUnique({ where: { id: resourceId } })
585+
586+
if (!resource) {
587+
throw new errors.NotFoundError(`Resource with id ${resourceId} not found`)
588+
}
589+
590+
const isMachineUser = Boolean(currentUser && currentUser.isMachine)
591+
const isAdminUser = Boolean(currentUser && helper.hasAdminRole(currentUser))
592+
593+
if (!isMachineUser && !isAdminUser) {
594+
if (!currentUser || _.toString(resource.memberId) !== _.toString(currentUser.userId)) {
595+
throw new errors.ForbiddenError('You may only update your own phase change notification preference.')
596+
}
597+
}
598+
599+
const updatedBy = isMachineUser
600+
? (currentUser.sub || currentUser.clientId || 'system')
601+
: _.toString(currentUser.userId)
602+
603+
const updatedResource = await prisma.resource.update({
604+
where: { id: resourceId },
605+
data: {
606+
phaseChangeNotifications: payload.phaseChangeNotifications,
607+
updatedBy
608+
}
609+
})
610+
611+
const ret = _.pick(updatedResource, payloadFields)
612+
ret.created = updatedResource.createdAt
613+
ret.updated = updatedResource.updatedAt
614+
ret.phaseChangeNotifications = Boolean(updatedResource.phaseChangeNotifications)
615+
616+
return ret
617+
}
618+
619+
updatePhaseChangeNotifications.schema = {
620+
currentUser: Joi.any(),
621+
resourceId: Joi.id().required(),
622+
payload: Joi.object().keys({
623+
phaseChangeNotifications: Joi.boolean().required()
624+
}).required()
625+
}
626+
571627
/**
572628
* List all challenge ids that given member has access to.
573629
* @param {Number} memberId the member id
@@ -658,6 +714,7 @@ module.exports = {
658714
getResources,
659715
createResource,
660716
deleteResource,
717+
updatePhaseChangeNotifications,
661718
listChallengesByMember,
662719
getResourceCount
663720
}

test/common/testHelper.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ async function assertResource (id, expected) {
141141
should.equal(entity.memberId, expected.memberId)
142142
should.equal(entity.memberHandle.toLowerCase(), expected.memberHandle.toLowerCase())
143143
should.equal(entity.roleId, expected.roleId)
144+
if (typeof expected.phaseChangeNotifications !== 'undefined') {
145+
should.equal(Boolean(entity.phaseChangeNotifications), expected.phaseChangeNotifications)
146+
}
144147
should.exist(expected.created)
145148
should.equal(entity.createdBy, expected.createdBy)
146149
}

test/unit/getResources.test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
const should = require('should')
66
const service = require('../../src/services/ResourceService')
77
const helper = require('../../src/common/helper')
8+
const prisma = require('../../src/common/prisma').getClient()
89
const { user } = require('../common/testData')
910
const { assertValidationError, assertError, getRoleIds } = require('../common/testHelper')
1011

@@ -145,4 +146,24 @@ module.exports = describe('Get resources', () => {
145146
assertError(err, `Challenge ID ${challengeNotFoundId} not found`)
146147
}
147148
})
149+
150+
it('returns false when phaseChangeNotifications is null', async () => {
151+
const target = await prisma.resource.findFirst({ where: { challengeId } })
152+
should.exist(target)
153+
const originalValue = target.phaseChangeNotifications
154+
await prisma.resource.update({
155+
where: { id: target.id },
156+
data: { phaseChangeNotifications: null }
157+
})
158+
159+
const result = await service.getResources(user.admin, challengeId)
160+
const resourceRecord = result.data.find(r => r.id === target.id)
161+
should.exist(resourceRecord)
162+
should.equal(resourceRecord.phaseChangeNotifications, false)
163+
164+
await prisma.resource.update({
165+
where: { id: target.id },
166+
data: { phaseChangeNotifications: originalValue }
167+
})
168+
})
148169
})

0 commit comments

Comments
 (0)