Skip to content

Commit c972242

Browse files
nbitonfranck-boullier
authored andcommitted
enhanced case API for 3rd party case viewers/editors (#889)
* feat: API endpoint to retrieve a specific case with its comments * feat: more sophisticated get case API obj transform after refactor * feat: case update API * feat: post /api/cases/:caseId/comments API allows comment creation * feat: 4 missing case fields added * fix; lint * feat: refactored case creation and enhanced update with user invites
1 parent 319e3cd commit c972242

File tree

10 files changed

+685
-218
lines changed

10 files changed

+685
-218
lines changed

imports/api/cases.js

+21-18
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,20 @@ export const severityIndex = [
5858
'minor'
5959
]
6060

61+
export const caseEditableFields = [
62+
'title',
63+
'solution',
64+
'solutionDeadline',
65+
'nextSteps',
66+
'nextStepsBy',
67+
'status',
68+
'resolution',
69+
'category',
70+
'subCategory',
71+
'priority',
72+
'severity'
73+
]
74+
6175
export const caseBzApiRoute = '/rest/bug'
6276

6377
export const REPORT_KEYWORD = 'inspection_report'
@@ -360,7 +374,7 @@ export const factoryOptions = {
360374
export const idUrlTemplate = caseId => `${caseBzApiRoute}/${caseId}`
361375

362376
export let reloadCaseFields
363-
let publicationObj
377+
export let publicationObj
364378

365379
export const noReportsExp = {
366380
field: 'keywords',
@@ -508,8 +522,9 @@ if (Meteor.isServer) {
508522
}
509523

510524
export const fieldEditMethodMaker = ({ editableFields, methodName, publicationObj }) =>
511-
(caseId, changeSet) => {
525+
(caseId, changeSet, userId) => {
512526
check(caseId, Number)
527+
userId = userId || Meteor.userId()
513528
Object.keys(changeSet).forEach(fieldName => {
514529
if (!editableFields.includes(fieldName)) {
515530
throw new Meteor.Error(`illegal field name ${fieldName}`)
@@ -519,7 +534,7 @@ export const fieldEditMethodMaker = ({ editableFields, methodName, publicationOb
519534
throw new Meteor.Error(`illegal value type of ${valType} set to field ${fieldName}`)
520535
}
521536
})
522-
if (!Meteor.userId()) {
537+
if (!userId) {
523538
throw new Meteor.Error('not-authorized')
524539
}
525540

@@ -529,7 +544,7 @@ export const fieldEditMethodMaker = ({ editableFields, methodName, publicationOb
529544
})
530545
} else { // is server
531546
const { callAPI } = bugzillaApi
532-
const { bugzillaCreds: { apiKey } } = Meteor.users.findOne({ _id: Meteor.userId() })
547+
const { bugzillaCreds: { apiKey } } = Meteor.users.findOne({ _id: userId })
533548
const changedFields = Object.keys(changeSet)
534549
try {
535550
const normalizedSet = changedFields.reduce((all, key) => {
@@ -549,7 +564,7 @@ export const fieldEditMethodMaker = ({ editableFields, methodName, publicationOb
549564
publicationObj.handleChanged(caseItem, changedFields)
550565
} catch (e) {
551566
logger.error({
552-
user: Meteor.userId(),
567+
user: userId,
553568
method: methodName,
554569
args: [caseId, changeSet],
555570
error: e
@@ -697,19 +712,7 @@ Meteor.methods({
697712
},
698713
[`${collectionName}.editCaseField`]: fieldEditMethodMaker({
699714
methodName: `${collectionName}.editCaseField`,
700-
editableFields: [
701-
'title',
702-
'solution',
703-
'solutionDeadline',
704-
'nextSteps',
705-
'nextStepsBy',
706-
'status',
707-
'resolution',
708-
'category',
709-
'subCategory',
710-
'priority',
711-
'severity'
712-
],
715+
editableFields: caseEditableFields,
713716
clientCollection: Cases,
714717
publicationObj
715718
})

imports/api/comments.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const createFloorPlanComment = ({ unitBzId, caseId, userApiKey, floorPlan
4949
}
5050

5151
export let publicationObj // Exported for testing purposes
52-
let FailedComments
52+
export let FailedComments
5353
if (Meteor.isServer) {
5454
const associationFactory = makeAssociationFactory(collectionName)
5555
publicationObj = publicationFactory(factoryOptions)

imports/api/rest/cases/common.js

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// @flow
2+
import { Meteor } from 'meteor/meteor'
3+
import UnitRolesData, { addUserToRole, roleEnum } from '../../unit-roles-data'
4+
import { factoryOptions, idUrlTemplate, transformCaseForClient } from '../../cases'
5+
import { Accounts } from 'meteor/accounts-base'
6+
import { KEEP_DEFAULT } from '../../pending-invitations'
7+
import { callAPI } from '../../../util/bugzilla-api'
8+
9+
type UserDoc = {
10+
_id: string,
11+
profile: {
12+
name?: string
13+
},
14+
emails: Array<{
15+
address: string
16+
}>
17+
}
18+
19+
type UserTransformer = (user: UserDoc) => {
20+
userId?: string,
21+
name?: string,
22+
role?: ?string
23+
}
24+
25+
type UnitMeta = {
26+
_id: string,
27+
bzId: string,
28+
ownerIds: Array<string>
29+
}
30+
31+
export const caseAPIFields = [
32+
'product',
33+
'summary',
34+
'id',
35+
'assigned_to',
36+
'creation_time',
37+
'cf_ipi_clust_1_next_step',
38+
'cf_ipi_clust_1_next_step_date',
39+
'description',
40+
'cf_ipi_clust_1_solution',
41+
'deadline',
42+
'cc',
43+
'platform',
44+
'cf_ipi_clust_6_claim_type',
45+
'creator',
46+
'priority',
47+
'bug_severity'
48+
]
49+
50+
export const makeUserAPIObjGenerator = (unitMongoId: string) => {
51+
const unitRolesDict = UnitRolesData.find({
52+
unitId: unitMongoId
53+
}, {
54+
fields: {
55+
'members.id': 1,
56+
roleType: 1
57+
}
58+
}).fetch().reduce((all, role) => {
59+
role.members.forEach(member => {
60+
all[member.id] = role.roleType
61+
})
62+
return all
63+
}, {})
64+
65+
return (userDoc: UserDoc) => {
66+
const userObj = {}
67+
if (userDoc) {
68+
userObj.userId = userDoc._id
69+
userObj.name = userDoc.profile.name || userDoc.emails[0].address.split('@')[0]
70+
userObj.role = unitRolesDict[userDoc._id] || null
71+
}
72+
return userObj
73+
}
74+
}
75+
76+
export const tranformCaseAPIObj = (bug: any, thisUser: { _id: string }, transformUserObj: UserTransformer) => {
77+
const {
78+
product,
79+
id,
80+
assigned_to: assignedTo,
81+
assigned_to_detail: a,
82+
cc,
83+
cc_detail: b,
84+
creator,
85+
creator_detail: c,
86+
creation_time: creationTime,
87+
...relevantBugFields
88+
} = bug
89+
const userRelevance = []
90+
const assigneeObj = transformUserObj(Meteor.users.findOne({ 'bugzillaCreds.login': assignedTo }))
91+
if (thisUser._id === assigneeObj.userId) {
92+
userRelevance.push('Assignee')
93+
}
94+
95+
const reporterObj = transformUserObj(Meteor.users.findOne({ 'bugzillaCreds.login': creator }))
96+
if (thisUser._id === reporterObj.userId) {
97+
userRelevance.push('Reporter')
98+
}
99+
100+
const involvedList = cc.map(ccItem => transformUserObj(Meteor.users.findOne({ 'bugzillaCreds.login': ccItem })))
101+
if (involvedList.some(involved => involved.userId === thisUser._id)) {
102+
userRelevance.push('Invited To')
103+
}
104+
return {
105+
assignee: assigneeObj,
106+
reporter: reporterObj,
107+
caseId: id,
108+
involvedList,
109+
userRelevance,
110+
creationTime,
111+
...transformCaseForClient(relevantBugFields)
112+
}
113+
}
114+
115+
export const attemptUserGeneration = (newUserEmail: string, creator: UserDoc) => {
116+
const existingUser = Accounts.findUserByEmail(newUserEmail)
117+
if (existingUser) {
118+
// logger.warn(`Creating user by alias ID '${newUserEmail}' failed, another user with this email address already exists`)
119+
return existingUser
120+
}
121+
const userId = Accounts.createUser({
122+
email: newUserEmail,
123+
profile: {
124+
isLimited: true,
125+
creatorId: creator._id
126+
}
127+
})
128+
129+
Meteor.users.update({ _id: userId }, {
130+
$set: {
131+
'emails.0.verified': true,
132+
apiAliases: {
133+
userId: creator._id,
134+
id: newUserEmail
135+
}
136+
}
137+
})
138+
139+
return Meteor.users.findOne({ _id: userId })
140+
}
141+
142+
export const verifyRole = (
143+
invitor: UserDoc,
144+
invitee: UserDoc,
145+
unitMetaData: UnitMeta,
146+
designatedRole: string,
147+
errorLogParams: {}
148+
) => {
149+
const existingAssigneeRole = UnitRolesData.findOne({ 'members.id': invitee._id, unitId: unitMetaData._id })
150+
if (!existingAssigneeRole) {
151+
if (!unitMetaData.ownerIds.includes(invitor._id)) {
152+
throw new Meteor.Error('The specified user doesn\'t have a role in this unit, and you are not allowed to invite it', 'client error')
153+
}
154+
if (!designatedRole) {
155+
throw new Meteor.Error('You must provide a role for the user, so it can be permitted access to this unit', 'client error')
156+
}
157+
158+
try {
159+
addUserToRole(invitor, invitee, unitMetaData.bzId, designatedRole, KEEP_DEFAULT, false, errorLogParams)
160+
} catch (e) {
161+
throw new Meteor.Error(e.message, 'server error')
162+
}
163+
}
164+
}
165+
166+
export const assigneeAllowedRoles:Array<mixed> = Object.values(roleEnum).filter(val => val !== roleEnum.CONTRACTOR)
167+
export const isDateString = (str:string) => typeof str === 'string' && !isNaN((new Date(str)).getTime())
168+
169+
export const getCaseById = (id:number) => {
170+
const resp = callAPI('get', idUrlTemplate(id), { api_key: process.env.BUGZILLA_ADMIN_KEY }, false, true)
171+
return factoryOptions.dataResolver(resp.data)[0]
172+
}

imports/api/rest/cases/get-case.js

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// @flow
2+
import { Meteor } from 'meteor/meteor'
3+
import userApiKey, {
4+
headerExtractor,
5+
makeComposedExtractor,
6+
queryExtractor
7+
} from '../middleware/user-api-key-middleware'
8+
import { idUrlTemplate } from '../../cases'
9+
import { factoryOptions as commentFactoryOptions } from '../../comments'
10+
import { callAPI } from '../../../util/bugzilla-api'
11+
import { logger } from '../../../util/logger'
12+
import { caseAPIFields, makeUserAPIObjGenerator, tranformCaseAPIObj } from './common'
13+
import UnitMetaData from '../../unit-meta-data'
14+
import { attachmentTextMatcher, floorPlanTextMatcher } from '../../../util/matchers'
15+
16+
import type { Request, Response } from '../rest-types'
17+
18+
export default userApiKey((req: Request, res: Response) => {
19+
const { user, params } = req
20+
const { apiKey } = user.bugzillaCreds
21+
22+
// Getting the case's data
23+
let bugFields
24+
try {
25+
const caseResp = callAPI('get', idUrlTemplate(params.id), { api_key: apiKey }, false, true)
26+
const bugItem = caseResp.data.bugs[0]
27+
if (!bugItem) {
28+
res.send(404, `No accessible case with id ${params.id} was found for this user`)
29+
return
30+
}
31+
bugFields = caseAPIFields.reduce((item, field) => {
32+
item[field] = bugItem[field]
33+
return item
34+
}, {})
35+
} catch (e) {
36+
logger.error(`Failed to fetch case ${params.id} from BZ API for user ${user._id} reason: ${e.message}`)
37+
res.send(500, e.message)
38+
return
39+
}
40+
41+
const unitMeta = UnitMetaData.findOne({ bzName: bugFields.product })
42+
if (!unitMeta) {
43+
logger.error(`No unit meta data found for product name ${bugFields.product} for case ${params.id} requested by user ${user._id}`)
44+
res.send(500, `No unit meta data found for this case's product ${bugFields.product}`)
45+
return
46+
}
47+
const userTransformer = makeUserAPIObjGenerator(unitMeta._id)
48+
const caseItem = tranformCaseAPIObj(bugFields, user, userTransformer)
49+
50+
caseItem.unitId = unitMeta._id
51+
caseItem.unitName = unitMeta.displayName
52+
53+
// Getting the comments' data
54+
let commentList
55+
try {
56+
const commentsResp = callAPI('get', `/rest/bug/${params.id}/comment`, { api_key: apiKey }, false, true)
57+
commentList = commentFactoryOptions.dataResolver(commentsResp.data, params.id).map(comment => {
58+
const { creator, id, count, creation_time: time, text } = comment
59+
const commentType = attachmentTextMatcher(text)
60+
const commentBody:any = {
61+
creator: userTransformer(Meteor.users.findOne({ 'bugzillaCreds.login': creator })),
62+
id,
63+
count,
64+
time
65+
}
66+
if (commentType) {
67+
commentBody.type = commentType
68+
commentBody[commentType + 'Url'] = text.split('\n')[1]
69+
} else {
70+
const floorPlanDetails = floorPlanTextMatcher(text)
71+
if (floorPlanDetails) {
72+
commentBody.type = 'floorPlan'
73+
commentBody.floorPlanId = floorPlanDetails.id
74+
const floorPlan = unitMeta.floorPlanUrls.find(f => f.id === floorPlanDetails.id)
75+
commentBody.imageUrl = floorPlan ? floorPlan.url : 'N/A'
76+
commentBody.pins = floorPlanDetails.pins
77+
} else {
78+
commentBody.type = 'text'
79+
commentBody.text = text
80+
}
81+
}
82+
return commentBody
83+
})
84+
} catch (e) {
85+
logger.error(`Failed to fetch comments for case ${params.id} from BZ API for user ${user._id} reason: ${e.message}`)
86+
res.send(500, e.message)
87+
return
88+
}
89+
caseItem.details = commentList[0].text
90+
res.send(200, {
91+
...caseItem,
92+
comments: commentList
93+
})
94+
}, makeComposedExtractor(queryExtractor, headerExtractor))

0 commit comments

Comments
 (0)