Created
July 22, 2025 15:26
-
-
Save markgarrigan/893fd3abd74fd6fab35496360821f9a6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const axios = require('axios'); | |
const RALLY_API_HOST = 'https://rally1.rallydev.com' | |
const RALLY_API_PATH = `/slm/webservice/v2.0` | |
const RALLY_WORKSPACE = '4729135433' | |
const RALLY_PROJECT = { | |
pidi: 344842856592, // 5 - PIDI Intake | |
dss: 89480156508, // AS DS - Infrastructure | |
} | |
const baseURL = `${RALLY_API_HOST}${RALLY_API_PATH}` | |
const RALLY_FIELDS = [ | |
'FormattedID', | |
'Name', | |
'ScheduleState', | |
'RequesterName', | |
'SEMAnnualizedSavingsEstimate', | |
'CreationDate', | |
'Project', | |
'FlowStateChangedDate', | |
'Attachments', | |
] | |
const RALLY_FIELDS_DETAILED = [ | |
...RALLY_FIELDS, | |
'Description', | |
'PlanEstimate', | |
'TaskStatus', | |
'PlannedDeploymentDate', | |
'Owner', | |
'Notes', | |
'AcceptanceCriteria', | |
'ObjectID', | |
'productIdentifier', | |
'LineofBusiness', | |
'OEAWorkEffortType', | |
'Client', | |
'Product', | |
'SubmittingTeam', | |
'Expedite', | |
'justification', | |
'OEAPSFinancialImpact', | |
'HardBenefitCategory', | |
'UnitedPlatform', | |
'ClaimType', | |
'InternalClient', | |
'PointinTime', | |
'AnalyticID', | |
'AnalyticName', | |
'ProviderSpecialty', | |
] | |
function transform(userStory, type = 'summary') { | |
const rv = { | |
summary: { | |
id: userStory.FormattedID || '', | |
name: userStory.Name || '', | |
scheduleState: userStory.ScheduleState || '', | |
requesterName: userStory.c_RequesterName || '', | |
semAnnualizedSavingsEstimate: userStory.c_SEMAnnualizedSavingsEstimate || '', | |
creationDate: userStory.CreationDate || '', | |
project: userStory.Project ? { | |
name: userStory.Project.Name || '', | |
creationDate: userStory.Project.CreationDate || '', | |
} : {}, | |
flowStateChangedDate: userStory.FlowStateChangedDate || '', | |
attachmentCount: userStory.Attachments?.Count || '', | |
}, | |
detailed: { | |
id: userStory.FormattedID || '', // US123456 | |
name: userStory.Name || '', // plain text | |
description: userStory.Description || '', // html | |
notes: userStory.Notes || '', // html | |
acceptanceCriteria: userStory.c_AcceptanceCriteria || '', // html | |
owner: userStory.Owner?._refObjectName || '', // Eric Nelson | |
taskStatus: userStory.TaskStatus || '', // COMPLETED | |
planEstimate: userStory.PlanEstimate || '', // 3 | |
plannedDeploymentDate: userStory.c_PlannedDeploymentDate || '', // 2019-11-03T16:14:12:999Z | |
scheduleState: userStory.ScheduleState || '', | |
projectId: userStory.Project?._ref?.split('/').pop(), | |
objectId: userStory.ObjectID, | |
productIdentifier: userStory.productIdentifier || '', | |
lineOfBusiness: userStory.c_LineofBusiness?._tagsNameArray?.map(t => t.Name) || [], | |
oeaWorkEffortType: userStory.c_OEAWorkEffortType || '', | |
client: userStory.c_Client?._tagsNameArray?.map(t => t.Name) || [], | |
product: userStory.c_Product || '', | |
submittingTeam: userStory.c_SubmittingTeam || '', | |
requesterName: userStory.c_RequesterName || '', | |
expedite: userStory.Expedite || '', | |
justification: userStory.justification || '', | |
oeaPSFinancialImpact: userStory.c_OEAPSFinancialImpact || '', | |
hardBenefitCategory: userStory.c_HardBenefitCategory || '', | |
unitedPlatform: userStory.c_UnitedPlatform || '', | |
claimType: userStory.c_ClaimType?._tagsNameArray?.map(t => t.Name) || [], | |
internalClient: userStory.c_InternalClient || '', | |
pointInTime: userStory.c_PointinTime || '', | |
analyticID: userStory.c_AnalyticID || '', | |
analyticName: userStory.c_AnalyticName || '', | |
providerSpecialty: userStory.c_ProviderSpecialty || '', | |
semAnnualizedSavingsEstimate: userStory.c_SEMAnnualizedSavingsEstimate || '', | |
creationDate: userStory.CreationDate || '', | |
project: userStory.Project ? { | |
name: userStory.Project.Name || '', | |
creationDate: userStory.Project.CreationDate || '', | |
} : {}, | |
flowStateChangedDate: userStory.FlowStateChangedDate || '', | |
attachmentCount: userStory.Attachments?.Count || '', | |
} | |
} | |
return rv[type] || {} | |
} | |
function cleanObject(obj) { | |
return Object.fromEntries( | |
Object.entries(obj) | |
.filter(([_, v]) => v !== null && (typeof v !== 'object' || Object.keys(v).length > 0)) | |
.map(([k, v]) => [k, v && typeof v === 'object' ? cleanObject(v) : v]) | |
); | |
} | |
async function addAttachmentContent({ Workspace: workspaceId = RALLY_WORKSPACE, content } = {}) { | |
const workspace = encodeURIComponent(`${baseURL}/workspace/${workspaceId}`) | |
const url = `attachmentcontent/create?workspace=${workspace}` | |
const payload = { AttachmentContent: { Content: content } } | |
const response = await axios.post(url, payload, { | |
baseURL, | |
headers: { zsessionid: process.env.RALLY_READ_WRITE_API_KEY } | |
}); | |
let result = response?.data?.CreateResult?.Object | |
if (!result) { | |
throw new Error('Error adding attachment content') | |
} | |
return result | |
} | |
async function addAttachmentToUserStory({ Workspace: workspaceId = RALLY_WORKSPACE, Artifact, Content, Name }) { | |
const workspace = encodeURIComponent(`${baseURL}/workspace/${workspaceId}`) | |
const url = `attachment/create?workspace=${workspace}` | |
const payload = { | |
Attachment: { | |
Content, | |
Artifact, | |
ContentType: 'application/octet-stream', | |
Name, | |
} | |
}; | |
// call rally api to add attachment | |
const response = await axios.post(url, payload, { | |
baseURL, | |
headers: { zsessionid: process.env.RALLY_READ_WRITE_API_KEY } | |
}); | |
const result = response?.data?.CreateResult?.Object | |
if (!result) { | |
throw new Error('Error adding attachment to user story') | |
} | |
return result | |
} | |
const userstories = {} | |
userstories.list = async ({ Workspace: workspaceId = RALLY_WORKSPACE, ...options } = {}) => { | |
let { start = 0, results = [] } = options | |
// build url parameters | |
const fetch = encodeURIComponent(RALLY_FIELDS) | |
const query = encodeURIComponent('(SupportIssueID = pidianalyticsintake)') | |
const workspace = encodeURIComponent(`${baseURL}/workspace/${workspaceId}`) | |
// build the url | |
const url = | |
`/hierarchicalrequirement?` + | |
`start=${start}&pagesize=200&order=DragAndDropRank` + | |
`&fetch=${fetch}` + | |
`&query=${query}` + | |
`&workspace=${workspace}` | |
// call rally api | |
const headers = { zsessionid: process.env.RALLY_READ_API_KEY } | |
const response = await axios.get(url, { | |
baseURL, | |
headers, | |
}) | |
// aggregate the results | |
const pageResults = response?.data?.QueryResult?.Results || [] | |
if (pageResults.length) { | |
results = results.concat(pageResults.map(us => transform(us))) | |
// check response for additional pages | |
const total = response?.data?.QueryResult?.TotalResultCount || 0 | |
if (results.length < total) { | |
await userstories.list({ Workspace: workspaceId, start: start + 200, results }) | |
} | |
} | |
return results | |
} | |
userstories.get = async ({ Workspace: workspaceId = RALLY_WORKSPACE, id, ...options } = {}) => { | |
const fetch = encodeURIComponent(RALLY_FIELDS_DETAILED) | |
const query = encodeURIComponent(`(FormattedID = ${id})`) | |
const workspace = encodeURIComponent(`${baseURL}/workspace/${workspaceId}`) | |
// build the url | |
const url = | |
`/hierarchicalrequirement?` + | |
`start=1&pagesize=1&order=DragAndDropRank` + | |
`&fetch=${fetch}` + | |
`&query=${query}` + | |
`&workspace=${workspace}` | |
// call rally api | |
const response = await axios.get(url, { | |
baseURL, | |
headers: { zsessionid: process.env.RALLY_READ_API_KEY } | |
}) | |
// make sure its there | |
const [userstory] = response?.data?.QueryResult?.Results || [] | |
return options.raw ? userstory : transform(userstory, 'detailed') | |
} | |
userstories.create = async ({ projectId, Workspace: workspaceId = RALLY_WORKSPACE, ...HierarchicalRequirement } = {}) => { | |
const project = RALLY_PROJECT[projectId || 'pidi'] | |
const projectRef = encodeURIComponent(`${baseURL}/project/${project}`) | |
const url = `/hierarchicalrequirement/create?project=${projectRef}` | |
const payload = { HierarchicalRequirement: cleanObject(HierarchicalRequirement) } | |
let response = {} | |
try { | |
response = await axios.post(url, payload, { | |
baseURL, | |
headers: { zsessionid: process.env.RALLY_READ_WRITE_API_KEY } | |
}) | |
} catch (error) { | |
console.log('Begin User Story Create Error') | |
console.log(payload) | |
console.log(error) | |
console.log('End User Story Create Error') | |
throw new Error('Error calling the Rally API') | |
} | |
// check response | |
const userstory = response?.data?.CreateResult?.Object | |
const id = userstory?.FormattedID | |
if (!userstory || !id) { | |
throw new Error('User story not created') | |
} | |
return { id, userstory } | |
} | |
userstories.attachments = {} | |
userstories.attachments.create = async ({ id, filename, content }) => { | |
const { _ref: Artifact } = await userstories.get({id, raw: true}) | |
const { _ref: Content } = await addAttachmentContent({content}) | |
const result = await addAttachmentToUserStory({ Artifact, Content, Name: filename }) | |
return result; | |
} | |
const iterations= {} | |
iterations.list = async ({ Workspace: workspaceId = RALLY_WORKSPACE, ...options } = {}) => { | |
// call rally api | |
const headers = { zsessionid: process.env.RALLY_READ_API_KEY } | |
const dssResponse = await axios.get(`/project/89480156508/iterations?shallowFetch=Name,State,Project&pagesize=200&query=((State != "Accepted") and (Name contains "2025"))`, { | |
baseURL, | |
headers, | |
}) | |
const cssResponse = await axios.get(`/project/601558843763/iterations?shallowFetch=Name,State,Project&pagesize=200&query=((State != "Accepted") and (Name contains "2025"))`, { | |
baseURL, | |
headers, | |
}) | |
const combinedResponse = [...dssResponse.data.QueryResult.Results, ...cssResponse.data.QueryResult.Results]; | |
return combinedResponse | |
} | |
const features= {} | |
features.list = async ({ Workspace: workspaceId = RALLY_WORKSPACE, ...options } = {}) => { | |
// call rally api | |
const headers = { zsessionid: process.env.RALLY_READ_API_KEY } | |
const dssResponse = await axios.get(`/portfolioitem/feature?shallowFetch=FormattedID,Name&pagesize=200&query=((Project = "/project/89480156508") and (Name contains "2025"))`, { | |
baseURL, | |
headers, | |
}) | |
const cssResponse = await axios.get(`/portfolioitem/feature?shallowFetch=FormattedID,Name&pagesize=200&query=((Project = "/project/601558843763") and (Name contains "2025"))`, { | |
baseURL, | |
headers, | |
}) | |
const combinedResponse = [...dssResponse.data.QueryResult.Results, ...cssResponse.data.QueryResult.Results]; | |
return combinedResponse | |
} | |
const releases= {} | |
releases.list = async ({ Workspace: workspaceId = RALLY_WORKSPACE, ...options } = {}) => { | |
// call rally api | |
const headers = { zsessionid: process.env.RALLY_READ_API_KEY } | |
const dssResponse = await axios.get(`/releases?shallowFetch=Name&pagesize=200&query=((Project = "/project/89480156508") and (Name contains "2025"))`, { | |
baseURL, | |
headers, | |
}) | |
const cssResponse = await axios.get(`/releases?shallowFetch=Name&pagesize=200&query=((Project = "/project/601558843763") and (Name contains "2025"))`, { | |
baseURL, | |
headers, | |
}) | |
const combinedResponse = [...dssResponse.data.QueryResult.Results, ...cssResponse.data.QueryResult.Results]; | |
return combinedResponse | |
} | |
module.exports = { | |
userstories, | |
iterations, | |
features, | |
releases, | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment