Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5ce5071
Handle conversations in registryOrg create/update
cberger8 Nov 10, 2025
8329b76
Joint approval pass
david-rocca Nov 10, 2025
2c6100d
merge conflicts be gone
david-rocca Nov 10, 2025
0100b88
Small fixes for integration
david-rocca Nov 10, 2025
032c4a9
Default values for conversation object
cberger8 Nov 10, 2025
208893b
Non sec users can request orgs
david-rocca Nov 11, 2025
3279940
fixing tests
david-rocca Nov 11, 2025
0c213bb
Conversations now properly tied to review objects
cberger8 Nov 11, 2025
e46eb1a
resolving regregression
david-rocca Nov 11, 2025
c408dbb
integration tests for new stuff
david-rocca Nov 11, 2025
94ad3a0
auditChanges
emathew5 Nov 12, 2025
f515e64
Merge branch 'dev' into emathew/audit-org-log
emathew5 Nov 12, 2025
d8983df
Added endpoint to get review object by UUID with conversation
cberger8 Nov 18, 2025
da63ecd
Fixed review object endpoints not returning conversation
cberger8 Nov 19, 2025
48a303f
fixing tests
david-rocca Nov 19, 2025
239e475
Integration tests for conversation endpoints
cberger8 Nov 20, 2025
f3a8241
fix tests
emathew5 Nov 24, 2025
bb10cca
Merge branch 'dr_cb_joint_comments' into emathew/audit-org-log
emathew5 Nov 24, 2025
9439f32
remove unused import
david-rocca Nov 24, 2025
1fa78d6
linting issues
david-rocca Nov 24, 2025
51c8107
Pass at removing
david-rocca Nov 19, 2025
3234bdf
Another pass
david-rocca Nov 25, 2025
15a6257
Fixed some unit tests
david-rocca Nov 25, 2025
23c07d4
Old tests are old
david-rocca Nov 25, 2025
4cc8e65
removed incorrect throw documentation
david-rocca Dec 1, 2025
a42d765
Merge branch 'dev' into dr_fix_registry_user_controller
david-rocca Dec 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ function getConstants () {
USER_ROLES: [
'ADMIN'
],
JOINT_APPROVAL_FIELDS: ['short_name', 'long_name'],
JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name'],
USER_ROLE_ENUM: {
ADMIN: 'ADMIN'
},
Expand Down
118 changes: 76 additions & 42 deletions src/controller/audit.controller/audit.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const validateUUID = require('uuid').validate
* Create a new audit document
* Called by POST /api/audit/org/
*/
async function createAuditDocument (req, res, next) {
async function createAuditDocumentForOrg (req, res, next) {
try {
const session = await mongoose.startSession()
const repo = req.ctx.repositories.getAuditRepository()
Expand Down Expand Up @@ -72,9 +72,25 @@ async function createAuditDocument (req, res, next) {
await session.abortTransaction()
return res.status(400).json(error.missingRequiredField('change_author'))
}

// Process entry immediately after validation
returnValue = await repo.appendToAuditHistoryForOrg(
body.target_uuid,
entry.audit_object,
entry.change_author,
{ session }
)
}
} else {
// Create audit document with initial empty entry or default entry
returnValue = await repo.appendToAuditHistoryForOrg(
body.target_uuid,
body.audit_object || {},
body.change_author || req.ctx.org,
{ session }
)
}
returnValue = await repo.createAuditDocument(body, { session })

await session.commitTransaction()

logger.info({
Expand All @@ -100,7 +116,7 @@ async function createAuditDocument (req, res, next) {
* Called by PUT /api/audit/org/
* Allows for multiple appends in a single request
*/
async function appendToAuditHistory (req, res, next) {
async function appendToAuditHistoryForOrg (req, res, next) {
try {
const session = await mongoose.startSession()
const repo = req.ctx.repositories.getAuditRepository()
Expand Down Expand Up @@ -149,7 +165,7 @@ async function appendToAuditHistory (req, res, next) {
}

// Append this history entry
returnValue = await repo.appendToAuditHistory(
returnValue = await repo.appendToAuditHistoryForOrg(
body.target_uuid,
entry.audit_object,
entry.change_author,
Expand Down Expand Up @@ -190,7 +206,7 @@ async function appendToAuditHistory (req, res, next) {
* Get all audit documents
* Called by GET /api/audit/org/
*/
async function getAllAuditDocuments (req, res, next) {
async function getAllOrgAuditDocuments (req, res, next) {
try {
const session = await mongoose.startSession()
const repo = req.ctx.repositories.getAuditRepository()
Expand All @@ -213,7 +229,7 @@ async function getAllAuditDocuments (req, res, next) {
* Get audit document by its document UUID
* Called by GET /api/audit/org/document/:document_uuid
*/
async function getAuditByDocumentUUID (req, res, next) {
async function getOrgAuditByDocumentUUID (req, res, next) {
try {
const session = await mongoose.startSession()
const repo = req.ctx.repositories.getAuditRepository()
Expand Down Expand Up @@ -247,48 +263,53 @@ async function getAuditByDocumentUUID (req, res, next) {
next(err)
}
}

/**
* Get audit history by target UUID
* Called by GET /api/audit/org/:target_uuid
* TODO: remove comment-> I changed parameter name from org_identifier to target_uuid to be more generic.
* Get audit history by target identifier (shortname or UUID)
* Called by GET /api/audit/org/:identifier
*/
async function getAuditByTargetUUID (req, res, next) {
async function getOrgAuditByOrgIdentifier (req, res, next) {
try {
const session = await mongoose.startSession()
const repo = req.ctx.repositories.getAuditRepository()
const orgRepo = req.ctx.repositories.getBaseOrgRepository()
const targetUUID = req.ctx.params.target_uuid
const identifier = req.ctx.params.org_identifier
const identifierIsUUID = validateUUID(identifier)
let returnValue

if (!targetUUID) {
logger.info({ uuid: req.ctx.uuid, message: 'Missing target_uuid parameter' })
return res.status(400).json(error.missingRequiredField('target_uuid'))
}

if (!validateUUID(targetUUID)) {
logger.info({ uuid: req.ctx.uuid, message: 'Invalid target_uuid format' })
return res.status(400).json(error.invalidUUID('target_uuid'))
if (!identifier) {
return res.status(400).json(error.missingRequiredField('identifier'))
}

try {
session.startTransaction()

// Find the target organization
const targetOrg = await orgRepo.findOneByUUID(targetUUID, { session })
// Find the target organization by either UUID or shortname
const targetOrg = identifierIsUUID
? await orgRepo.findOneByUUID(identifier, { session })
: await orgRepo.findOneByShortName(identifier, { session })

if (!targetOrg) {
logger.info({ uuid: req.ctx.uuid, message: `No organization found with UUID ${targetUUID}` })
logger.info({
uuid: req.ctx.uuid,
message: `No organization found with ${identifierIsUUID ? 'UUID' : 'shortname'} ${identifier}`
})
await session.abortTransaction()
return res.status(404).json(error.orgDne(targetUUID))
return res.status(404).json(error.orgDne(identifier))
}

// TODO: confirm middleware is checking admin and secretariat permissions properly
// Get the org's UUID for audit lookup
const targetUUID = targetOrg.UUID

returnValue = await repo.findOneByTargetUUID(targetUUID, { session })

if (!returnValue) {
logger.info({ uuid: req.ctx.uuid, message: `No audit history found for target UUID ${targetUUID}` })
logger.info({
uuid: req.ctx.uuid,
message: `No audit history found for organization ${identifier} (UUID: ${targetUUID})`
})
await session.abortTransaction()
return res.status(404).json(error.auditDneByTarget(targetUUID))
return res.status(404).json(error.auditDneByTarget(identifier))
}

await session.commitTransaction()
Expand All @@ -301,7 +322,7 @@ async function getAuditByTargetUUID (req, res, next) {

logger.info({
uuid: req.ctx.uuid,
message: `Audit history for target UUID ${targetUUID} sent to user ${req.ctx.user}`
message: `Audit history for ${identifierIsUUID ? 'UUID' : 'shortname'} ${identifier} sent to user ${req.ctx.user}`
})
return res.status(200).json(returnValue)
} catch (err) {
Expand All @@ -317,18 +338,14 @@ async function getLastXChanges (req, res, next) {
try {
const session = await mongoose.startSession()
const repo = req.ctx.repositories.getAuditRepository()
const targetUUID = req.ctx.params.target_uuid
const orgRepo = req.ctx.repositories.getBaseOrgRepository()
const identifier = req.ctx.params.org_identifier
const identifierIsUUID = validateUUID(identifier)
const numberOfChanges = parseInt(req.ctx.params.number_of_changes)
let returnValue

if (!targetUUID) {
logger.info({ uuid: req.ctx.uuid, message: 'Missing org_identifier parameter' })
return res.status(400).json(error.missingRequiredField('org_identifier'))
}

if (!validateUUID(targetUUID)) {
logger.info({ uuid: req.ctx.uuid, message: 'Invalid target_uuid format' })
return res.status(400).json(error.invalidUUID('target_uuid'))
if (!identifier) {
return res.status(400).json(error.missingRequiredField('identifier'))
}

if (isNaN(numberOfChanges) || numberOfChanges < 1) {
Expand All @@ -339,6 +356,23 @@ async function getLastXChanges (req, res, next) {
try {
session.startTransaction()

// Find the target organization by either UUID or shortname
const targetOrg = identifierIsUUID
? await orgRepo.findOneByUUID(identifier, { session })
: await orgRepo.findOneByShortName(identifier, { session })

if (!targetOrg) {
logger.info({
uuid: req.ctx.uuid,
message: `No organization found with ${identifierIsUUID ? 'UUID' : 'shortname'} ${identifier}`
})
await session.abortTransaction()
return res.status(404).json(error.orgDne(identifier))
}

// Get the org's UUID for audit lookup
const targetUUID = targetOrg.UUID

const lastChanges = await repo.getLastXChanges(targetUUID, numberOfChanges, { session })

if (!lastChanges || lastChanges.length === 0) {
Expand All @@ -362,7 +396,7 @@ async function getLastXChanges (req, res, next) {

logger.info({
uuid: req.ctx.uuid,
message: `Last ${numberOfChanges} changes for ${targetUUID} sent to user ${req.ctx.user}`
message: `Last ${numberOfChanges} changes for ${identifier} sent to user ${req.ctx.user}`
})
return res.status(200).json(returnValue)
} catch (err) {
Expand All @@ -371,10 +405,10 @@ async function getLastXChanges (req, res, next) {
}

module.exports = {
AUDIT_CREATE_SINGLE: createAuditDocument,
AUDIT_UPDATE: appendToAuditHistory,
AUDIT_GET_ALL: getAllAuditDocuments,
AUDIT_GET_BY_UUID: getAuditByDocumentUUID,
AUDIT_GET_BY_TARGET_UUID: getAuditByTargetUUID,
AUDIT_CREATE_SINGLE: createAuditDocumentForOrg,
AUDIT_UPDATE: appendToAuditHistoryForOrg,
AUDIT_GET_ALL: getAllOrgAuditDocuments,
AUDIT_GET_BY_UUID: getOrgAuditByDocumentUUID,
AUDIT_GET_BY_ORG_IDENTIFIER: getOrgAuditByOrgIdentifier,
AUDIT_GET_LAST: getLastXChanges
}
6 changes: 3 additions & 3 deletions src/controller/audit.controller/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ router.get('/audit/org/document/:document_uuid',
)

// Get audit by org identifier (Secretariat or Admin)
router.get('/audit/org/:target_uuid',
router.get('/audit/org/:org_identifier',
mw.validateUser,
mw.onlySecretariatOrAdmin,
auditMw.parseGetParams,
controller.AUDIT_GET_BY_TARGET_UUID
controller.AUDIT_GET_BY_ORG_IDENTIFIER
)

// Get last X changes (Secretariat or Org Admin)
router.get('/audit/org/:target_uuid/:number_of_changes',
router.get('/audit/org/:org_identifier/:number_of_changes',
mw.onlySecretariatOrAdmin,
mw.validateUser,
auditMw.parseGetParams,
Expand Down
72 changes: 13 additions & 59 deletions src/controller/conversation.controller/conversation.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,84 +19,38 @@ async function getAllConversations (req, res, next) {
return res.status(200).json(response)
}

async function getConversationsForOrg (req, res, next) {
const session = await mongoose.startSession()

try {
session.startTransaction()

const repo = req.ctx.repositories.getConversationRepository()
const orgRepo = req.ctx.repositories.getBaseOrgRepository()
const requesterOrg = req.ctx.org
const targetOrgUUID = req.params.uuid

// Make sure target org matches user org if not secretariat
const isSecretariat = await orgRepo.isSecretariatByShortName(requesterOrg, { session })
const requesterOrgUUID = await orgRepo.getOrgUUID(requesterOrg, { session })
if (!isSecretariat && (requesterOrgUUID !== targetOrgUUID)) {
return res.status(400).json({ message: 'User is not secretariat or admin for target org' })
}

// temporary measure to allow tests to work after fixing #920
// tests required changing the global limit to force pagination
if (req.TEST_PAGINATOR_LIMIT) {
CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT
}

const options = CONSTANTS.PAGINATOR_OPTIONS
options.sort = { posted_at: 'desc' }
async function getConversationsForTargetUUID (req, res, next) {
const repo = req.ctx.repositories.getConversationRepository()
const targetUUID = req.params.uuid

const response = await repo.getAllByTargetUUID(targetOrgUUID, options)
await session.commitTransaction()
return res.status(200).json(response)
} catch (err) {
if (session && session.inTransaction()) {
await session.abortTransaction()
}
next(err)
} finally {
if (session && session.id) { // Check if session is still valid before trying to end
try {
await session.endSession()
} catch (sessionEndError) {
logger.error({ uuid: req.ctx.uuid, message: 'Error ending session in finally block', error: sessionEndError })
}
}
}
const response = await repo.getAllByTargetUUID(targetUUID)
return res.status(200).json(response)
}

async function createConversationForOrg (req, res, next) {
async function createConversationForTargetUUID (req, res, next) {
const session = await mongoose.startSession()

try {
session.startTransaction()

const repo = req.ctx.repositories.getConversationRepository()
const orgRepo = req.ctx.repositories.getBaseOrgRepository()
const userRepo = req.ctx.repositories.getBaseUserRepository()
const requesterOrg = req.ctx.org
const requesterUsername = req.ctx.user
const targetOrgUUID = req.params.uuid
const targetUUID = req.params.uuid
const body = req.body

// Make sure target org matches user org if not secretariat
const isSecretariat = await orgRepo.isSecretariatByShortName(requesterOrg, { session })
const requesterOrgUUID = await orgRepo.getOrgUUID(requesterOrg, { session })
if (!isSecretariat && (requesterOrgUUID !== targetOrgUUID)) {
return res.status(400).json({ message: 'User is not secretariat or admin for target org' })
}

const user = await userRepo.findOneByUsernameAndOrgShortname(requesterUsername, requesterOrg, { session })

if (!body.body) {
return res.status(400).json({ message: 'Missing required field body' })
}

const conversationBody = {
target_uuid: targetOrgUUID,
target_uuid: targetUUID,
author_id: user.UUID,
author_name: [user.name.first, user.name.last].join(' '),
author_role: isSecretariat ? 'Secretariat' : 'Partner',
author_role: 'Secretariat',
visibility: body.visibility ? body.visibility.toLowerCase() : 'private',
body: body.body
}
Expand Down Expand Up @@ -129,20 +83,20 @@ async function createConversationForOrg (req, res, next) {

async function updateMessage (req, res, next) {
const repo = req.ctx.repositories.getConversationRepository()
const targetOrgUUID = req.params.uuid
const targetUUID = req.params.uuid
const body = req.body

if (!body.body) {
return res.status(400).json({ message: 'Missing required field body' })
}

const result = await repo.updateConversation(body, targetOrgUUID)
const result = await repo.updateConversation(body, targetUUID)
return res.status(200).json(result)
}

module.exports = {
getAllConversations,
getConversationsForOrg,
createConversationForOrg,
getConversationsForTargetUUID,
createConversationForTargetUUID,
updateMessage
}
Loading
Loading