From 27e4c8637baf4784e3e0ab80eed9ad5220dd00ad Mon Sep 17 00:00:00 2001 From: eesa456 Date: Sun, 10 Nov 2024 20:10:38 +0000 Subject: [PATCH 01/25] NRL-1053 add category parameter to search requests --- .../search_document_reference.py | 16 +++++++++- .../search_post_document_reference.py | 20 ++++++++++-- api/consumer/swagger.yaml | 32 +++++++++++++++++++ .../search_document_reference.py | 14 +++++++- .../search_post_document_reference.py | 14 +++++++- api/producer/swagger.yaml | 32 +++++++++++++++++++ layer/nrlf/consumer/fhir/r4/model.py | 7 +++- layer/nrlf/core/dynamodb/repository.py | 28 ++++++++++++++++ layer/nrlf/core/log_references.py | 14 +++++++- layer/nrlf/core/validators.py | 24 +++++++++++++- layer/nrlf/producer/fhir/r4/model.py | 7 +++- layer/nrlf/producer/fhir/r4/strict_model.py | 7 +++- 12 files changed, 205 insertions(+), 10 deletions(-) diff --git a/api/consumer/searchDocumentReference/search_document_reference.py b/api/consumer/searchDocumentReference/search_document_reference.py index b313e11b1..397ea0580 100644 --- a/api/consumer/searchDocumentReference/search_document_reference.py +++ b/api/consumer/searchDocumentReference/search_document_reference.py @@ -9,7 +9,7 @@ from nrlf.core.logger import LogReference, logger from nrlf.core.model import ConnectionMetadata, ConsumerRequestParams from nrlf.core.response import Response, SpineErrorResponse -from nrlf.core.validators import validate_type_system +from nrlf.core.validators import validate_category, validate_type_system @request_handler(params=ConsumerRequestParams) @@ -58,6 +58,16 @@ def handler( expression="type", ) + if not validate_category(params.category): + logger.log( + LogReference.CONSEARCH002b, + type=params.category, + ) # TODO - Should update error message once permissioning by category is implemented + return SpineErrorResponse.INVALID_CODE_SYSTEM( + diagnostics="Invalid query parameter (The provided category is not valid)", + expression="category", + ) + custodian_id = ( params.custodian_identifier.root.split("|", maxsplit=1)[1] if params.custodian_identifier @@ -70,6 +80,9 @@ def handler( if params.type: self_link += f"&type={params.type.root}" + if params.category: + self_link += f"&category={params.category.root}" + bundle = { "resourceType": "Bundle", "type": "searchset", @@ -89,6 +102,7 @@ def handler( nhs_number=params.nhs_number, custodian=custodian_id, pointer_types=pointer_types, + categories=[params.category.root] if params.category else [], ): try: document_reference = DocumentReference.model_validate_json(result.document) diff --git a/api/consumer/searchPostDocumentReference/search_post_document_reference.py b/api/consumer/searchPostDocumentReference/search_post_document_reference.py index 353ebf5c5..34a35cc1e 100644 --- a/api/consumer/searchPostDocumentReference/search_post_document_reference.py +++ b/api/consumer/searchPostDocumentReference/search_post_document_reference.py @@ -9,7 +9,7 @@ from nrlf.core.logger import LogReference, logger from nrlf.core.model import ConnectionMetadata, ConsumerRequestParams from nrlf.core.response import Response, SpineErrorResponse -from nrlf.core.validators import validate_type_system +from nrlf.core.validators import validate_category, validate_type_system @request_handler(body=ConsumerRequestParams) @@ -61,6 +61,16 @@ def handler( expression="type", ) + if not validate_category(body.category): + logger.log( + LogReference.CONPOSTSEARCH002b, + type=body.category, + ) # TODO - Should update error message once permissioning by category is implemented + return SpineErrorResponse.INVALID_CODE_SYSTEM( + diagnostics="Invalid query parameter (The provided category is not valid)", + expression="category", + ) + custodian_id = ( body.custodian_identifier.root.split("|", maxsplit=1)[1] if body.custodian_identifier @@ -73,6 +83,9 @@ def handler( if body.type: self_link += f"&type={body.type.root}" + if body.category: + self_link += f"&category={body.category.root}" + bundle = { "resourceType": "Bundle", "type": "searchset", @@ -89,7 +102,10 @@ def handler( ) for result in repository.search( - nhs_number=body.nhs_number, custodian=custodian_id, pointer_types=pointer_types + nhs_number=body.nhs_number, + custodian=custodian_id, + pointer_types=pointer_types, + categories=[body.category.root] if body.category else [], ): try: document_reference = DocumentReference.model_validate_json(result.document) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index 6e831a1bd..fd5ce2535 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1399,6 +1399,8 @@ components: $ref: "#/components/schemas/RequestQueryCustodian" type: $ref: "#/components/schemas/RequestQueryType" + category: + $ref: "#/components/schemas/RequestQueryCategory" next-page-token: $ref: "#/components/schemas/NextPageToken" required: @@ -1421,6 +1423,9 @@ components: RequestQueryType: type: string example: "http://snomed.info/sct|736253002" + RequestQueryCategory: + type: string + example: "http://snomed.info/sct|103693007" NextPageToken: type: string RequestHeaderOdsCode: @@ -1531,6 +1536,33 @@ components: invalid: summary: Unknown value: http://snomed.info/sct|410970009 + category: + name: category + in: query + schema: + $ref: "#/components/schemas/RequestQueryCategory" + examples: + none: + summary: None + value: "" + SNOMED_CODES_CARE_PLAN: + summary: Care plan + value: http://snomed.info/sct|734163000 + SNOMED_CODES_OBSERVATIONS: + summary: Observations + value: http://snomed.info/sct|1102421000000108 + SNOMED_CODES_CLINICAL_NOTE: + summary: Clinical note + value: http://snomed.info/sct|823651000000106 + SNOMED_CODES_DIAGNOSTIC_STUDIES_REPORT: + summary: Diagnostic studies report + value: http://snomed.info/sct|721981007 + SNOMED_CODES_DIAGNOSTIC_PROCEDURE: + summary: RDiagnostic procedure + value: http://snomed.info/sct|103693007 + invalid: + summary: Unknown + value: http://snomed.info/sct|410970009 nextPageToken: name: next-page-token description: | diff --git a/api/producer/searchDocumentReference/search_document_reference.py b/api/producer/searchDocumentReference/search_document_reference.py index 133d47fd1..9f25d7c94 100644 --- a/api/producer/searchDocumentReference/search_document_reference.py +++ b/api/producer/searchDocumentReference/search_document_reference.py @@ -6,7 +6,7 @@ from nrlf.core.logger import LogReference, logger from nrlf.core.model import ConnectionMetadata, ProducerRequestParams from nrlf.core.response import Response, SpineErrorResponse -from nrlf.core.validators import validate_type_system +from nrlf.core.validators import validate_category, validate_type_system from nrlf.producer.fhir.r4.model import Bundle, DocumentReference @@ -59,6 +59,16 @@ def handler( expression="type", ) + if not validate_category(params.category): + logger.log( + LogReference.PROSEARCH002b, + type=params.category, + ) # TODO - Should update error message once permissioning by category is implemented + return SpineErrorResponse.INVALID_CODE_SYSTEM( + diagnostics="Invalid query parameter (The provided category is not valid)", + expression="category", + ) + pointer_types = [params.type.root] if params.type else metadata.pointer_types bundle = {"resourceType": "Bundle", "type": "searchset", "total": 0, "entry": []} @@ -68,6 +78,7 @@ def handler( custodian_suffix=metadata.ods_code_extension, nhs_number=params.nhs_number, pointer_types=pointer_types, + categories=[params.category.root], ) for result in repository.search( @@ -75,6 +86,7 @@ def handler( custodian_suffix=metadata.ods_code_extension, nhs_number=params.nhs_number, pointer_types=pointer_types, + categories=[params.category.root] if params.category else [], ): try: document_reference = DocumentReference.model_validate_json(result.document) diff --git a/api/producer/searchPostDocumentReference/search_post_document_reference.py b/api/producer/searchPostDocumentReference/search_post_document_reference.py index 35291de6b..b4f4737e0 100644 --- a/api/producer/searchPostDocumentReference/search_post_document_reference.py +++ b/api/producer/searchPostDocumentReference/search_post_document_reference.py @@ -6,7 +6,7 @@ from nrlf.core.logger import LogReference, logger from nrlf.core.model import ConnectionMetadata, ProducerRequestParams from nrlf.core.response import Response, SpineErrorResponse -from nrlf.core.validators import validate_type_system +from nrlf.core.validators import validate_category, validate_type_system from nrlf.producer.fhir.r4.model import Bundle, DocumentReference @@ -53,6 +53,16 @@ def handler( expression="type", ) + if not validate_category(body.category): + logger.log( + LogReference.PROPOSTSEARCH002b, + type=body.category, + ) # TODO - Should update error message once permissioning by category is implemented + return SpineErrorResponse.INVALID_CODE_SYSTEM( + diagnostics="Invalid query parameter (The provided category is not valid)", + expression="category", + ) + pointer_types = [body.type.root] if body.type else metadata.pointer_types bundle = {"resourceType": "Bundle", "type": "searchset", "total": 0, "entry": []} @@ -62,6 +72,7 @@ def handler( custodian_suffix=metadata.ods_code_extension, nhs_number=body.nhs_number, pointer_types=pointer_types, + categories=[body.category.root] if body.category else [], ) for result in repository.search( @@ -69,6 +80,7 @@ def handler( custodian_suffix=metadata.ods_code_extension, nhs_number=body.nhs_number, pointer_types=pointer_types, + categories=[body.category.root] if body.category else [], ): try: document_reference = DocumentReference.model_validate_json(result.document) diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index 5cadfa08b..7a6812a6d 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1934,6 +1934,8 @@ components: $ref: "#/components/schemas/RequestQuerySubject" type: $ref: "#/components/schemas/RequestQueryType" + category: + $ref: "#/components/schemas/RequestQueryCategory" next-page-token: $ref: "#/components/schemas/NextPageToken" RequestQuerySubject: @@ -1943,6 +1945,9 @@ components: RequestQueryType: type: string example: "http://snomed.info/sct|736253002" + RequestQueryCategory: + type: string + example: "http://snomed.info/sct|103693007" NextPageToken: type: string RequestHeaderOdsCode: @@ -2037,6 +2042,33 @@ components: invalid: summary: Unknown value: http://snomed.info/sct|410970009 + category: + name: category + in: query + schema: + $ref: "#/components/schemas/RequestQueryCategory" + examples: + none: + summary: None + value: "" + SNOMED_CODES_CARE_PLAN: + summary: Care plan + value: http://snomed.info/sct|734163000 + SNOMED_CODES_OBSERVATIONS: + summary: Observations + value: http://snomed.info/sct|1102421000000108 + SNOMED_CODES_CLINICAL_NOTE: + summary: Clinical note + value: http://snomed.info/sct|823651000000106 + SNOMED_CODES_DIAGNOSTIC_STUDIES_REPORT: + summary: Diagnostic studies report + value: http://snomed.info/sct|721981007 + SNOMED_CODES_DIAGNOSTIC_PROCEDURE: + summary: RDiagnostic procedure + value: http://snomed.info/sct|103693007 + invalid: + summary: Unknown + value: http://snomed.info/sct|410970009 nextPageToken: name: next-page-token in: query diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index da76fe858..061754db2 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-10-28T10:30:48+00:00 +# timestamp: 2024-11-10T19:12:31+00:00 from __future__ import annotations @@ -397,6 +397,10 @@ class RequestQueryType(RootModel[str]): root: Annotated[str, Field(examples=["http://snomed.info/sct|736253002"])] +class RequestQueryCategory(RootModel[str]): + root: Annotated[str, Field(examples=["http://snomed.info/sct|103693007"])] + + class NextPageToken(RootModel[str]): root: str @@ -435,6 +439,7 @@ class RequestParams(BaseModel): Optional[RequestQueryCustodian], Field(alias="custodian:identifier") ] = None type: Optional[RequestQueryType] = None + category: Optional[RequestQueryCategory] = None next_page_token: Annotated[ Optional[NextPageToken], Field(alias="next-page-token") ] = None diff --git a/layer/nrlf/core/dynamodb/repository.py b/layer/nrlf/core/dynamodb/repository.py index 8b1026bbb..261b72714 100644 --- a/layer/nrlf/core/dynamodb/repository.py +++ b/layer/nrlf/core/dynamodb/repository.py @@ -33,6 +33,15 @@ def _get_sk_ids_for_type(pointer_type: str) -> tuple: return category_id, type_id +def _get_sk_id_for_category(category: str) -> tuple: + category_system, category_code = category.split("|") + if category_system not in SYSTEM_SHORT_IDS: + raise ValueError(f"Unknown system for category: {category_system}") + category_id = SYSTEM_SHORT_IDS[category_system] + "-" + category_code + + return category_id + + class Repository(ABC, Generic[RepositoryModel]): ITEM_TYPE: Type[RepositoryModel] @@ -218,6 +227,7 @@ def search( custodian: Optional[str] = None, custodian_suffix: Optional[str] = None, pointer_types: Optional[List[str]] = [], + categories: Optional[List[str]] = [], ) -> Iterator[DocumentPointer]: """""" logger.log( @@ -239,6 +249,24 @@ def search( key_conditions.append("begins_with(patient_sort, :patient_sort)") expression_values[":patient_sort"] = patient_sort + if len(categories) == 1: + # Optimisation for single category + category_id = _get_sk_id_for_category(categories[0]) + patient_sort = f"C#{category_id}" + key_conditions.append("begins_with(patient_sort, :patient_sort)") + expression_values[":patient_sort"] = patient_sort + + if len(categories) > 1: + expression_names["#category"] = "category" + category_filters = [ + f"#category = :category_{i}" for i in range(len(categories)) + ] + caetgory_filter_values = { + f":category_{i}": categories[i] for i in range(len(categories)) + } + filter_expressions.append(f"({' OR '.join(category_filters)})") + expression_values.update(caetgory_filter_values) + # Handle multiple categories and pointer types with filter expressions if len(pointer_types) > 1: expression_names["#pointer_type"] = "type" diff --git a/layer/nrlf/core/log_references.py b/layer/nrlf/core/log_references.py index 095a1e704..a140b7164 100644 --- a/layer/nrlf/core/log_references.py +++ b/layer/nrlf/core/log_references.py @@ -175,6 +175,9 @@ class LogReference(Enum): CONSEARCH002 = _Reference( "INFO", "Invalid document type provided in the query parameters" ) + CONSEARCH002b = _Reference( + "INFO", "Invalid document category provided in the query parameters" + ) CONSEARCH003 = _Reference("DEBUG", "Performing search by NHS number") CONSEARCH004 = _Reference( "DEBUG", "Parsed DocumentReference and added to search results" @@ -196,6 +199,9 @@ class LogReference(Enum): CONPOSTSEARCH002 = _Reference( "INFO", "Invalid document type provided in the request body" ) + CONPOSTSEARCH002b = _Reference( + "INFO", "Invalid document category provided in the request body" + ) CONPOSTSEARCH003 = _Reference("DEBUG", "Performing search by NHS number") CONPOSTSEARCH004 = _Reference( "DEBUG", "Parsed DocumentReference and added to search results" @@ -350,12 +356,15 @@ class LogReference(Enum): PROSEARCH002 = _Reference( "INFO", "Invalid document type provided in the query parameters" ) + PROSEARCH002b = _Reference( + "INFO", "Invalid document category provided in the query parameters" + ) PROSEARCH003 = _Reference("DEBUG", "Performing search by custodian") PROSEARCH004 = _Reference( "DEBUG", "Parsed DocumentReference and added to search results" ) PROSEARCH005 = _Reference( - "EXCEPTION", "The DocumentReference esource could not be parsed" + "EXCEPTION", "The DocumentReference resource could not be parsed" ) PROSEARCH999 = _Reference( "INFO", "Successfully completed producer searchDocumentReference" @@ -371,6 +380,9 @@ class LogReference(Enum): PROPOSTSEARCH002 = _Reference( "INFO", "Invalid document type provided in the request body" ) + PROPOSTSEARCH002b = _Reference( + "INFO", "Invalid document category provided in the request body" + ) PROPOSTSEARCH003 = _Reference("DEBUG", "Performing search by custodian") PROPOSTSEARCH004 = _Reference( "DEBUG", "Parsed DocumentReference and added to search results" diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index 061b11134..ddb70f2f0 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -2,10 +2,11 @@ from re import match from typing import Any, Dict, List, Optional +from consumer.fhir.r4.model import RequestQueryCategory from pydantic import ValidationError from nrlf.core.codes import SpineErrorConcept -from nrlf.core.constants import CATEGORY_ATTRIBUTES, REQUIRED_CREATE_FIELDS +from nrlf.core.constants import CATEGORY_ATTRIBUTES, REQUIRED_CREATE_FIELDS, Categories from nrlf.core.errors import ParseError from nrlf.core.logger import LogReference, logger from nrlf.core.types import DocumentReference, OperationOutcomeIssue, RequestQueryType @@ -29,6 +30,27 @@ def validate_type_system( return type_system in pointer_type_systems +# TODO - Validate category is in set permissions once permissioning by category is done. +def validate_category(category_: Optional[RequestQueryCategory]) -> bool: + """ + Validates if the given category is valid. + """ + if not category_: + return True + + category_system = category_.root.split("|", 1)[0] + category_code = category_.root.split("|", 1)[1] + + category_list = Categories.list() + category_systems = [category.split("|", 1)[0] for category in category_list] + category_codes = [category.split("|", 1)[1] for category in category_list] + + if not category_system in category_systems: + return False + + return category_code in category_codes + + @dataclass class ValidationResult: resource: DocumentReference diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index 697b8afdb..f0655bf0a 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-10-28T10:30:43+00:00 +# timestamp: 2024-11-10T19:12:27+00:00 from __future__ import annotations @@ -387,6 +387,10 @@ class RequestQueryType(RootModel[str]): root: Annotated[str, Field(examples=["http://snomed.info/sct|736253002"])] +class RequestQueryCategory(RootModel[str]): + root: Annotated[str, Field(examples=["http://snomed.info/sct|103693007"])] + + class NextPageToken(RootModel[str]): root: str @@ -422,6 +426,7 @@ class RequestParams(BaseModel): Optional[RequestQuerySubject], Field(alias="subject:identifier") ] = None type: Optional[RequestQueryType] = None + category: Optional[RequestQueryCategory] = None next_page_token: Annotated[ Optional[NextPageToken], Field(alias="next-page-token") ] = None diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index 78545b2bf..627392182 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-10-28T10:30:45+00:00 +# timestamp: 2024-11-10T19:12:29+00:00 from __future__ import annotations @@ -342,6 +342,10 @@ class RequestQueryType(RootModel[StrictStr]): root: Annotated[StrictStr, Field(examples=["http://snomed.info/sct|736253002"])] +class RequestQueryCategory(RootModel[StrictStr]): + root: Annotated[StrictStr, Field(examples=["http://snomed.info/sct|103693007"])] + + class NextPageToken(RootModel[StrictStr]): root: StrictStr @@ -371,6 +375,7 @@ class RequestParams(BaseModel): Optional[RequestQuerySubject], Field(alias="subject:identifier") ] = None type: Optional[RequestQueryType] = None + category: Optional[RequestQueryCategory] = None next_page_token: Annotated[ Optional[NextPageToken], Field(alias="next-page-token") ] = None From 000c691f23cef1af23400c8c02224637b715b106 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Sun, 10 Nov 2024 21:22:04 +0000 Subject: [PATCH 02/25] NRL-1053 fix import --- layer/nrlf/core/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index ddb70f2f0..025a2a6d4 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -2,9 +2,9 @@ from re import match from typing import Any, Dict, List, Optional -from consumer.fhir.r4.model import RequestQueryCategory from pydantic import ValidationError +from nrlf.consumer.fhir.r4.model import RequestQueryCategory from nrlf.core.codes import SpineErrorConcept from nrlf.core.constants import CATEGORY_ATTRIBUTES, REQUIRED_CREATE_FIELDS, Categories from nrlf.core.errors import ParseError From ddfdf7d46b964ab57efc511bfbf509bed95b1fc0 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Sun, 10 Nov 2024 21:27:18 +0000 Subject: [PATCH 03/25] NRL-1053 fix log and tests --- .../searchDocumentReference/search_document_reference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/producer/searchDocumentReference/search_document_reference.py b/api/producer/searchDocumentReference/search_document_reference.py index 9f25d7c94..d2dad8fd5 100644 --- a/api/producer/searchDocumentReference/search_document_reference.py +++ b/api/producer/searchDocumentReference/search_document_reference.py @@ -78,7 +78,7 @@ def handler( custodian_suffix=metadata.ods_code_extension, nhs_number=params.nhs_number, pointer_types=pointer_types, - categories=[params.category.root], + categories=[params.category.root] if params.category else [], ) for result in repository.search( From 4b5bc392b7c1dd18ad43603bca74089b9afc9cce Mon Sep 17 00:00:00 2001 From: eesa456 Date: Tue, 12 Nov 2024 10:14:12 +0000 Subject: [PATCH 04/25] NRL-1053 add category to components yaml for query params --- swagger/consumer-static/components.yaml | 5 +++++ swagger/producer-static/components.yaml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/swagger/consumer-static/components.yaml b/swagger/consumer-static/components.yaml index 8201e93fc..26580034c 100644 --- a/swagger/consumer-static/components.yaml +++ b/swagger/consumer-static/components.yaml @@ -36,6 +36,11 @@ components: in: query schema: $ref: "#/components/schemas/RequestQueryType" + category: + name: type + in: query + schema: + $ref: "#/components/schemas/RequestQueryCategory" nextPageToken: name: next-page-token description: | diff --git a/swagger/producer-static/components.yaml b/swagger/producer-static/components.yaml index 80e027ecb..f83f45d88 100644 --- a/swagger/producer-static/components.yaml +++ b/swagger/producer-static/components.yaml @@ -18,6 +18,11 @@ components: in: query schema: $ref: "#/components/schemas/RequestQueryType" + category: + name: type + in: query + schema: + $ref: "#/components/schemas/RequestQueryCategory" nextPageToken: name: next-page-token in: query From e1e280d7c42470caf5e893506b039a8ae52cd51d Mon Sep 17 00:00:00 2001 From: eesa456 Date: Tue, 12 Nov 2024 11:39:00 +0000 Subject: [PATCH 05/25] NRL-1053 add import back --- layer/nrlf/core/validators.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index 302fe3078..df5791ed7 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -6,7 +6,12 @@ from nrlf.consumer.fhir.r4.model import RequestQueryCategory from nrlf.core.codes import SpineErrorConcept -from nrlf.core.constants import CATEGORY_ATTRIBUTES, REQUIRED_CREATE_FIELDS, Categories +from nrlf.core.constants import ( + CATEGORY_ATTRIBUTES, + ODS_SYSTEM, + REQUIRED_CREATE_FIELDS, + Categories, +) from nrlf.core.errors import ParseError from nrlf.core.logger import LogReference, logger from nrlf.core.types import DocumentReference, OperationOutcomeIssue, RequestQueryType From 062c31ecbb1e610706fcfcec89ec998982f029ab Mon Sep 17 00:00:00 2001 From: eesa456 Date: Tue, 12 Nov 2024 15:36:25 +0000 Subject: [PATCH 06/25] NRL-1053 add to api spec yaml --- swagger/consumer-static/components.yaml | 4 ++++ swagger/consumer-static/narrative.yaml | 2 ++ swagger/producer-static/components.yaml | 4 ++++ swagger/producer-static/narrative.yaml | 2 ++ 4 files changed, 12 insertions(+) diff --git a/swagger/consumer-static/components.yaml b/swagger/consumer-static/components.yaml index 26580034c..c5f07a505 100644 --- a/swagger/consumer-static/components.yaml +++ b/swagger/consumer-static/components.yaml @@ -136,6 +136,8 @@ components: $ref: "#/components/schemas/RequestQueryCustodian" type: $ref: "#/components/schemas/RequestQueryType" + category: + $ref: "#/components/schemas/RequestQueryCategory" next-page-token: $ref: "#/components/schemas/NextPageToken" required: @@ -156,6 +158,8 @@ components: example: "https://fhir.nhs.uk/Id/ods-organization-code|Y05868" RequestQueryType: type: string + RequestQueryCategory: + type: string NextPageToken: type: string RequestHeaderOdsCode: diff --git a/swagger/consumer-static/narrative.yaml b/swagger/consumer-static/narrative.yaml index 0250bd219..a349758ea 100644 --- a/swagger/consumer-static/narrative.yaml +++ b/swagger/consumer-static/narrative.yaml @@ -624,6 +624,8 @@ components: example: "https://fhir.nhs.uk/Id/ods-organization-code|Y05868" RequestQueryType: example: "http://snomed.info/sct|736253002" + RequestQueryCategory: + example: "http://snomed.info/sct|734163000" parameters: id: diff --git a/swagger/producer-static/components.yaml b/swagger/producer-static/components.yaml index f83f45d88..22a096326 100644 --- a/swagger/producer-static/components.yaml +++ b/swagger/producer-static/components.yaml @@ -186,6 +186,8 @@ components: $ref: "#/components/schemas/RequestQuerySubject" type: $ref: "#/components/schemas/RequestQueryType" + category: + $ref: "#/components/schemas/RequestQueryCategory" next-page-token: $ref: "#/components/schemas/NextPageToken" RequestQuerySubject: @@ -193,6 +195,8 @@ components: pattern: ^https\:\/\/fhir\.nhs\.uk\/Id\/nhs-number\|(\d+)$ RequestQueryType: type: string + RequestQueryCategory: + type: string NextPageToken: type: string RequestHeaderOdsCode: diff --git a/swagger/producer-static/narrative.yaml b/swagger/producer-static/narrative.yaml index 6316c581c..4341faf86 100644 --- a/swagger/producer-static/narrative.yaml +++ b/swagger/producer-static/narrative.yaml @@ -1195,6 +1195,8 @@ components: example: "https://fhir.nhs.uk/Id/nhs-number|4409815415" RequestQueryType: example: "http://snomed.info/sct|736253002" + RequestQueryCategory: + example: "http://snomed.info/sct|734163000" RequestHeaderRequestId: example: 60E0B220-8136-4CA5-AE46-1D97EF59D068 RequestHeaderCorrelationId: From 656bfadc71b09de6111598f896dd43535fe6e557 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Tue, 12 Nov 2024 19:53:10 +0000 Subject: [PATCH 07/25] NRL-1053 add category param --- api/consumer/swagger.yaml | 1 + api/producer/swagger.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index fd5ce2535..69e5d1c36 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -257,6 +257,7 @@ paths: - $ref: "#/components/parameters/subject" - $ref: "#/components/parameters/custodian" - $ref: "#/components/parameters/type" + - $ref: "#/components/parameters/category" - $ref: "#/components/parameters/nextPageToken" - $ref: "#/components/parameters/odsCode" - $ref: "#/components/parameters/odsCodeExtension" diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index 49f4b1c2b..81fcd49f3 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -396,6 +396,7 @@ paths: parameters: - $ref: "#/components/parameters/subject" - $ref: "#/components/parameters/type" + - $ref: "#/components/parameters/category" - $ref: "#/components/parameters/nextPageToken" - $ref: "#/components/parameters/odsCode" - $ref: "#/components/parameters/odsCodeExtension" From 7fbc81b9cbb3946b686861d7bfca914058e16ca2 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Tue, 12 Nov 2024 21:02:24 +0000 Subject: [PATCH 08/25] NRL-1053 add unit tests --- ...test_search_document_reference_consumer.py | 86 ++++++++++++++++++ .../search_post_document_reference.py | 2 +- ...search_post_document_reference_consumer.py | 90 +++++++++++++++++++ ...test_search_document_reference_producer.py | 82 ++++++++++++++++- .../search_post_document_reference.py | 2 +- ...search_post_document_reference_producer.py | 86 +++++++++++++++++- layer/nrlf/core/tests/test_model.py | 1 + tests/utilities/api_clients.py | 16 +++- 8 files changed, 360 insertions(+), 5 deletions(-) diff --git a/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py b/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py index 72d1b70b8..1b8cd23b1 100644 --- a/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py +++ b/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py @@ -135,6 +135,47 @@ def test_search_document_reference_happy_path_with_type( } +@mock_aws +@mock_repository +def test_search_document_reference_happy_path_with_category( + repository: DocumentPointerRepository, +): + doc_ref = load_document_reference("Y05868-736253002-Valid") + doc_pointer = DocumentPointer.from_document_reference(doc_ref) + repository.create(doc_pointer) + + event = create_test_api_gateway_event( + headers=create_headers(), + query_string_parameters={ + "subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|6700028191", + "category": "http://snomed.info/sct|734163000", + }, + ) + + result = handler(event, create_mock_context()) + body = result.pop("body") + + assert result == { + "statusCode": "200", + "headers": default_response_headers(), + "isBase64Encoded": False, + } + + parsed_body = json.loads(body) + assert parsed_body == { + "resourceType": "Bundle", + "type": "searchset", + "link": [ + { + "relation": "self", + "url": "https://pytest.api.service.nhs.uk/record-locator/consumer/FHIR/R4/DocumentReference?subject:identifier=https://fhir.nhs.uk/Id/nhs-number|6700028191&category=http://snomed.info/sct|734163000", + } + ], + "total": 1, + "entry": [{"resource": doc_ref.model_dump(exclude_none=True)}], + } + + @mock_aws @mock_repository def test_search_document_reference_happy_path_with_nicip_type( @@ -342,6 +383,51 @@ def test_search_document_reference_invalid_type(repository: DocumentPointerRepos } +@mock_aws +@mock_repository +def test_search_document_reference_invalid_category( + repository: DocumentPointerRepository, +): + event = create_test_api_gateway_event( + headers=create_headers(), + query_string_parameters={ + "subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|6700028191", + "category": "https://fhir.nhs.uk/CodeSystem/Document-Type|invalid", + }, + ) + + result = handler(event, create_mock_context()) + body = result.pop("body") + + assert result == { + "statusCode": "400", + "headers": default_response_headers(), + "isBase64Encoded": False, + } + + parsed_body = json.loads(body) + assert parsed_body == { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "error", + "code": "code-invalid", + "details": { + "coding": [ + { + "code": "INVALID_CODE_SYSTEM", + "display": "Invalid code system", + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + } + ] + }, + "diagnostics": "Invalid query parameter (The provided category is not valid)", + "expression": ["category"], + } + ], + } + + @mock_aws @mock_repository def test_search_document_reference_invalid_json(repository: DocumentPointerRepository): diff --git a/api/consumer/searchPostDocumentReference/search_post_document_reference.py b/api/consumer/searchPostDocumentReference/search_post_document_reference.py index 34a35cc1e..18ac8e606 100644 --- a/api/consumer/searchPostDocumentReference/search_post_document_reference.py +++ b/api/consumer/searchPostDocumentReference/search_post_document_reference.py @@ -67,7 +67,7 @@ def handler( type=body.category, ) # TODO - Should update error message once permissioning by category is implemented return SpineErrorResponse.INVALID_CODE_SYSTEM( - diagnostics="Invalid query parameter (The provided category is not valid)", + diagnostics="The provided category is not valid", expression="category", ) diff --git a/api/consumer/searchPostDocumentReference/tests/test_search_post_document_reference_consumer.py b/api/consumer/searchPostDocumentReference/tests/test_search_post_document_reference_consumer.py index 016f6ebc9..3dc41ba45 100644 --- a/api/consumer/searchPostDocumentReference/tests/test_search_post_document_reference_consumer.py +++ b/api/consumer/searchPostDocumentReference/tests/test_search_post_document_reference_consumer.py @@ -144,6 +144,49 @@ def test_search_post_document_reference_happy_path_with_type( } +@mock_aws +@mock_repository +def test_search_post_document_reference_happy_path_with_category( + repository: DocumentPointerRepository, +): + doc_ref = load_document_reference("Y05868-736253002-Valid") + doc_pointer = DocumentPointer.from_document_reference(doc_ref) + repository.create(doc_pointer) + + event = create_test_api_gateway_event( + headers=create_headers(), + body=json.dumps( + { + "subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|6700028191", + "category": "http://snomed.info/sct|734163000", + }, + ), + ) + + result = handler(event, create_mock_context()) + body = result.pop("body") + + assert result == { + "statusCode": "200", + "headers": default_response_headers(), + "isBase64Encoded": False, + } + + parsed_body = json.loads(body) + assert parsed_body == { + "resourceType": "Bundle", + "type": "searchset", + "link": [ + { + "relation": "self", + "url": "https://pytest.api.service.nhs.uk/record-locator/consumer/FHIR/R4/DocumentReference?subject:identifier=https://fhir.nhs.uk/Id/nhs-number|6700028191&category=http://snomed.info/sct|734163000", + } + ], + "total": 1, + "entry": [{"resource": doc_ref.model_dump(exclude_none=True)}], + } + + @mock_aws @mock_repository def test_search_document_reference_no_results(repository: DocumentPointerRepository): @@ -310,6 +353,53 @@ def test_search_post_document_reference_invalid_type( } +@mock_aws +@mock_repository +def test_search_document_reference_invalid_category( + repository: DocumentPointerRepository, +): + event = create_test_api_gateway_event( + headers=create_headers(), + body=json.dumps( + { + "subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|6700028191", + "category": "https://fhir.nhs.uk/CodeSystem/Document-Type|invalid", + } + ), + ) + + result = handler(event, create_mock_context()) + body = result.pop("body") + + assert result == { + "statusCode": "400", + "headers": default_response_headers(), + "isBase64Encoded": False, + } + + parsed_body = json.loads(body) + assert parsed_body == { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "error", + "code": "code-invalid", + "details": { + "coding": [ + { + "code": "INVALID_CODE_SYSTEM", + "display": "Invalid code system", + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + } + ] + }, + "diagnostics": "The provided category is not valid", + "expression": ["category"], + } + ], + } + + @mock_aws @mock_repository def test_search_post_document_reference_invalid_json( diff --git a/api/producer/searchDocumentReference/tests/test_search_document_reference_producer.py b/api/producer/searchDocumentReference/tests/test_search_document_reference_producer.py index e571bedef..dc68d6e30 100644 --- a/api/producer/searchDocumentReference/tests/test_search_document_reference_producer.py +++ b/api/producer/searchDocumentReference/tests/test_search_document_reference_producer.py @@ -3,7 +3,7 @@ from moto import mock_aws from api.producer.searchDocumentReference.search_document_reference import handler -from nrlf.core.constants import PointerTypes +from nrlf.core.constants import Categories, PointerTypes from nrlf.core.dynamodb.repository import DocumentPointer, DocumentPointerRepository from nrlf.tests.data import load_document_reference from nrlf.tests.dynamodb import mock_repository @@ -203,6 +203,51 @@ def test_search_document_reference_invalid_type(repository: DocumentPointerRepos } +@mock_aws +@mock_repository +def test_search_document_reference_invalid_category( + repository: DocumentPointerRepository, +): + event = create_test_api_gateway_event( + headers=create_headers(), + query_string_parameters={ + "subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|6700028191", + "category": "https://fhir.nhs.uk/CodeSystem/Document-Type|invalid", + }, + ) + + result = handler(event, create_mock_context()) + body = result.pop("body") + + assert result == { + "statusCode": "400", + "headers": default_response_headers(), + "isBase64Encoded": False, + } + + parsed_body = json.loads(body) + assert parsed_body == { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "error", + "code": "code-invalid", + "details": { + "coding": [ + { + "code": "INVALID_CODE_SYSTEM", + "display": "Invalid code system", + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + } + ] + }, + "diagnostics": "Invalid query parameter (The provided category is not valid)", + "expression": ["category"], + } + ], + } + + @mock_aws @mock_repository def test_search_document_reference_only_returns_custodian_pointers( @@ -272,6 +317,41 @@ def test_search_document_reference_filters_by_type( } +@mock_aws +@mock_repository +def test_search_document_reference_filters_by_type( + repository: DocumentPointerRepository, +): + doc_ref = load_document_reference("Y05868-736253002-Valid") + doc_pointer = DocumentPointer.from_document_reference(doc_ref) + repository.create(doc_pointer) + + event = create_test_api_gateway_event( + headers=create_headers(), + query_string_parameters={ + "subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|6700028191", + "category": Categories.CARE_PLAN.value, + }, + ) + + result = handler(event, create_mock_context()) + body = result.pop("body") + + assert result == { + "statusCode": "200", + "headers": default_response_headers(), + "isBase64Encoded": False, + } + + parsed_body = json.loads(body) + assert parsed_body == { + "resourceType": "Bundle", + "type": "searchset", + "total": 1, + "entry": [{"resource": doc_ref.model_dump(exclude_none=True)}], + } + + @mock_aws @mock_repository def test_search_document_reference_filters_by_pointer_types( diff --git a/api/producer/searchPostDocumentReference/search_post_document_reference.py b/api/producer/searchPostDocumentReference/search_post_document_reference.py index b4f4737e0..12c1144e1 100644 --- a/api/producer/searchPostDocumentReference/search_post_document_reference.py +++ b/api/producer/searchPostDocumentReference/search_post_document_reference.py @@ -59,7 +59,7 @@ def handler( type=body.category, ) # TODO - Should update error message once permissioning by category is implemented return SpineErrorResponse.INVALID_CODE_SYSTEM( - diagnostics="Invalid query parameter (The provided category is not valid)", + diagnostics="The provided category is not valid", expression="category", ) diff --git a/api/producer/searchPostDocumentReference/tests/test_search_post_document_reference_producer.py b/api/producer/searchPostDocumentReference/tests/test_search_post_document_reference_producer.py index 9fe1b7db1..66bd579a8 100644 --- a/api/producer/searchPostDocumentReference/tests/test_search_post_document_reference_producer.py +++ b/api/producer/searchPostDocumentReference/tests/test_search_post_document_reference_producer.py @@ -5,7 +5,7 @@ from api.producer.searchPostDocumentReference.search_post_document_reference import ( handler, ) -from nrlf.core.constants import PointerTypes +from nrlf.core.constants import Categories, PointerTypes from nrlf.core.dynamodb.repository import DocumentPointer, DocumentPointerRepository from nrlf.tests.data import load_document_reference from nrlf.tests.dynamodb import mock_repository @@ -208,6 +208,53 @@ def test_search_document_reference_invalid_type(repository: DocumentPointerRepos } +@mock_aws +@mock_repository +def test_search_document_reference_invalid_category( + repository: DocumentPointerRepository, +): + event = create_test_api_gateway_event( + headers=create_headers(), + body=json.dumps( + { + "subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|6700028191", + "category": "https://fhir.nhs.uk/CodeSystem/Document-Type|invalid", + } + ), + ) + + result = handler(event, create_mock_context()) + body = result.pop("body") + + assert result == { + "statusCode": "400", + "headers": default_response_headers(), + "isBase64Encoded": False, + } + + parsed_body = json.loads(body) + assert parsed_body == { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "error", + "code": "code-invalid", + "details": { + "coding": [ + { + "code": "INVALID_CODE_SYSTEM", + "display": "Invalid code system", + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + } + ] + }, + "diagnostics": "The provided category is not valid", + "expression": ["category"], + } + ], + } + + @mock_aws @mock_repository def test_search_document_reference_only_returns_custodian_pointers( @@ -281,6 +328,43 @@ def test_search_document_reference_filters_by_type( } +@mock_aws +@mock_repository +def test_search_document_reference_filters_by_category( + repository: DocumentPointerRepository, +): + doc_ref = load_document_reference("Y05868-736253002-Valid") + doc_pointer = DocumentPointer.from_document_reference(doc_ref) + repository.create(doc_pointer) + + event = create_test_api_gateway_event( + headers=create_headers(), + body=json.dumps( + { + "subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|6700028191", + "category": Categories.CARE_PLAN.value, + } + ), + ) + + result = handler(event, create_mock_context()) + body = result.pop("body") + + assert result == { + "statusCode": "200", + "headers": default_response_headers(), + "isBase64Encoded": False, + } + + parsed_body = json.loads(body) + assert parsed_body == { + "resourceType": "Bundle", + "type": "searchset", + "total": 1, + "entry": [{"resource": doc_ref.model_dump(exclude_none=True)}], + } + + @mock_aws @mock_repository def test_search_document_reference_filters_by_pointer_types( diff --git a/layer/nrlf/core/tests/test_model.py b/layer/nrlf/core/tests/test_model.py index ac254c405..c8d1c9eaa 100644 --- a/layer/nrlf/core/tests/test_model.py +++ b/layer/nrlf/core/tests/test_model.py @@ -64,6 +64,7 @@ def test_consumer_request_params(): "subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|9999999999", "custodian:identifier": "https://fhir.nhs.uk/Id/ods-organization-code|test", "type": "test-type", + "category": "test-category", "next-page-token": "page-token", } ) diff --git a/tests/utilities/api_clients.py b/tests/utilities/api_clients.py index 3a3225135..1d06bf2a8 100644 --- a/tests/utilities/api_clients.py +++ b/tests/utilities/api_clients.py @@ -3,7 +3,7 @@ import requests from pydantic import BaseModel -from nrlf.core.constants import PointerTypes +from nrlf.core.constants import Categories, PointerTypes from nrlf.core.model import ConnectionMetadata @@ -80,6 +80,7 @@ def search( nhs_number: str | None = None, custodian: str | None = None, pointer_type: PointerTypes | None = None, + category: Categories | None = None, extra_params: dict[str, str] | None = None, ): params = {**(extra_params or {})} @@ -100,6 +101,12 @@ def search( else: params["type"] = f"http://snomed.info/sct|{pointer_type}" + if category: + if "|" in category: + params["category"] = category + else: + params["category"] = f"http://snomed.info/sct|{category}" + return requests.get( f"{self.api_url}/DocumentReference", params=params, @@ -112,6 +119,7 @@ def search_post( nhs_number: str | None = None, custodian: str | None = None, pointer_type: PointerTypes | None = None, + category: Categories | None = None, extra_fields: dict[str, str] | None = None, ): body = {**(extra_fields or {})} @@ -132,6 +140,12 @@ def search_post( else: body["type"] = f"http://snomed.info/sct|{pointer_type}" + if category: + if "|" in category: + body["category"] = category + else: + body["category"] = f"http://snomed.info/sct|{category}" + return requests.post( f"{self.api_url}/DocumentReference/_search", json=body, From 58b808656812f281ff6a898e929eb1c411f75c2e Mon Sep 17 00:00:00 2001 From: eesa456 Date: Wed, 13 Nov 2024 14:33:02 +0000 Subject: [PATCH 09/25] NRL-1053 fix type and category filter expression issue --- layer/nrlf/core/dynamodb/repository.py | 41 ++++++++++---------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/layer/nrlf/core/dynamodb/repository.py b/layer/nrlf/core/dynamodb/repository.py index 261b72714..7df5776db 100644 --- a/layer/nrlf/core/dynamodb/repository.py +++ b/layer/nrlf/core/dynamodb/repository.py @@ -33,15 +33,6 @@ def _get_sk_ids_for_type(pointer_type: str) -> tuple: return category_id, type_id -def _get_sk_id_for_category(category: str) -> tuple: - category_system, category_code = category.split("|") - if category_system not in SYSTEM_SHORT_IDS: - raise ValueError(f"Unknown system for category: {category_system}") - category_id = SYSTEM_SHORT_IDS[category_system] + "-" + category_code - - return category_id - - class Repository(ABC, Generic[RepositoryModel]): ITEM_TYPE: Type[RepositoryModel] @@ -249,14 +240,26 @@ def search( key_conditions.append("begins_with(patient_sort, :patient_sort)") expression_values[":patient_sort"] = patient_sort - if len(categories) == 1: - # Optimisation for single category - category_id = _get_sk_id_for_category(categories[0]) + # Handle multiple categories and pointer types with filter expressions + if len(pointer_types) > 1: + expression_names["#pointer_type"] = "type" + types_filters = [ + f"#pointer_type = :type_{i}" for i in range(len(pointer_types)) + ] + types_filter_values = { + f":type_{i}": pointer_types[i] for i in range(len(pointer_types)) + } + filter_expressions.append(f"({' OR '.join(types_filters)})") + expression_values.update(types_filter_values) + + # if pointer_types then category filter is not needed + if not pointer_types and len(categories) == 1: + category_id = categories[0].replace("|", "-") patient_sort = f"C#{category_id}" key_conditions.append("begins_with(patient_sort, :patient_sort)") expression_values[":patient_sort"] = patient_sort - if len(categories) > 1: + if not pointer_types and len(categories) > 1: expression_names["#category"] = "category" category_filters = [ f"#category = :category_{i}" for i in range(len(categories)) @@ -267,18 +270,6 @@ def search( filter_expressions.append(f"({' OR '.join(category_filters)})") expression_values.update(caetgory_filter_values) - # Handle multiple categories and pointer types with filter expressions - if len(pointer_types) > 1: - expression_names["#pointer_type"] = "type" - types_filters = [ - f"#pointer_type = :type_{i}" for i in range(len(pointer_types)) - ] - types_filter_values = { - f":type_{i}": pointer_types[i] for i in range(len(pointer_types)) - } - filter_expressions.append(f"({' OR '.join(types_filters)})") - expression_values.update(types_filter_values) - if custodian: logger.log( LogReference.REPOSITORY016, From 3d043c34ff70ddea92148376dce91a543df63a8c Mon Sep 17 00:00:00 2001 From: eesa456 Date: Wed, 13 Nov 2024 15:12:07 +0000 Subject: [PATCH 10/25] NRL-1053 update performance tests --- tests/performance/constants.js | 7 +++++++ tests/performance/consumer/baseline.js | 11 +++++++++++ tests/performance/consumer/client.js | 21 +++++++++++++++++++++ tests/performance/consumer/soak.js | 11 +++++++++++ tests/performance/consumer/stress.js | 11 +++++++++++ tests/performance/producer/client.js | 1 + 6 files changed, 62 insertions(+) diff --git a/tests/performance/constants.js b/tests/performance/constants.js index 3cd571e76..668cc9fb3 100644 --- a/tests/performance/constants.js +++ b/tests/performance/constants.js @@ -25,3 +25,10 @@ export const POINTER_TYPES = [ "824321000000109", "2181441000000107", ]; +export const CATEGORIES = [ + "734163000", + "1102421000000108", + "823651000000106", + "721981007", + "103693007", +]; diff --git a/tests/performance/consumer/baseline.js b/tests/performance/consumer/baseline.js index cc05b0204..59e021ee9 100644 --- a/tests/performance/consumer/baseline.js +++ b/tests/performance/consumer/baseline.js @@ -41,6 +41,17 @@ export const options = { { target: 5, duration: "1m" }, ], }, + searchDocumentReferenceByCategory: { + exec: "searchDocumentReferenceByCategory", + executor: "ramping-arrival-rate", + startRate: 1, + timeUnit: "1s", + preAllocatedVUs: 5, + stages: [ + { target: 5, duration: "30s" }, + { target: 5, duration: "1m" }, + ], + }, searchPostDocumentReference: { exec: "searchPostDocumentReference", executor: "ramping-arrival-rate", diff --git a/tests/performance/consumer/client.js b/tests/performance/consumer/client.js index 64e22d799..8d09c05e0 100644 --- a/tests/performance/consumer/client.js +++ b/tests/performance/consumer/client.js @@ -82,6 +82,27 @@ export function searchDocumentReference() { checkResponse(res); } +export function searchDocumentReferenceByCategory() { + const nhsNumber = NHS_NUMBERS[Math.floor(Math.random() * NHS_NUMBERS.length)]; + const randomCategory = + CATEGORIES[Math.floor(Math.random() * CATEGORIES.length)]; + + const identifier = encodeURIComponent( + `https://fhir.nhs.uk/Id/nhs-number|${nhsNumber}` + ); + const category = encodeURIComponent( + `http://snomed.info/sct|${randomCategory}` + ); + + const res = http.get( + `https://${__ENV.HOST}/consumer/DocumentReference?subject:identifier=${identifier}&category=${category}`, + { + headers: getHeaders(), + } + ); + checkResponse(res); +} + export function searchPostDocumentReference() { const nhsNumber = NHS_NUMBERS[Math.floor(Math.random() * NHS_NUMBERS.length)]; const pointer_type = diff --git a/tests/performance/consumer/soak.js b/tests/performance/consumer/soak.js index fad1af82d..d05965b6e 100644 --- a/tests/performance/consumer/soak.js +++ b/tests/performance/consumer/soak.js @@ -44,6 +44,17 @@ export const options = { { target: 0, duration: "1m" }, ], }, + searchDocumentReferenceByCategory: { + exec: "searchDocumentReferenceByCategory", + executor: "ramping-arrival-rate", + startRate: 1, + timeUnit: "1s", + preAllocatedVUs: 5, + stages: [ + { target: 5, duration: "30s" }, + { target: 5, duration: "1m" }, + ], + }, searchPostDocumentReference: { exec: "searchPostDocumentReference", executor: "ramping-arrival-rate", diff --git a/tests/performance/consumer/stress.js b/tests/performance/consumer/stress.js index 5df6e69e4..02d4984a8 100644 --- a/tests/performance/consumer/stress.js +++ b/tests/performance/consumer/stress.js @@ -35,6 +35,17 @@ export const options = { { target: 10, duration: "1m" }, ], }, + searchDocumentReferenceByCategory: { + exec: "searchDocumentReferenceByCategory", + executor: "ramping-arrival-rate", + startRate: 1, + timeUnit: "1s", + preAllocatedVUs: 5, + stages: [ + { target: 5, duration: "30s" }, + { target: 5, duration: "1m" }, + ], + }, searchPostDocumentReference: { exec: "searchPostDocumentReference", executor: "ramping-vus", diff --git a/tests/performance/producer/client.js b/tests/performance/producer/client.js index eddb53f07..c9ba7a754 100644 --- a/tests/performance/producer/client.js +++ b/tests/performance/producer/client.js @@ -6,6 +6,7 @@ import { POINTER_IDS, POINTER_DOCUMENTS, POINTERS_TO_DELETE, + CATEGORIES, } from "../constants.js"; import { check } from "k6"; import { randomItem } from "https://jslib.k6.io/k6-utils/1.2.0/index.js"; From 969ff8f747e0eb4686f2244654d4a85000d648d1 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Wed, 13 Nov 2024 15:28:12 +0000 Subject: [PATCH 11/25] NRL-1053 add feature tests --- .../searchDocumentReference-failure.feature | 28 ++++++++ .../searchDocumentReference-success.feature | 72 +++++++++++++++++++ ...earchPostDocumentReference-failure.feature | 29 ++++++++ ...earchPostDocumentReference-success.feature | 71 ++++++++++++++++++ 4 files changed, 200 insertions(+) diff --git a/tests/features/consumer/searchDocumentReference-failure.feature b/tests/features/consumer/searchDocumentReference-failure.feature index 9865bc226..5b4d2d1ce 100644 --- a/tests/features/consumer/searchDocumentReference-failure.feature +++ b/tests/features/consumer/searchDocumentReference-failure.feature @@ -191,3 +191,31 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios "diagnostics": "Your organisation 'RX898' does not have permission to access this resource. Contact the onboarding team." } """ + + Scenario: Search rejects request with type system they are not allowed to use + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'RX898' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When consumer 'RX898' searches for DocumentReferences with parameters: + | parameter | value | + | subject | 9278693472 | + | category | http://incorrect.info/sct\|736253002 | + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "code-invalid", + "details": { + "coding": [{ + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_CODE_SYSTEM", + "display": "Invalid code system" + }] + }, + "diagnostics": "Invalid query parameter (The provided category is not valid)", + "expression": ["type"] + } + """ diff --git a/tests/features/consumer/searchDocumentReference-success.feature b/tests/features/consumer/searchDocumentReference-success.feature index 287e91d20..f1530c1e5 100644 --- a/tests/features/consumer/searchDocumentReference-success.feature +++ b/tests/features/consumer/searchDocumentReference-success.feature @@ -271,6 +271,78 @@ Feature: Consumer - searchDocumentReference - Success Scenarios And the Bundle has a total of 0 And the Bundle has 0 entry + Scenario: Search for multiple DocumentReferences by NHS number and Category + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'RX898' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + | http://snomed.info/sct | 1363501000000100 | + And a DocumentReference resource exists with values: + | property | value | + | id | 02V-1111111111-SearchMultipleRefTest1 | + | subject | 9278693472 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc-1.pdf | + | custodian | 02V | + | author | 02V | + And a DocumentReference resource exists with values: + | property | value | + | id | 02V-1111111111-SearchMultipleRefTest2 | + | subject | 9278693472 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc-2.pdf | + | custodian | 02V | + | author | 02V | + And a DocumentReference resource exists with values: + | property | value | + | id | 02V-1111111111-SearchMultipleRefTest3 | + | subject | 9278693472 | + | status | current | + | type | 1363501000000100 | + | category | 1102421000000108 | + | contentType | application/pdf | + | url | https://example.org/my-doc-3.pdf | + | custodian | 02V | + | author | 02V | + When consumer 'RX898' searches for DocumentReferences with parameters: + | parameter | value | + | subject | 9278693472 | + | category | 734163000 | + Then the response status code is 200 + And the response is a searchset Bundle + And the Bundle has a self link matching 'DocumentReference?subject:identifier=https://fhir.nhs.uk/Id/nhs-number|9278693472&category=http://snomed.info/sct|734163000' + And the Bundle has a total of 2 + And the Bundle has 2 entries + And the Bundle contains an DocumentReference with values + | property | value | + | id | 02V-1111111111-SearchMultipleRefTest1 | + | subject | 9278693472 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc-1.pdf | + | custodian | 02V | + | author | 02V | + And the Bundle contains an DocumentReference with values + | property | value | + | id | 02V-1111111111-SearchMultipleRefTest2 | + | subject | 9278693472 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc-2.pdf | + | custodian | 02V | + | author | 02V | + And the Bundle does not contain a DocumentReference with ID '02V-1111111111-SearchMultipleRefTest3' + # No pointers found - done # Pointers exist but no permissions - covered in failure scenarios # Search by custodian - done diff --git a/tests/features/consumer/searchPostDocumentReference-failure.feature b/tests/features/consumer/searchPostDocumentReference-failure.feature index e4754e67c..d3d60b1fc 100644 --- a/tests/features/consumer/searchPostDocumentReference-failure.feature +++ b/tests/features/consumer/searchPostDocumentReference-failure.feature @@ -191,3 +191,32 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios "diagnostics": "Your organisation 'RX898' does not have permission to access this resource. Contact the onboarding team." } """ + + Scenario: Search for multiple DocumentReferences by NHS number and an invalid Category + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'RX898' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + | http://snomed.info/sct | 1363501000000100 | + When consumer 'RX898' searches for DocumentReferences with parameters: + | parameter | value | + | subject | 9278693472 | + | category | invalid | + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "code-invalid", + "details": { + "coding": [{ + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_CODE_SYSTEM", + "display": "Invalid code system" + }] + }, + "diagnostics": "Invalid category (The provided category is not valid)", + "expression": ["category"] + } + """ diff --git a/tests/features/consumer/searchPostDocumentReference-success.feature b/tests/features/consumer/searchPostDocumentReference-success.feature index 529f27df5..dfbbc9022 100644 --- a/tests/features/consumer/searchPostDocumentReference-success.feature +++ b/tests/features/consumer/searchPostDocumentReference-success.feature @@ -242,3 +242,74 @@ Feature: Consumer - searchDocumentReference - Success Scenarios And the Bundle has a self link matching 'DocumentReference?subject:identifier=https://fhir.nhs.uk/Id/nhs-number|9278693472&custodian:identifier=https://fhir.nhs.uk/Id/ods-organization-code|RX898' And the Bundle has a total of 0 And the Bundle has 0 entry + + Scenario: Search for multiple DocumentReferences by NHS number and Category + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'RX898' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + | http://snomed.info/sct | 1363501000000100 | + And a DocumentReference resource exists with values: + | property | value | + | id | X26-1111111111-SearchMultipleRefTest1 | + | subject | 9278693472 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc-1.pdf | + | custodian | X26 | + | author | X26 | + And a DocumentReference resource exists with values: + | property | value | + | id | X26-1111111111-SearchMultipleRefTest2 | + | subject | 9278693472 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc-2.pdf | + | custodian | X26 | + | author | X26 | + And a DocumentReference resource exists with values: + | property | value | + | id | x26-1111111111-SearchMultipleRefTest3 | + | subject | 9278693472 | + | status | current | + | type | 1363501000000100 | + | category | 1102421000000108 | + | contentType | application/pdf | + | url | https://example.org/my-doc-3.pdf | + | custodian | x26 | + | author | x26 | + When consumer 'RX898' searches for DocumentReferences using POST with request body: + | key | value | + | subject | 9278693472 | + | category | 734163000 | + Then the response status code is 200 + And the response is a searchset Bundle + And the Bundle has a total of 2 + And the Bundle has 2 entries + And the Bundle contains an DocumentReference with values + | property | value | + | id | X26-1111111111-SearchMultipleRefTest1 | + | subject | 9278693472 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc-1.pdf | + | custodian | X26 | + | author | X26 | + And the Bundle contains an DocumentReference with values + | property | value | + | id | X26-1111111111-SearchMultipleRefTest2 | + | subject | 9278693472 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc-2.pdf | + | custodian | X26 | + | author | X26 | + And the Bundle does not contain a DocumentReference with ID 'x26-1111111111-SearchMultipleRefTest3' From d29c6cae0a7ee13d4150d43bda2ac94d6a02d8e8 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Wed, 13 Nov 2024 15:44:57 +0000 Subject: [PATCH 12/25] NRL-1053 fix tests by adding system in --- .../consumer/searchDocumentReference-failure.feature | 2 +- .../consumer/searchDocumentReference-success.feature | 6 +++--- .../consumer/searchPostDocumentReference-failure.feature | 6 +++--- .../consumer/searchPostDocumentReference-success.feature | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/features/consumer/searchDocumentReference-failure.feature b/tests/features/consumer/searchDocumentReference-failure.feature index 5b4d2d1ce..2f559926d 100644 --- a/tests/features/consumer/searchDocumentReference-failure.feature +++ b/tests/features/consumer/searchDocumentReference-failure.feature @@ -216,6 +216,6 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios }] }, "diagnostics": "Invalid query parameter (The provided category is not valid)", - "expression": ["type"] + "expression": ["category"] } """ diff --git a/tests/features/consumer/searchDocumentReference-success.feature b/tests/features/consumer/searchDocumentReference-success.feature index f1530c1e5..960dfb823 100644 --- a/tests/features/consumer/searchDocumentReference-success.feature +++ b/tests/features/consumer/searchDocumentReference-success.feature @@ -311,9 +311,9 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | custodian | 02V | | author | 02V | When consumer 'RX898' searches for DocumentReferences with parameters: - | parameter | value | - | subject | 9278693472 | - | category | 734163000 | + | parameter | value | + | subject | 9278693472 | + | category | http://snomed.info/sct\|734163000 | Then the response status code is 200 And the response is a searchset Bundle And the Bundle has a self link matching 'DocumentReference?subject:identifier=https://fhir.nhs.uk/Id/nhs-number|9278693472&category=http://snomed.info/sct|734163000' diff --git a/tests/features/consumer/searchPostDocumentReference-failure.feature b/tests/features/consumer/searchPostDocumentReference-failure.feature index d3d60b1fc..e89ee3ec3 100644 --- a/tests/features/consumer/searchPostDocumentReference-failure.feature +++ b/tests/features/consumer/searchPostDocumentReference-failure.feature @@ -199,9 +199,9 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios | http://snomed.info/sct | 736253002 | | http://snomed.info/sct | 1363501000000100 | When consumer 'RX898' searches for DocumentReferences with parameters: - | parameter | value | - | subject | 9278693472 | - | category | invalid | + | parameter | value | + | subject | 9278693472 | + | category | http://incorrect.info/sct\|736253002 | Then the response status code is 400 And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: diff --git a/tests/features/consumer/searchPostDocumentReference-success.feature b/tests/features/consumer/searchPostDocumentReference-success.feature index dfbbc9022..27d3fff18 100644 --- a/tests/features/consumer/searchPostDocumentReference-success.feature +++ b/tests/features/consumer/searchPostDocumentReference-success.feature @@ -283,9 +283,9 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | custodian | x26 | | author | x26 | When consumer 'RX898' searches for DocumentReferences using POST with request body: - | key | value | - | subject | 9278693472 | - | category | 734163000 | + | key | value | + | subject | 9278693472 | + | category | http://snomed.info/sct\|734163000 | Then the response status code is 200 And the response is a searchset Bundle And the Bundle has a total of 2 From 311e5226660661726b74fec83ee5932322350ac6 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Wed, 13 Nov 2024 16:01:48 +0000 Subject: [PATCH 13/25] NRL-1053 fix category filter if no type filter --- layer/nrlf/core/dynamodb/repository.py | 29 +++++++++++++------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/layer/nrlf/core/dynamodb/repository.py b/layer/nrlf/core/dynamodb/repository.py index 7df5776db..71ff03bd8 100644 --- a/layer/nrlf/core/dynamodb/repository.py +++ b/layer/nrlf/core/dynamodb/repository.py @@ -240,26 +240,13 @@ def search( key_conditions.append("begins_with(patient_sort, :patient_sort)") expression_values[":patient_sort"] = patient_sort - # Handle multiple categories and pointer types with filter expressions - if len(pointer_types) > 1: - expression_names["#pointer_type"] = "type" - types_filters = [ - f"#pointer_type = :type_{i}" for i in range(len(pointer_types)) - ] - types_filter_values = { - f":type_{i}": pointer_types[i] for i in range(len(pointer_types)) - } - filter_expressions.append(f"({' OR '.join(types_filters)})") - expression_values.update(types_filter_values) - - # if pointer_types then category filter is not needed - if not pointer_types and len(categories) == 1: + if len(categories) == 1: category_id = categories[0].replace("|", "-") patient_sort = f"C#{category_id}" key_conditions.append("begins_with(patient_sort, :patient_sort)") expression_values[":patient_sort"] = patient_sort - if not pointer_types and len(categories) > 1: + if len(categories) > 1: expression_names["#category"] = "category" category_filters = [ f"#category = :category_{i}" for i in range(len(categories)) @@ -270,6 +257,18 @@ def search( filter_expressions.append(f"({' OR '.join(category_filters)})") expression_values.update(caetgory_filter_values) + # Handle multiple types if no category filter + if not categories and len(pointer_types) > 1: + expression_names["#pointer_type"] = "type" + types_filters = [ + f"#pointer_type = :type_{i}" for i in range(len(pointer_types)) + ] + types_filter_values = { + f":type_{i}": pointer_types[i] for i in range(len(pointer_types)) + } + filter_expressions.append(f"({' OR '.join(types_filters)})") + expression_values.update(types_filter_values) + if custodian: logger.log( LogReference.REPOSITORY016, From 7d37727d8ff4b6a4b3e573df18a9c8f96053c5a4 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Wed, 13 Nov 2024 16:06:39 +0000 Subject: [PATCH 14/25] NRL-1053 fix failure test --- .../searchPostDocumentReference-failure.feature | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/features/consumer/searchPostDocumentReference-failure.feature b/tests/features/consumer/searchPostDocumentReference-failure.feature index e89ee3ec3..30576334b 100644 --- a/tests/features/consumer/searchPostDocumentReference-failure.feature +++ b/tests/features/consumer/searchPostDocumentReference-failure.feature @@ -194,14 +194,14 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios Scenario: Search for multiple DocumentReferences by NHS number and an invalid Category Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API - And the organisation 'RX898' is authorised to access pointer types: + When consumer 'RX898' searches for DocumentReferences using POST with request body: | system | value | | http://snomed.info/sct | 736253002 | | http://snomed.info/sct | 1363501000000100 | - When consumer 'RX898' searches for DocumentReferences with parameters: - | parameter | value | - | subject | 9278693472 | - | category | http://incorrect.info/sct\|736253002 | + When consumer 'RX898' searches for DocumentReferences with body: + | key | value | + | subject | 9278693472 | + | category | http://incorrect.info/sct\|736253002 | Then the response status code is 400 And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: From 051fb834ac4df05660de49ff434facd004ab8326 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Wed, 13 Nov 2024 16:16:41 +0000 Subject: [PATCH 15/25] NRL-1053 fix category expression value --- layer/nrlf/core/dynamodb/repository.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/layer/nrlf/core/dynamodb/repository.py b/layer/nrlf/core/dynamodb/repository.py index 71ff03bd8..5e0621f8b 100644 --- a/layer/nrlf/core/dynamodb/repository.py +++ b/layer/nrlf/core/dynamodb/repository.py @@ -241,7 +241,8 @@ def search( expression_values[":patient_sort"] = patient_sort if len(categories) == 1: - category_id = categories[0].replace("|", "-") + split_category = categories[0].split("|") + category_id = SYSTEM_SHORT_IDS[split_category[0]] + "-" + split_category[1] patient_sort = f"C#{category_id}" key_conditions.append("begins_with(patient_sort, :patient_sort)") expression_values[":patient_sort"] = patient_sort From 5fbd07024ad5cee1a6d8eba4acc53f5ba173ad26 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Wed, 13 Nov 2024 16:29:39 +0000 Subject: [PATCH 16/25] NRL-1053 fix test steps --- .../consumer/searchPostDocumentReference-failure.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/features/consumer/searchPostDocumentReference-failure.feature b/tests/features/consumer/searchPostDocumentReference-failure.feature index 30576334b..5100573e3 100644 --- a/tests/features/consumer/searchPostDocumentReference-failure.feature +++ b/tests/features/consumer/searchPostDocumentReference-failure.feature @@ -194,11 +194,11 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios Scenario: Search for multiple DocumentReferences by NHS number and an invalid Category Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API - When consumer 'RX898' searches for DocumentReferences using POST with request body: + And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | | http://snomed.info/sct | 1363501000000100 | - When consumer 'RX898' searches for DocumentReferences with body: + When consumer 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | | category | http://incorrect.info/sct\|736253002 | From 0dced150a89be2f25cc65de50cd59aeecfa1adec Mon Sep 17 00:00:00 2001 From: eesa456 Date: Wed, 13 Nov 2024 16:40:03 +0000 Subject: [PATCH 17/25] NRL-1053 fix message for test --- .../consumer/searchPostDocumentReference-failure.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/features/consumer/searchPostDocumentReference-failure.feature b/tests/features/consumer/searchPostDocumentReference-failure.feature index 5100573e3..ad0846310 100644 --- a/tests/features/consumer/searchPostDocumentReference-failure.feature +++ b/tests/features/consumer/searchPostDocumentReference-failure.feature @@ -216,7 +216,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios "display": "Invalid code system" }] }, - "diagnostics": "Invalid category (The provided category is not valid)", + "diagnostics": "The provided category is not valid", "expression": ["category"] } """ From 912137f1e24ddd6882697120079faa4bc8671978 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Wed, 13 Nov 2024 17:16:23 +0000 Subject: [PATCH 18/25] NRL-1053 fix repository and perf tests --- layer/nrlf/core/dynamodb/repository.py | 41 +++++++++++++------------- tests/performance/consumer/client.js | 1 + tests/performance/producer/client.js | 1 - 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/layer/nrlf/core/dynamodb/repository.py b/layer/nrlf/core/dynamodb/repository.py index 5e0621f8b..88c82c325 100644 --- a/layer/nrlf/core/dynamodb/repository.py +++ b/layer/nrlf/core/dynamodb/repository.py @@ -239,27 +239,28 @@ def search( patient_sort = f"C#{category_id}#T#{type_id}" key_conditions.append("begins_with(patient_sort, :patient_sort)") expression_values[":patient_sort"] = patient_sort + else: + # Handle single/multiple categories and pointer types with filter expressions + if len(categories) == 1: + split_category = categories[0].split("|") + category_id = ( + SYSTEM_SHORT_IDS[split_category[0]] + "-" + split_category[1] + ) + patient_sort = f"C#{category_id}" + key_conditions.append("begins_with(patient_sort, :patient_sort)") + expression_values[":patient_sort"] = patient_sort + + if len(categories) > 1: + expression_names["#category"] = "category" + category_filters = [ + f"#category = :category_{i}" for i in range(len(categories)) + ] + caetgory_filter_values = { + f":category_{i}": categories[i] for i in range(len(categories)) + } + filter_expressions.append(f"({' OR '.join(category_filters)})") + expression_values.update(caetgory_filter_values) - if len(categories) == 1: - split_category = categories[0].split("|") - category_id = SYSTEM_SHORT_IDS[split_category[0]] + "-" + split_category[1] - patient_sort = f"C#{category_id}" - key_conditions.append("begins_with(patient_sort, :patient_sort)") - expression_values[":patient_sort"] = patient_sort - - if len(categories) > 1: - expression_names["#category"] = "category" - category_filters = [ - f"#category = :category_{i}" for i in range(len(categories)) - ] - caetgory_filter_values = { - f":category_{i}": categories[i] for i in range(len(categories)) - } - filter_expressions.append(f"({' OR '.join(category_filters)})") - expression_values.update(caetgory_filter_values) - - # Handle multiple types if no category filter - if not categories and len(pointer_types) > 1: expression_names["#pointer_type"] = "type" types_filters = [ f"#pointer_type = :type_{i}" for i in range(len(pointer_types)) diff --git a/tests/performance/consumer/client.js b/tests/performance/consumer/client.js index 8d09c05e0..e7f29ce09 100644 --- a/tests/performance/consumer/client.js +++ b/tests/performance/consumer/client.js @@ -3,6 +3,7 @@ import { POINTER_IDS, POINTER_TYPES, ODS_CODE, + CATEGORIES, } from "../constants.js"; import http from "k6/http"; import { check } from "k6"; diff --git a/tests/performance/producer/client.js b/tests/performance/producer/client.js index c9ba7a754..eddb53f07 100644 --- a/tests/performance/producer/client.js +++ b/tests/performance/producer/client.js @@ -6,7 +6,6 @@ import { POINTER_IDS, POINTER_DOCUMENTS, POINTERS_TO_DELETE, - CATEGORIES, } from "../constants.js"; import { check } from "k6"; import { randomItem } from "https://jslib.k6.io/k6-utils/1.2.0/index.js"; From f6061796deb4eca3fdc81dc2658815e45779fc20 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Wed, 13 Nov 2024 18:04:21 +0000 Subject: [PATCH 19/25] NRL-1053 fix performance tests --- tests/performance/consumer/baseline.js | 11 +++++++++++ tests/performance/consumer/client.js | 19 +++++++++++++++++++ tests/performance/consumer/soak.js | 19 ++++++++++++++++--- tests/performance/consumer/stress.js | 19 +++++++++++++------ tests/performance/process_results.py | 4 ++-- 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/tests/performance/consumer/baseline.js b/tests/performance/consumer/baseline.js index 59e021ee9..ffa06c58a 100644 --- a/tests/performance/consumer/baseline.js +++ b/tests/performance/consumer/baseline.js @@ -63,5 +63,16 @@ export const options = { { target: 5, duration: "1m" }, ], }, + searchPostDocumentReferenceByCategory: { + exec: "searchPostDocumentReferenceByCategory", + executor: "ramping-arrival-rate", + startRate: 1, + timeUnit: "1s", + preAllocatedVUs: 5, + stages: [ + { target: 5, duration: "30s" }, + { target: 5, duration: "1m" }, + ], + }, }, }; diff --git a/tests/performance/consumer/client.js b/tests/performance/consumer/client.js index e7f29ce09..7f5ac6031 100644 --- a/tests/performance/consumer/client.js +++ b/tests/performance/consumer/client.js @@ -123,3 +123,22 @@ export function searchPostDocumentReference() { ); checkResponse(res); } + +export function searchPostDocumentReferenceByCategory() { + const nhsNumber = NHS_NUMBERS[Math.floor(Math.random() * NHS_NUMBERS.length)]; + const category = CATEGORIES[Math.floor(Math.random() * CATEGORIES.length)]; + + const body = JSON.stringify({ + "subject:identifier": `https://fhir.nhs.uk/Id/nhs-number|${nhsNumber}`, + category: `http://snomed.info/sct|${category}`, + }); + + const res = http.post( + `https://${__ENV.HOST}/consumer/DocumentReference/_search`, + body, + { + headers: getHeaders(), + } + ); + checkResponse(res); +} diff --git a/tests/performance/consumer/soak.js b/tests/performance/consumer/soak.js index d05965b6e..34495d153 100644 --- a/tests/performance/consumer/soak.js +++ b/tests/performance/consumer/soak.js @@ -47,12 +47,13 @@ export const options = { searchDocumentReferenceByCategory: { exec: "searchDocumentReferenceByCategory", executor: "ramping-arrival-rate", - startRate: 1, + startRate: 0, timeUnit: "1s", preAllocatedVUs: 5, stages: [ - { target: 5, duration: "30s" }, - { target: 5, duration: "1m" }, + { target: 10, duration: "5m" }, + { target: 10, duration: "30m" }, + { target: 0, duration: "1m" }, ], }, searchPostDocumentReference: { @@ -67,5 +68,17 @@ export const options = { { target: 0, duration: "1m" }, ], }, + searchPostDocumentReferenceByCategory: { + exec: "searchPostDocumentReferenceByCategory", + executor: "ramping-arrival-rate", + startRate: 0, + timeUnit: "1s", + preAllocatedVUs: 5, + stages: [ + { target: 10, duration: "5m" }, + { target: 10, duration: "30m" }, + { target: 0, duration: "1m" }, + ], + }, }, }; diff --git a/tests/performance/consumer/stress.js b/tests/performance/consumer/stress.js index 02d4984a8..1ee5a7df0 100644 --- a/tests/performance/consumer/stress.js +++ b/tests/performance/consumer/stress.js @@ -37,13 +37,11 @@ export const options = { }, searchDocumentReferenceByCategory: { exec: "searchDocumentReferenceByCategory", - executor: "ramping-arrival-rate", - startRate: 1, - timeUnit: "1s", - preAllocatedVUs: 5, + executor: "ramping-vus", + startVUs: 1, stages: [ - { target: 5, duration: "30s" }, - { target: 5, duration: "1m" }, + { target: 10, duration: "30s" }, + { target: 10, duration: "1m" }, ], }, searchPostDocumentReference: { @@ -55,5 +53,14 @@ export const options = { { target: 10, duration: "1m" }, ], }, + searchPostDocumentReferenceByCategory: { + exec: "searchPostDocumentReferenceByCategory", + executor: "ramping-vus", + startVUs: 1, + stages: [ + { target: 10, duration: "30s" }, + { target: 10, duration: "1m" }, + ], + }, }, }; diff --git a/tests/performance/process_results.py b/tests/performance/process_results.py index e8a1a8d08..440f65da6 100644 --- a/tests/performance/process_results.py +++ b/tests/performance/process_results.py @@ -80,7 +80,7 @@ def _create_response_count_figure(data: dict, title: str): fig_labels.add(f"Failure - Status Code {failure.status}") for index, scenario in enumerate(data.keys()): - axes = fig.add_subplot(2, 2, index + 1) + axes = fig.add_subplot(3, 3, index + 1) scenario_data = data[scenario] timestamps = [] @@ -149,7 +149,7 @@ def _create_response_time_figure(data: dict, title: str): fig.suptitle(title) for index, scenario in enumerate(data.keys()): - axes = fig.add_subplot(2, 2, index + 1) + axes = fig.add_subplot(3, 3, index + 1) scenario_data = data[scenario] timestamps = [] From 68dcb4546093bfad8856ebf5f6c4ae28abb4e3e3 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Thu, 14 Nov 2024 12:08:34 +0000 Subject: [PATCH 20/25] NRL-1053 PR comments --- .../search_document_reference.py | 2 +- api/consumer/swagger.yaml | 2 +- .../tests/test_search_document_reference_producer.py | 2 +- api/producer/swagger.yaml | 2 +- layer/nrlf/core/dynamodb/repository.py | 4 ++-- layer/nrlf/core/validators.py | 12 +----------- swagger/consumer-static/components.yaml | 2 +- .../consumer/searchDocumentReference-failure.feature | 2 +- .../searchPostDocumentReference-failure.feature | 2 +- 9 files changed, 10 insertions(+), 20 deletions(-) diff --git a/api/consumer/searchDocumentReference/search_document_reference.py b/api/consumer/searchDocumentReference/search_document_reference.py index 397ea0580..b420c7132 100644 --- a/api/consumer/searchDocumentReference/search_document_reference.py +++ b/api/consumer/searchDocumentReference/search_document_reference.py @@ -61,7 +61,7 @@ def handler( if not validate_category(params.category): logger.log( LogReference.CONSEARCH002b, - type=params.category, + category=params.category, ) # TODO - Should update error message once permissioning by category is implemented return SpineErrorResponse.INVALID_CODE_SYSTEM( diagnostics="Invalid query parameter (The provided category is not valid)", diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index 69e5d1c36..6356607b3 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1559,7 +1559,7 @@ components: summary: Diagnostic studies report value: http://snomed.info/sct|721981007 SNOMED_CODES_DIAGNOSTIC_PROCEDURE: - summary: RDiagnostic procedure + summary: Diagnostic procedure value: http://snomed.info/sct|103693007 invalid: summary: Unknown diff --git a/api/producer/searchDocumentReference/tests/test_search_document_reference_producer.py b/api/producer/searchDocumentReference/tests/test_search_document_reference_producer.py index dc68d6e30..7f3b89165 100644 --- a/api/producer/searchDocumentReference/tests/test_search_document_reference_producer.py +++ b/api/producer/searchDocumentReference/tests/test_search_document_reference_producer.py @@ -319,7 +319,7 @@ def test_search_document_reference_filters_by_type( @mock_aws @mock_repository -def test_search_document_reference_filters_by_type( +def test_search_document_reference_filters_by_category( repository: DocumentPointerRepository, ): doc_ref = load_document_reference("Y05868-736253002-Valid") diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index 81fcd49f3..b3316d199 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -2066,7 +2066,7 @@ components: summary: Diagnostic studies report value: http://snomed.info/sct|721981007 SNOMED_CODES_DIAGNOSTIC_PROCEDURE: - summary: RDiagnostic procedure + summary: Diagnostic procedure value: http://snomed.info/sct|103693007 invalid: summary: Unknown diff --git a/layer/nrlf/core/dynamodb/repository.py b/layer/nrlf/core/dynamodb/repository.py index 88c82c325..a65931ccb 100644 --- a/layer/nrlf/core/dynamodb/repository.py +++ b/layer/nrlf/core/dynamodb/repository.py @@ -255,11 +255,11 @@ def search( category_filters = [ f"#category = :category_{i}" for i in range(len(categories)) ] - caetgory_filter_values = { + category_filter_values = { f":category_{i}": categories[i] for i in range(len(categories)) } filter_expressions.append(f"({' OR '.join(category_filters)})") - expression_values.update(caetgory_filter_values) + expression_values.update(category_filter_values) expression_names["#pointer_type"] = "type" types_filters = [ diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index df5791ed7..45627ccc7 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -43,17 +43,7 @@ def validate_category(category_: Optional[RequestQueryCategory]) -> bool: if not category_: return True - category_system = category_.root.split("|", 1)[0] - category_code = category_.root.split("|", 1)[1] - - category_list = Categories.list() - category_systems = [category.split("|", 1)[0] for category in category_list] - category_codes = [category.split("|", 1)[1] for category in category_list] - - if not category_system in category_systems: - return False - - return category_code in category_codes + return category_.root in Categories.list() @dataclass diff --git a/swagger/consumer-static/components.yaml b/swagger/consumer-static/components.yaml index c5f07a505..be736a02d 100644 --- a/swagger/consumer-static/components.yaml +++ b/swagger/consumer-static/components.yaml @@ -37,7 +37,7 @@ components: schema: $ref: "#/components/schemas/RequestQueryType" category: - name: type + name: category in: query schema: $ref: "#/components/schemas/RequestQueryCategory" diff --git a/tests/features/consumer/searchDocumentReference-failure.feature b/tests/features/consumer/searchDocumentReference-failure.feature index 2f559926d..8ed4300e6 100644 --- a/tests/features/consumer/searchDocumentReference-failure.feature +++ b/tests/features/consumer/searchDocumentReference-failure.feature @@ -192,7 +192,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios } """ - Scenario: Search rejects request with type system they are not allowed to use + Scenario: Search rejects request with invalid category system Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API And the organisation 'RX898' is authorised to access pointer types: | system | value | diff --git a/tests/features/consumer/searchPostDocumentReference-failure.feature b/tests/features/consumer/searchPostDocumentReference-failure.feature index ad0846310..8f5b507bd 100644 --- a/tests/features/consumer/searchPostDocumentReference-failure.feature +++ b/tests/features/consumer/searchPostDocumentReference-failure.feature @@ -192,7 +192,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios } """ - Scenario: Search for multiple DocumentReferences by NHS number and an invalid Category + Scenario: Search rejects request with category system they are not allowed to use Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API And the organisation 'RX898' is authorised to access pointer types: | system | value | From f813b47e6b22db3bd9ca7c672cba107d6702cce4 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Mon, 18 Nov 2024 10:38:48 +0000 Subject: [PATCH 21/25] NRL-1053 do not allow invalid/extra params for consumer search --- layer/nrlf/core/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layer/nrlf/core/model.py b/layer/nrlf/core/model.py index 85c6b4f1f..05b3e5245 100644 --- a/layer/nrlf/core/model.py +++ b/layer/nrlf/core/model.py @@ -22,7 +22,7 @@ def nhs_number(self) -> Union[str, None]: class ProducerRequestParams(producer_model.RequestParams, _NhsNumberMixin): - pass + model_config = {"extra": "forbid"} class ConsumerRequestParams(consumer_model.RequestParams, _NhsNumberMixin): From 8e1c1866ab4d61ae9180f0f1ae8f3b1cf8ee0ae2 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Tue, 19 Nov 2024 12:58:59 +0000 Subject: [PATCH 22/25] NRL-1053 update models --- api/consumer/swagger.yaml | 2 ++ api/producer/swagger.yaml | 3 +++ layer/nrlf/consumer/fhir/r4/model.py | 6 +++--- layer/nrlf/producer/fhir/r4/model.py | 10 +++++----- layer/nrlf/producer/fhir/r4/strict_model.py | 10 +++++----- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index 6356607b3..ecb45e0be 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1057,6 +1057,8 @@ components: items: $ref: "#/components/schemas/Reference" description: Related identifiers or resources associated with the DocumentReference. + required: + - practiceSetting DocumentReferenceContent: type: object properties: diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index b3316d199..277fd3e7e 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1385,6 +1385,7 @@ components: - status - content - author + - context Bundle: type: object properties: @@ -1594,6 +1595,8 @@ components: items: $ref: "#/components/schemas/Reference" description: Related identifiers or resources associated with the DocumentReference. + required: + - practiceSetting DocumentReferenceContent: type: object properties: diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 061754db2..83d1af3bd 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-11-10T19:12:31+00:00 +# timestamp: 2024-11-19T12:56:58+00:00 from __future__ import annotations @@ -804,11 +804,11 @@ class DocumentReferenceContext(BaseModel): Field(description="The kind of facility where the patient was seen."), ] = None practiceSetting: Annotated[ - Optional[CodeableConcept], + CodeableConcept, Field( description="This property may convey specifics about the practice setting where the content was created, often reflecting the clinical specialty." ), - ] = None + ] sourcePatientInfo: Annotated[ Optional[Reference], Field( diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index e50694170..d08886362 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-11-10T19:12:27+00:00 +# timestamp: 2024-11-19T12:56:54+00:00 from __future__ import annotations @@ -611,9 +611,9 @@ class DocumentReference(BaseModel): securityLabel: Optional[List[CodeableConcept]] = None content: Annotated[List[DocumentReferenceContent], Field(min_length=1)] context: Annotated[ - Optional[DocumentReferenceContext], + DocumentReferenceContext, Field(description="The clinical context in which the document was prepared."), - ] = None + ] class Bundle(BaseModel): @@ -788,11 +788,11 @@ class DocumentReferenceContext(BaseModel): Field(description="The kind of facility where the patient was seen."), ] = None practiceSetting: Annotated[ - Optional[CodeableConcept], + CodeableConcept, Field( description="This property may convey specifics about the practice setting where the content was created, often reflecting the clinical specialty." ), - ] = None + ] sourcePatientInfo: Annotated[ Optional[Reference], Field( diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index fb9a4cc66..d99a82143 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-11-10T19:12:29+00:00 +# timestamp: 2024-11-19T12:56:56+00:00 from __future__ import annotations @@ -531,9 +531,9 @@ class DocumentReference(BaseModel): securityLabel: Optional[List[CodeableConcept]] = None content: Annotated[List[DocumentReferenceContent], Field(min_length=1)] context: Annotated[ - Optional[DocumentReferenceContext], + DocumentReferenceContext, Field(description="The clinical context in which the document was prepared."), - ] = None + ] class Bundle(BaseModel): @@ -693,11 +693,11 @@ class DocumentReferenceContext(BaseModel): Field(description="The kind of facility where the patient was seen."), ] = None practiceSetting: Annotated[ - Optional[CodeableConcept], + CodeableConcept, Field( description="This property may convey specifics about the practice setting where the content was created, often reflecting the clinical specialty." ), - ] = None + ] sourcePatientInfo: Annotated[ Optional[Reference], Field( From 236afbb4e2d64061eaf4ef81cefc61485191a1fa Mon Sep 17 00:00:00 2001 From: eesa456 Date: Tue, 19 Nov 2024 15:33:50 +0000 Subject: [PATCH 23/25] NRL-1053 tests --- layer/nrlf/core/tests/test_model.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/layer/nrlf/core/tests/test_model.py b/layer/nrlf/core/tests/test_model.py index c8d1c9eaa..f844175f6 100644 --- a/layer/nrlf/core/tests/test_model.py +++ b/layer/nrlf/core/tests/test_model.py @@ -1,3 +1,6 @@ +import pytest +from pydantic import ValidationError + from nrlf.core.model import ( ConnectionMetadata, ConsumerRequestParams, @@ -91,6 +94,30 @@ def test_consumer_request_params(): assert params.nhs_number == "9999999999" +def test_producer_request_params_extra_fields(): + with pytest.raises(ValidationError): + ProducerRequestParams.model_validate( + { + "subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|9999999999", + "type": "test-type", + "next-page-token": "page-token", + "extra_field": "extra_value", + } + ) + + +def test_consumer_request_params_extra_fields(): + with pytest.raises(ValidationError): + ConsumerRequestParams.model_validate( + { + "subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|9999999999", + "type": "test-type", + "next-page-token": "page-token", + "extra_field": "extra_value", + } + ) + + def test_count_request_params(): params = CountRequestParams.model_validate( { From 02b5ca28f2553f99ea5a462045dba358b34eae8a Mon Sep 17 00:00:00 2001 From: eesa456 Date: Tue, 19 Nov 2024 15:36:14 +0000 Subject: [PATCH 24/25] NRL-1053 remove npt --- layer/nrlf/core/tests/test_model.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/layer/nrlf/core/tests/test_model.py b/layer/nrlf/core/tests/test_model.py index f844175f6..07641989e 100644 --- a/layer/nrlf/core/tests/test_model.py +++ b/layer/nrlf/core/tests/test_model.py @@ -100,7 +100,6 @@ def test_producer_request_params_extra_fields(): { "subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|9999999999", "type": "test-type", - "next-page-token": "page-token", "extra_field": "extra_value", } ) @@ -112,7 +111,6 @@ def test_consumer_request_params_extra_fields(): { "subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|9999999999", "type": "test-type", - "next-page-token": "page-token", "extra_field": "extra_value", } ) From 50b3dca4b4cc2dc103b345ad79330f6528274090 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Tue, 19 Nov 2024 17:32:34 +0000 Subject: [PATCH 25/25] NRL-1053 revert models --- api/consumer/swagger.yaml | 2 -- api/producer/swagger.yaml | 3 --- layer/nrlf/consumer/fhir/r4/model.py | 6 +++--- layer/nrlf/producer/fhir/r4/model.py | 10 +++++----- layer/nrlf/producer/fhir/r4/strict_model.py | 10 +++++----- 5 files changed, 13 insertions(+), 18 deletions(-) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index ecb45e0be..6356607b3 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1057,8 +1057,6 @@ components: items: $ref: "#/components/schemas/Reference" description: Related identifiers or resources associated with the DocumentReference. - required: - - practiceSetting DocumentReferenceContent: type: object properties: diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index 277fd3e7e..b3316d199 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1385,7 +1385,6 @@ components: - status - content - author - - context Bundle: type: object properties: @@ -1595,8 +1594,6 @@ components: items: $ref: "#/components/schemas/Reference" description: Related identifiers or resources associated with the DocumentReference. - required: - - practiceSetting DocumentReferenceContent: type: object properties: diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 83d1af3bd..061754db2 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-11-19T12:56:58+00:00 +# timestamp: 2024-11-10T19:12:31+00:00 from __future__ import annotations @@ -804,11 +804,11 @@ class DocumentReferenceContext(BaseModel): Field(description="The kind of facility where the patient was seen."), ] = None practiceSetting: Annotated[ - CodeableConcept, + Optional[CodeableConcept], Field( description="This property may convey specifics about the practice setting where the content was created, often reflecting the clinical specialty." ), - ] + ] = None sourcePatientInfo: Annotated[ Optional[Reference], Field( diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index d08886362..d8ad9f87f 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-11-19T12:56:54+00:00 +# timestamp: 2024-11-10T19:12:31+00:00 from __future__ import annotations @@ -611,9 +611,9 @@ class DocumentReference(BaseModel): securityLabel: Optional[List[CodeableConcept]] = None content: Annotated[List[DocumentReferenceContent], Field(min_length=1)] context: Annotated[ - DocumentReferenceContext, + Optional[DocumentReferenceContext], Field(description="The clinical context in which the document was prepared."), - ] + ] = None class Bundle(BaseModel): @@ -788,11 +788,11 @@ class DocumentReferenceContext(BaseModel): Field(description="The kind of facility where the patient was seen."), ] = None practiceSetting: Annotated[ - CodeableConcept, + Optional[CodeableConcept], Field( description="This property may convey specifics about the practice setting where the content was created, often reflecting the clinical specialty." ), - ] + ] = None sourcePatientInfo: Annotated[ Optional[Reference], Field( diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index d99a82143..c74b28ab5 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-11-19T12:56:56+00:00 +# timestamp: 2024-11-10T19:12:31+00:00 from __future__ import annotations @@ -531,9 +531,9 @@ class DocumentReference(BaseModel): securityLabel: Optional[List[CodeableConcept]] = None content: Annotated[List[DocumentReferenceContent], Field(min_length=1)] context: Annotated[ - DocumentReferenceContext, + Optional[DocumentReferenceContext], Field(description="The clinical context in which the document was prepared."), - ] + ] = None class Bundle(BaseModel): @@ -693,11 +693,11 @@ class DocumentReferenceContext(BaseModel): Field(description="The kind of facility where the patient was seen."), ] = None practiceSetting: Annotated[ - CodeableConcept, + Optional[CodeableConcept], Field( description="This property may convey specifics about the practice setting where the content was created, often reflecting the clinical specialty." ), - ] + ] = None sourcePatientInfo: Annotated[ Optional[Reference], Field(