Skip to content

Audit 49 : Add the fetch Patient Audit Revisions API #45

Open
sudhanshu-raj wants to merge 21 commits into
openmrs:mainfrom
sudhanshu-raj:AUDIT-49
Open

Audit 49 : Add the fetch Patient Audit Revisions API #45
sudhanshu-raj wants to merge 21 commits into
openmrs:mainfrom
sudhanshu-raj:AUDIT-49

Conversation

@sudhanshu-raj
Copy link
Copy Markdown
Contributor

Description of what I changed

Created the API to fetch the patient audit revision history in detailed way by including the changed related audited entities. Also added the separate endpoint to fetch the single entity audit revision by their rev id with entity id. I tried to make the change as generic so that it would work for other entity also.

  • I changed PR title to what ticket has bcz I wanted to change the ticket title but I can't and this one best fits the changes..

Issue I worked on

see https://openmrs.atlassian.net/browse/AUDIT-

Checklist: I completed these to help reviewers :)

  • My IDE is configured to follow the code style of this project.

    No? Unsure? -> configure your IDE, format the code and add the changes with git add . && git commit --amend

  • I have added tests to cover my changes. (If you refactored
    existing code that was well tested you do not have to add tests)

    No? -> write tests and add them to this commit git add . && git commit --amend

  • I ran mvn clean package right before creating this pull request and
    added all formatting changes to my commit.

    No? -> execute above command

  • All new and existing tests passed.

    No? -> figure out why and add the fix to your commit. It is your responsibility to make sure your code works.

  • My pull request is based on the latest changes of the master branch.

    No? Unsure? -> execute command git pull --rebase upstream master

@sudhanshu-raj
Copy link
Copy Markdown
Contributor Author

Hi @ManojLL @wikumChamith , let me know if there is any change on implementation or are we going good with this ?


@RestController
@RequiredArgsConstructor
@RequestMapping("/rest/" + RestConstants.VERSION_1 + "/auditlogs/patient")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@RequestMapping("/rest/" + RestConstants.VERSION_1 + "/auditlogs/patient")
@RequestMapping("/rest/" + RestConstants.VERSION_1 + "/auditlogs/patients")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we fetching the detailed revisions for single patient, right ? So do we still need patients ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resource names are typically plural because the endpoint represents a collection of resources

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corrected !

