Audit 49 : Add the fetch Patient Audit Revisions API #45
Audit 49 : Add the fetch Patient Audit Revisions API #45sudhanshu-raj wants to merge 21 commits into
Conversation
…PI to fetch single revision by revision id of entity with their id
|
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") |
There was a problem hiding this comment.
| @RequestMapping("/rest/" + RestConstants.VERSION_1 + "/auditlogs/patient") | |
| @RequestMapping("/rest/" + RestConstants.VERSION_1 + "/auditlogs/patients") |
There was a problem hiding this comment.
Here we fetching the detailed revisions for single patient, right ? So do we still need patients ?
There was a problem hiding this comment.
resource names are typically plural because the endpoint represents a collection of resources
| @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; |
There was a problem hiding this comment.
why we need this api? instead of adding a new api we can add id filter to existing get all audit logs api
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
can there be multiple revisions for a single entity ID?
There was a problem hiding this comment.
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 .
There was a problem hiding this comment.
we can use fetch by revision id for this case right?
There was a problem hiding this comment.
ya, revision id + entity name + entity id to get the actual change for entity revision
There was a problem hiding this comment.
can't we do the same thing only using revision id?
There was a problem hiding this comment.
There can be more than one changes on different entity for a single revision id , so we need more details.
There was a problem hiding this comment.
rename the API endpoint to /auditlogs/{revisionId} and pass revisionId as a path variable, while keeping entityName and entityId as query parameters
| import static org.mockito.Mockito.mock; | ||
| import static org.mockito.Mockito.mockStatic; | ||
| import static org.mockito.Mockito.when; | ||
| import static org.mockito.Mockito.*; |
| </modules> | ||
|
|
||
| <properties> | ||
| <openmrsPlatformVersion>2.7.0</openmrsPlatformVersion> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
This gives mixed signals, the body says "Bad Request" while the status is 404
There was a problem hiding this comment.
My bad, replaced with BAD_REQUEST now
| dto.setRevisionID(entity.getRevisionEntity().getId()); | ||
| dto.setEntityType(currentEntity.getClass().getSimpleName()); | ||
| dto.setEventType(auditType); | ||
| dto.setEventType(String.valueOf(entity.getRevisionType())); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Shouldn't this be entityId instead of patientId ?
There was a problem hiding this comment.
Ahh I missed it, corrected
| @Data | ||
| @AllArgsConstructor | ||
| @NoArgsConstructor | ||
| public class AuditEntityDetailsDTO { |
There was a problem hiding this comment.
Can't we make the existing DTO carry an optional List<RelatedEntityDto> relatedEntities and reuse AuditLogResponseDto?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Not really, i have added for testing only will remove it
| * @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) { |
There was a problem hiding this comment.
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) { |
| @RequestMapping("/rest/" + RestConstants.VERSION_1 + "/auditlogs") | ||
| public class AuditLogRestController { | ||
|
|
||
| private final Logger log = LoggerFactory.getLogger(AuditLogRestController.class); |
There was a problem hiding this comment.
It's not use of now , I removed it
…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>
| /** | ||
| * Tests {@code PatientLogRestController#getPatientAuditLogs} | ||
| * It verifies negative/zero pagination values are normalized to defaults. | ||
| */ |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
do we need this debug log?
There was a problem hiding this comment.
Sure , we can remove it , comments are enough
|
|
||
| @ExceptionHandler(ObjectNotFoundException.class) | ||
| public ResponseEntity<Map<String, String>> handleObjectNotFound(ObjectNotFoundException ex) { | ||
| return buildResponseEntity("Bad Request", ex.getMessage(), HttpStatus.BAD_REQUEST); |
There was a problem hiding this comment.
Why not HttpStatus.NOT_FOUND?
| } 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<>(); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 .
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Is this the correct exception to catch?
There was a problem hiding this comment.
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); |
…ed rest exception handler
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.
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 --amendI 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 --amendI ran
mvn clean packageright before creating this pull request andadded 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