Skip to content

Conversation

@dlzhry2nhs
Copy link
Contributor

@dlzhry2nhs dlzhry2nhs commented Oct 2, 2025

Summary

  • Routine Change

Refactors the Get by ID journey to use the Controller -> Service -> Repository pattern properly. As this is quite a simple endpoint, I have also made quite a number of additional changes to show how I would like us to begin structuring the API code.

Changes:

  • Added some Controller layer utilities for handling the parsing and response creation specific to API GW -> Lambda integration.
  • Added top level exception wrapper to be used by controller methods. Means we can allow known custom exceptions to bubble up and return an appropriate response while also handling unexpected exceptions gracefully and getting a full stack trace.
  • Vastly simplified get by ID journey to use the new utilities, raise errors where appropriate and ensure functionality is at the right level. (Due to the number of changes, it may be easiest to check out the branch and walk through the journey.)
  • Create folders for controller, service, repository. Relevant functionality should go in those directories.
  • Introduced a new utils directory for generic utilities across the project. I have just added a dictionary util for now. Changed utils in the testing folder to testing_utils to make the intent of this directory clearer, but also so it does not clash!

Reviews Required

  • Dev

Review Checklist

ℹ️ This section is to be filled in by the reviewer.

  • I have reviewed the changes in this PR and they fill all or part of the acceptance criteria of the ticket, and the code is in a mergeable state.
  • If there were infrastructure, operational, or build changes, I have made sure there is sufficient evidence that the changes will work.
  • I have ensured the changelog has been updated by the submitter, if necessary.

@dlzhry2nhs dlzhry2nhs force-pushed the feature/VED-821-refactor-get-by-id branch from d8b7cf6 to 3cde49d Compare October 2, 2025 09:59
@dlzhry2nhs dlzhry2nhs marked this pull request as ready for review October 3, 2025 11:12
@dlzhry2nhs dlzhry2nhs requested a review from mfjarvis October 3, 2025 11:15
@dlzhry2nhs dlzhry2nhs force-pushed the feature/VED-821-refactor-get-by-id branch from 13e04f1 to 8c152ac Compare October 3, 2025 14:24
JamesW1-NHS
JamesW1-NHS previously approved these changes Oct 3, 2025
def get_path_parameter(event: APIGatewayProxyEventV1, param_name: str) -> str:
return dict_utils.get_field(
dict(event),
"pathParameters",
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor point but I don't love the "reflective" access of pathParameters (and headers below) as we don't really get the benefits of the type hints. What about:

return dict_utils.get_field(
    event.pathParameters,
    param_name,
    default=""
)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeh, that's fair. I'll look into it once I'm free.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

return None, None

def get_immunization_by_id(self, imms_id: str) -> Optional[dict]:
def get_immunization_by_id(self, imms_id: str) -> tuple[Optional[dict], Optional[str]]:
Copy link
Contributor

Choose a reason for hiding this comment

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

I do think tuple is a less misleading return value than the previous dict, but I still had to read the last line to know what that tuple contained. Maybe something like get_immunisation_and_version_by_id would be clearer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, that sounds good. Had been thinking it might be better to have a single object that contains it, but it seems that the version is separate to the FHIR content and goes out as the header.

Will rename the function as such.

return form_json(imms_resp, element, identifier, base_url)

def get_immunization_by_id(self, imms_id: str, supplier_system: str) -> Optional[dict]:
def get_immunization_by_id(self, imms_id: str, supplier_system: str) -> tuple[Immunization, str]:
Copy link
Contributor

Choose a reason for hiding this comment

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

Same deal here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done for both.

Comment on lines 28 to 44
test_cases = {
("Test UnauthorisedError", UnauthorizedError, 403, "forbidden", "Unauthorized request"),
("Test UnauthorizedVaxError", UnauthorizedVaxError, 403, "forbidden",
"Unauthorized request for vaccine type"),
("Test ResourceNotFoundError", ResourceNotFoundError, 404, "not-found", "Immunization resource does not "
"exist. ID: 123")
}

for msg, error_type, expected_status, expected_code, expected_message in test_cases:
with self.subTest(msg=msg):

@fhir_api_exception_handler
def dummy_func():
if msg == "Test ResourceNotFoundError":
raise error_type(resource_type="Immunization", resource_id="123")

raise error_type()
Copy link
Contributor

@mfjarvis mfjarvis Oct 7, 2025

Choose a reason for hiding this comment

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

Could provide the exact error to raise in the test case, then we wouldn't need the msg logic:

Suggested change
test_cases = {
("Test UnauthorisedError", UnauthorizedError, 403, "forbidden", "Unauthorized request"),
("Test UnauthorizedVaxError", UnauthorizedVaxError, 403, "forbidden",
"Unauthorized request for vaccine type"),
("Test ResourceNotFoundError", ResourceNotFoundError, 404, "not-found", "Immunization resource does not "
"exist. ID: 123")
}
for msg, error_type, expected_status, expected_code, expected_message in test_cases:
with self.subTest(msg=msg):
@fhir_api_exception_handler
def dummy_func():
if msg == "Test ResourceNotFoundError":
raise error_type(resource_type="Immunization", resource_id="123")
raise error_type()
test_cases = {
(UnauthorizedError(), 403, "forbidden", "Unauthorized request"),
(UnauthorizedVaxError(), 403, "forbidden", "Unauthorized request for vaccine type"),
(
ResourceNotFoundError(resource_type="Immunization", resource_id="123"),
404,
"not-found",
"Immunization resource does not exist. ID: 123"
)
}
for error, expected_status, expected_code, expected_message in test_cases:
with self.subTest(msg=f`Test {error.__class__.__name__}`):
@fhir_api_exception_handler
def dummy_func():
raise error

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done - much nicer.

@dlzhry2nhs dlzhry2nhs enabled auto-merge (squash) October 7, 2025 15:03
@sonarqubecloud
Copy link

sonarqubecloud bot commented Oct 7, 2025

@dlzhry2nhs dlzhry2nhs merged commit 25b9e9f into master Oct 7, 2025
7 checks passed
@dlzhry2nhs dlzhry2nhs deleted the feature/VED-821-refactor-get-by-id branch October 7, 2025 15:24
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