Comment on lines +108 to +126
@GetMapping("/fetchByEntity")
public AuditLogDetailDTO getAuditLogByEntity(
@RequestParam() String entityName,
@RequestParam() String entityId,
@RequestParam() Integer revisionId
){
if (entityName == null || entityId == null || revisionId == null
|| entityName.isEmpty() || entityId.isEmpty()) {
throw new IllegalArgumentException("Missing the one or more required parameter");
}

Class<?> entityClass = UtilClass.resolveAuditedEntityClass(entityName);
if (entityClass == null) {
throw new IllegalArgumentException("Cannot find class for " + entityName);
}

Object entityIdVal;
if (Role.class.isAssignableFrom(entityClass) || GlobalProperty.class.isAssignableFrom(entityClass)) {
entityIdVal = entityId;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why we need this api? instead of adding a new api we can add id filter to existing get all audit logs api

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API is only for to fetch the audit revision for a particular entity only, and to do this we need to add two more filters to existing API method and more checks, existing API fits best to return the list of audit entities and on this new one we are returning only the revision for one entity , so I thought it more cleaner way to create new API to handle this . Let me know if I'm wrong.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can there be multiple revisions for a single entity ID?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, a single entity id can have many revisions but here we fetching the changes for particular revision id , the main purpose of adding this api is because when we return the related changed entities metadata on patient api then from that api response we can easily fetch what all got changed for that related entity .

Copy link
Copy Markdown
Contributor

@ManojLL ManojLL May 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can use fetch by revision id for this case right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya, revision id + entity name + entity id to get the actual change for entity revision

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we do the same thing only using revision id?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There can be more than one changes on different entity for a single revision id , so we need more details.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename the API endpoint to /auditlogs/{revisionId} and pass revisionId as a path variable, while keeping entityName and entityId as query parameters

CC @wikumChamith

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done !

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove wild card imports

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done !

Comment thread pom.xml
</modules>

<properties>
<openmrsPlatformVersion>2.7.0</openmrsPlatformVersion>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like something we should do in a separate PR. If we're updating the platform version, we'll also have to bump the module's major version too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Back to 2.7.0 now


@ExceptionHandler(ObjectNotFoundException.class)
public ResponseEntity<Map<String, String>> handleObjectNotFound(ObjectNotFoundException ex) {
return buildResponseEntity("Bad Request", ex.getMessage(), HttpStatus.NOT_FOUND);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gives mixed signals, the body says "Bad Request" while the status is 404

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, replaced with BAD_REQUEST now

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this fixed?

dto.setRevisionID(entity.getRevisionEntity().getId());
dto.setEntityType(currentEntity.getClass().getSimpleName());
dto.setEventType(auditType);
dto.setEventType(String.valueOf(entity.getRevisionType()));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we changing this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So existing auditType returns the human readable string like Record was added , etc but we want clean rest api readable format so that's why returning the the revision type directly wrapping in string something like ADD , MOD , DEL.

* @return a list of {@link AuditEntity} records for this patient
*/
@Authorized(AuditLogConstants.VIEW_AUDIT_LOGS)
List<AuditEntity<?>> getEntityAuditRevisionsById(Integer patientId, Class<?> entityClass, int page, int size, String sortOrder);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be entityId instead of patientId ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh I missed it, corrected

@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuditEntityDetailsDTO {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we make the existing DTO carry an optional List<RelatedEntityDto> relatedEntities and reuse AuditLogResponseDto?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's nyc approach , corrected

try {
auditEntity = auditService.getAuditEntityRevisionById(entityClass, entityIdVal, revisionId);
} catch (ObjectNotFoundException ex) {
log.debug("Audit for entity {} with id {} not found", entityName, entityId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this log here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, i have added for testing only will remove it

@sudhanshu-raj sudhanshu-raj marked this pull request as ready for review May 16, 2026 22:11
* @param sortOrder "asc" or "desc" by revision timestamp
* @return a paginated list of {@link AuditEntity} records for the patient
*/
public List<AuditEntity<?>> getRevisionsForEntityById(Integer patientId, Class<?> entityClass, int page, int size, String sortOrder) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

patientId or entityId?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohh again 🙂, done !

* @param patientId the integer primary key of the Patient
* @return the total number of recorded revisions for this patient
*/
public long countRevisionsForEntityById(Integer patientId, Class<?> entityClass) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

Comment thread api/src/test/java/org/openmrs/module/auditlogweb/api/dao/AuditDaoTest.java Outdated
@RequestMapping("/rest/" + RestConstants.VERSION_1 + "/auditlogs")
public class AuditLogRestController {

private final Logger log = LoggerFactory.getLogger(AuditLogRestController.class);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this log here?

Copy link
Copy Markdown
Contributor Author

@sudhanshu-raj sudhanshu-raj May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not use of now , I removed it

Comment thread api/src/main/java/org/openmrs/module/auditlogweb/api/utils/UtilClass.java Outdated
Comment thread api/src/main/java/org/openmrs/module/auditlogweb/api/dto/AuditLogDetailDTO.java Outdated
Comment thread api/src/main/java/org/openmrs/module/auditlogweb/api/dto/AuditLogDetailDTO.java Outdated
sudhanshu-raj and others added 7 commits May 22, 2026 12:41
…DaoTest.java


Added new line at AuditDaoTest #390

Co-authored-by: Manoj Lakshan <48247516+ManojLL@users.noreply.github.com>
…lClass.java


Removed debug log

Co-authored-by: Manoj Lakshan <48247516+ManojLL@users.noreply.github.com>
…edEntityDto.java


Removed extra empty lines in RelatedEntityDto

Co-authored-by: Manoj Lakshan <48247516+ManojLL@users.noreply.github.com>
…LogDetailDTO.java

Co-authored-by: Manoj Lakshan <48247516+ManojLL@users.noreply.github.com>
…LogDetailDTO.java

Co-authored-by: Manoj Lakshan <48247516+ManojLL@users.noreply.github.com>
…LogRestControllerTest.java

Co-authored-by: Manoj Lakshan <48247516+ManojLL@users.noreply.github.com>
…LogRestController.java

Co-authored-by: Manoj Lakshan <48247516+ManojLL@users.noreply.github.com>
Comment on lines +161 to +164
/**
* Tests {@code PatientLogRestController#getPatientAuditLogs}
* It verifies negative/zero pagination values are normalized to defaults.
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove comments from tests

entityId = Integer.parseInt(entityId.toString());
} catch (NumberFormatException e){
//If this exception occurred then it may be the uuid, which we're trying to convert to int, so leave it as string
log.debug("Entity ID is not an integer, leaving it as type string");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this debug log?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure , we can remove it , comments are enough

@ManojLL ManojLL requested a review from wikumChamith May 25, 2026 01:02

@ExceptionHandler(ObjectNotFoundException.class)
public ResponseEntity<Map<String, String>> handleObjectNotFound(ObjectNotFoundException ex) {
return buildResponseEntity("Bad Request", ex.getMessage(), HttpStatus.BAD_REQUEST);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not HttpStatus.NOT_FOUND?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment on lines +544 to +551
} catch (Exception ex) {
if (isMissingAuditTableException(ex)) {
log.warn("Skipping revisions for class {} due to missing audit table: {}", entityClass.getName(), ex.getMessage());
} else {
log.error("Unexpected error while fetching revisions for class {}: {}", entityClass.getName(), ex.getMessage(), ex);
}
return new ArrayList<>();
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is problematic. Both of these branches return an empty list, which could make a DB outage look identical to "no history."

Additionally, if the user asks, "What happened to Patient 42?," a missing Patient_AUD table is not the same as saying "nothing happened to Patient 42."

We need to find a better way to handle this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed it, now throwing the AuditLogUnavailableException which will propagate to the controller and will show the error message instead of any actual result.

if (identifier != null && !identifier.isEmpty()) {
List<Patient> byIdentifier = patientService.getPatients(null, identifier, null, false);
if (!byIdentifier.isEmpty()) {
return byIdentifier.get(0);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Identifier matches can be ambiguous (e.g., getPatients(null, "123", null, false) can match across different identifier types), and name lookup will collide for any common name. Returning the first match silently in an audit feature can surface the wrong patient's history.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it can not be optimal to search on Identifier or patient name to fetch the revision history for a particular patient , because both case can give more than one output. Instead we need to search on unique value, like we already using uuid, and we can also use patient id too .

Copy link
Copy Markdown
Contributor Author

@sudhanshu-raj sudhanshu-raj May 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CC : @ManojLL , let me know if we can replace identifier and name with only patient id or have any better idea

AuditEntity<?> auditEntity;
try {
auditEntity = auditService.getAuditEntityRevisionById(entityClass, entityIdVal, revisionId);
} catch (ObjectNotFoundException ex) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the correct exception to catch?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh i thought this could be , but corrected now with NoResultException first


@ExceptionHandler(ObjectNotFoundException.class)
public ResponseEntity<Map<String, String>> handleObjectNotFound(ObjectNotFoundException ex) {
return buildResponseEntity("Bad Request", ex.getMessage(), HttpStatus.NOT_FOUND);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this fixed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants