diff --git a/README.md b/README.md index 0bfe1783f..afd9e6979 100644 --- a/README.md +++ b/README.md @@ -1,215 +1,89 @@ # FHIR Converter -FHIR Converter is an open source project that enables conversion of health data from legacy formats to FHIR. +FHIR converter is an open source project that enables conversion of health data from legacy formats to and from FHIR. The FHIR converter uses the [Liquid template language](https://shopify.github.io/liquid/) and the .NET runtime. -The first version of the FHIR Converter released to open source on Mar 6th, 2020. It used Handlebars template language and Javascript runtime. A new converter engine was released on Nov 13, 2020 that uses Liquid templating language and .Net runtime. +The FHIR converter supports the following conversions: **HL7v2 to FHIR**, **C-CDA to FHIR**, **JSON to FHIR**, **FHIR STU3 to R4**, and **FHIR to HL7v2** (*Preview*). -Currently, Microsoft supports two types of converter engines, Handlebars engine and Liquid engine, along with templates and filters. **We strongly recommend using the Liquid converter** for a better alignment with [Azure Healthcare APIs](https://docs.microsoft.com/azure/healthcare-apis/), [Azure Health Data Services](https://azure.microsoft.com/en-us/services/health-data-services/#overview), [FHIR Server for Azure](https://github.com/microsoft/fhir-server), and [Microsoft Logic Apps](https://azure.microsoft.com/services/logic-apps/). +The converter uses templates that define mappings between these different data formats. The templates are written in [Liquid](https://shopify.github.io/liquid/) templating language and make use of custom [filters](docs/Filters-and-Tags.md). -The following table shows the differences between two converter engines: +The converter comes with a few ready-to-use templates. If needed, you can create a new template, or modify existing templates to meet your specific conversion requirements. The provided templates are based off of HL7 v2.8. Other versions may require you to make modifications to these templates on your own. See [Templates & Authoring](#templates--authoring) for specifics. -| | Handlebars engine | Liquid engine | -| ----- | ----- | ----- | -| **Template language** | [Handlebars](https://handlebarsjs.com/) | [Liquid](https://shopify.github.io/liquid/) | -| **Template authoring tool** | Self-hosted web-app | [VS Code extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-health-fhir-converter)| -| **Supported conversions** | 1. HL7v2 to FHIR
2. C-CDA to FHIR | 1. HL7v2 to FHIR
2. C-CDA to FHIR
3. JSON to FHIR
4. FHIR STU3 to FHIR R4 **(new!)** | -| **Available as** | 1. Self-deployed web service
(on-prem or on Azure)| 1. Command line tool
2. $convert-data operation in [FHIR Server for Azure (OSS)](https://github.com/microsoft/fhir-server/blob/main/docs/ConvertDataOperation.md)
3. $convert-data operation in both [Azure Health Data Services](https://docs.microsoft.com/en-us/azure/healthcare-apis/fhir/convert-data) and [Azure API for FHIR](https://docs.microsoft.com/azure/healthcare-apis/azure-api-for-fhir/convert-data)| +## What's New? +The latest iteration of the *Preview* FHIR converter makes some significant changes over [previous versions](#previous-versions). -⚠ Rest of this document is about the Liquid converter. For the Handlebars converter, please refer to the [Handlebars branch](https://github.com/microsoft/FHIR-Converter/tree/handlebars). +Some of the changes include: + * Containerized API + * Support Azure Storage for customer templates. + * Removal of Azure Container repository dependency for custom templates. + * Support for FHIR to HL7v2 conversion. -Currently, FHIR Converter supports four types of conversions, **HL7v2 to FHIR**, **C-CDA to FHIR**, **JSON to FHIR** and **FHIR STU3 to R4**. The converter uses templates that define mappings between these different data formats. The templates are written in [Liquid](https://shopify.github.io/liquid/) templating language and make use of custom [filters](docs/Filters-and-Tags.md). + All the documentation for the new *preview* FHIR converter API can be found in the [How to Guides](docs/how-to-guides/) folder. -The converter comes with a few ready-to-use templates. If needed, you can create a new template, or modify existing templates to meet your specific conversion requirements. The provided templates are based off of HL7 v2.8. Other versions may require you to make modifications to these templates on your own. +## Architecture -FHIR Converter with DotLiquid engine transforms the input data into FHIR bundles that are persisted to a FHIR server. The converter is integrated into both [Azure Health Data Services](https://azure.microsoft.com/en-us/services/health-data-services/#overview) and [Azure API for FHIR](https://docs.microsoft.com/azure/healthcare-apis/azure-api-for-fhir/), as well as in the open-source [FHIR Server](https://github.com/microsoft/fhir-server) as a [`$convert-data`] #using-convert-data-in-fhir-server) operation. It is also available as a [command line tool](#using-command-line-tool). +The FHIR converter API *preview* provides [REST based APIs](#api) to perform conversion requests. -NOTE: **FHIR STU3 to R4** conversion is only available in the open-source [FHIR Server](https://github.com/microsoft/fhir-server) as a [`$convert-data`](#using-convert-data-in-fhir-server) operation. It is also available as a [command line tool](#using-command-line-tool). +The FHIR converter APIs are offered as a container artifact in [Microsoft Container Registry](https://github.com/microsoft/containerregistry). +This image can be downloaded and run as a web service on a container hosting platform in your Azure tenant; that clients can target for conversion requests. -## Using $convert-data in FHIR server +![Convert setup](/docs/images/convert-setup.png) -The `$convert-data` operation is integrated into Azure Health Data Services, Azure API for FHIR and FHIR server to run as part of the service. After enabling `$convert-data` in your server, you can make API calls in the form of ```<>/$convert-data``` to the server to convert your data into FHIR. In the API call request body, you would include parameters such as `inputData`, `inputDataType`, `templateCollectionReference`, and `rootTemplate`, to specify the message and the type of message you are converting. +## Templates & Authoring -For more information on configuring and using `$convert-data` operation on your server, please refer to these documentation: +The FHIR converter API comes with several pre-built templates you can use as reference as to create your own. -- [$convert-data for Azure Health Data Services](https://docs.microsoft.com/en-us/azure/healthcare-apis/fhir/convert-data) -- [$convert-data for Azure API for FHIR](https://docs.microsoft.com/azure/healthcare-apis/azure-api-for-fhir/convert-data) -- [$convert-data for FHIR Server](https://github.com/microsoft/fhir-server/blob/main/docs/ConvertDataOperation.md) +| Conversion | Notes | +| ----- | ----- | +| [HL7v2 to FHIR](/docs/HL7v2-templates.md)| Important points to note for HL7v2 to FHIR conversion: [see here](docs/HL7v2-ImportantPoints.md)
Common FHIR Validator errors/warning you might run into, and their explanations: [see here](docs/HL7v2-FHIRValidator.md) | +| [C-CDA to FHIR](/data/Templates/Ccda/) | | +| [JSON to FHIR](/data/Templates/Json/) | | +| [FHIR STU3 to R4](/data/Templates/Stu3ToR4/) | [Differences between STU3 & R4](/docs/Stu3R4-resources-differences.md) | +| FHIR to HL7v2 (*Preview*) | | -## Using Command line tool +### Concepts -### Supported parameters - -The command line tool is another way of converting data, as well as managing templates. The tool converts a folder containing input messages (HL7v2, C-CDA, JSON or FHIR STU3) into FHIR R4 resources. It accepts the following parameters in the command line: - -| Option | Name | Optionality | Default | Description | -| ----- | ----- | ----- |----- |----- | -| -d | TemplateDirectory | Required | | Root directory of templates. | -| -r | RootTemplate | Required | | Name of root template.

**HL7v2 to FHIR** (57 templates): "ADT_A01", "ADT_A02", "ADT_A03", "ADT_A04", "ADT_A05", "ADT_A06". "ADT_A07", "ADT_A08", "ADT_A09", "ADT_A10", "ADT_A11", "ADT_A13", "ADT_A14", "ADT_A15", "ADT_A16", "ADT_A25", "ADT_A26", "ADT_A27", "ADT_A28", "ADT_A29", "ADT_A31", "ADT_A40", "ADT_A41", "ADT_A45", "ADT_A47", "ADT_A60", "BAR_P01", "BAR_P02", "BAR_B12", "DFT_P03", "DFT_P11", "MDM_T01", "MDM_T02", "MDM_T05", "MDM_T06", "MDM_T09", "MDM_T10", "OMG_O19" "OML_O21", "ORM_O01", "ORU_R01", "OUL_R22", "OUL_R23", "OUL_R24", "RDE_O11", "RDE_O25", "RDS_O13", "REF_I12", "REF_I14", "SIU_S12", "SIU_S13", "SIU_S14", "SIU_S15", "SIU_S16", "SIU_S17", "SIU_S26", "VXU_V04"

**C-CDA to FHIR** (9 templates): "CCD", "ConsultationNote", "DischargeSummary", "HistoryandPhysical", "OperativeNote", "ProcedureNote", "ProgressNote", "ReferralNote", "TransferSummary"

**JSON to FHIR**: "Stu3ChargeItem", "ExamplePatient"
(*These JSON templates are sample templates for use, not default templates that adhere to any pre-defined JSON message types. JSON does not have any standardized message types, unlike HL7v2 messages or C-CDA documents. Therefore, instead of "default" templates we provide you with some sample templates that you can use as a starting guide for you to modify and customize.*)

**FHIR STU3 to R4**: Name of the root template that is the same as the STU3 resource name e.g., "Patient", "Observation", "Organization". Some of the STU3 resources are renamed or removed from R4. Please refer to [Resource differences and constraints for STU3 to R4](docs/Stu3R4-resources-differences.md). | -| -c | InputDataContent | Optional| | Input data content. Specify OutputDataFile to get the results. | -| -n | InputDataFile | Optional| | Input data file. Specify OutputDataFile to get the results. | -| -f | OutputDataFile | Optional | | Output data file. | -| -i | InputDataFolder | Optional | | Input data folder. Specify OutputDataFolder to get the results. | -| -o | OutputDataFolder | Optional | | Output data folder. | -| -t | IsTraceInfo | Optional | | Provide trace information in the output if "-t" is set. **This parameter is only supported in HL7v2 to FHIR conversion.** | -| --version | Version | Optional | | Display version information. | -| --help | Help | Optional | | Display usage information of this tool. | - -### Command line example - -First, build the executable file, `Microsoft.Health.Fhir.Liquid.Converter.Tool.exe`, in your local directory. Have the input data, as well as the conversion templates, saved in separate folders. - -#### Converting data inside folders - -You can run the built .exe file in your terminal by running the following command line and specifying the directories: - -``` ->.\Microsoft.Health.Fhir.Liquid.Converter.Tool.exe convert -d myTemplateDirectory -r myRootTemplate -i myInputDataFolder -o myOutputDataFolder -``` - -For example, you can run a command like below to convert all C-CDA sample documents inside the `SampleData\Ccda` folder using the default `CCD` template: - -``` ->.\Microsoft.Health.Fhir.Liquid.Converter.Tool.exe convert -d {path}\Templates\Ccda -r CCD -i {path}\SampleData\Ccda -o {path to your output folder} -``` - -After running the command line, there will be a series of "Processing..." lines being written in the terminal window. When the conversion is complete, you will see "Conversion completed!" message. - -For example, if you were doing the C-CDA to FHIR conversion in a folder as in the command above, you will see something like this: - -![Conversion running on the terminal (screenshot)](docs/conversion-terminal-screenshot.png) - -#### Converting individual files - -Instead of converting multiples messages and documents inside a folder, you can also convert single files using the parameters in the table above. For example, if you were to convert a C-CDA document into a FHIR resource using the `CCD` template: -``` ->.\Microsoft.Health.Fhir.Liquid.Converter.Tool.exe convert -n {path}\SampleData\Ccda\CCD.ccda -d {path}\Templates\Ccda -f {path to your output json file} -r CCD -``` - -## Using Templates - -The command line tool supports managing different versions of templates from Azure Container Registry (ACR). You can customize templates and store them in your ACR if default templates are not sufficient for meeting conversion requirements. After [ACR authentication](docs/TemplateManagementCLI.md#authentication), you can pull templates from ACR or push templates to ACR using our command line tool. - -> Note: Template version is aligned with the version of FHIR Converter. - -### Command line example - -Example command to push a collection of templates to ACR image from a folder: -``` ->.\Microsoft.Health.Fhir.Liquid.Converter.Tool.exe push testacr.azurecr.io/templatetest:default myInputFolder -``` -Example usage of pulling an image of templates in a folder: -``` ->.\Microsoft.Health.Fhir.Liquid.Converter.Tool.exe pull testacr.azurecr.io/templatetest@sha256:412ea84f1bb1a9d98345efb7b427ba89616ec29ac332d543eff9a2161ca12a58 myOutputFolder -``` - -For more details on how to push and pull template collections, please refer to the documentation on [Template Management CLI tool](docs/TemplateManagementCLI.md). - -To see the current version of templates we support, check out the complete list of [templates](data/Templates). - -There are other versions released by Microsoft that are stored in a public ACR `healthplatformregistry.azurecr.io`. You can directly pull templates from ``` healthplatformregistry.azurecr.io/hl7v2defaulttemplates: ``` without ACR authentication. - -### HL7v2 to FHIR conversion templates - -There are three documentations to note for HL7v2 to FHIR conversion. Please make sure to reference these as you use our HL7v2 default templates: - -* **A complete list and explanation of each of the 57 HL7v2 to FHIR conversion templates:** [see here](docs/HL7v2-templates.md) -* **Important points to note for HL7v2 to FHIR conversion:** [see here](docs/HL7v2-ImportantPoints.md) -* **Common FHIR Validator errors/warning you might run into, and their explanations:** [see here](docs/HL7v2-FHIRValidator.md) - -## Resource ID generation - -The default templates provided with the Converter computes Resource IDs using the input data fields. In order to preserve the generated Resource IDs, the converter creates **PUT requests**, instead of POST requests in the generated bundles. - -For **HL7v2 to FHIR conversion**, [HL7v2 DotLiquid templates](data/Templates/Hl7v2/ID) help generate FHIR resource IDs from HL7v2 messages. An ID generation template does three things: 1) extract identifiers from the input segment or field; 2) combine the identifers with resource type and base ID (optional) as hash seed; 3) compute hash as output ID. - -For **C-CDA to FHIR conversion**, [C-CDA DotLiquid templates](data/Templates/Ccda/Utils) generate FHIR resource IDs in two ways: 1) [ID generation template](data/Templates/Ccda/Utils/_GenerateId.liquid) helps generate Patient ID and Practitioner ID; 2) the resource IDs for other resources are generated from the resource object directly. - -For **JSON to FHIR conversion**, there is no standardized JSON input message types unlike HL7v2 messages or C-CDA documents. Therefore, instead of default templates we provide you with some sample JSON DotLiquid templates that you can use as a starting guide for your custom JSON conversion templates. You can decide how to generate the resource IDs according to your own inputs, and use our sample templates as a reference. - -For **FHIR STU3 to R4 conversion**, the Resource ID from STU3 resource is copied over to corresponding R4 resource. - -The Converter introduces a concept of "base resource/base ID". Base resources are independent entities, like Patient, Organization, Device, etc, whose IDs are defined as base ID. Base IDs could be used to generate IDs for other resources that relate to them. It helps enrich the input for hash and thus reduce ID collision. -For example, a Patient ID is used as part of hash input for an AllergyIntolerance ID, as this resource is closely related with a specific patient. - -Below is an example where an AllergyIntolerance ID is generated, using ID/AllergyIntolerance template, AL1 segment and patient ID as its base ID. -The syntax is `{% evaluate [id] using [template] [variables] -%}`. - -```liquid -{% evaluate allergyIntoleranceId using 'ID/AllergyIntolerance' AL1: al1Segment, baseId: patientId -%} -``` +In addition to the example [templates](data/Templates) provided there are several important concepts to review and consider when creating your own templates, including: +- [Filters summary](docs/Filters-and-Tags.md) +- [Snippet concept](docs/SnippetConcept.md) +- [Resource Id generation](docs/concepts/resource-id-generation.md) +- [Validation & post processing](docs/concepts/validation-and-postprocessing.md) -## Resource validation and post-processing +To use your custom templates, the FHIR converter API offers robust support for storing and retrieving your templates from Azure storage. For more information see: [Template Store Integration](/docs/how-to-guides/enable-template-store-integration.md). -The output of converter depends on the templates as well as the quality and richness of input messages. Therefore, it is important that you review and validate the Converter output before using those in production. +## Deployment -In general, you can use [HL7 FHIR validator](https://wiki.hl7.org/Using_the_FHIR_Validator) to validate a FHIR resource. You may be able to fix some of the conversion issues by appropriately changing the templates. For other issues, you may need to have a post-processing step in your pipeline. +You can deploy the FHIR converter API using the instructions found [here](/docs/how-to-guides/deployment-options.md). The default deployment will deploy the FHIR Conventer API container hosted on Azure Container Apps. -In some cases, due to lack of field level data in the incoming messages, the Converter may produce resources without useful information or even without ID. You can use `Hl7.Fhir.R4` .NET library to filter such resources in your pipeline. Here is the sample code for such purpose. +## API -```C# -using Hl7.Fhir.Model; -using Hl7.Fhir.Serialization; -using System; -using System.Collections.Generic; -using System.Linq; +The conversion APIs process the provided input data of the specified format and use the specified Liquid template (default or custom) and return the converted result as per the transformations in the template. -public class PostProcessor -{ - private readonly FhirJsonParser _parser = new FhirJsonParser(); +![Convert API summary](docs/images/convert-api-summary.png) - public IEnumerable FilterResources(IEnumerable fhirResources) - { - return fhirResources - .Select(fhirResource => _parser.Parse(fhirResource)) - .Where(resource => !IsEmptyResource(resource)) - .Where(resource => !IsIdAbsentResource(resource)); - } +Complete details on the FHIR converter APIs and examples can be found [here](/docs/how-to-guides/use-convert-web-apis.md). - public bool IsEmptyResource(Resource resource) - { - try - { - var fhirResource = resource.ToJObject(); - var properties = fhirResource.Properties().Select(property => property.Name); - // an empty resource contains no properties other than "resourceType" and "id" - return !properties - .Where(property => !property.Equals("resourceType")) - .Where(property => !property.Equals("id")) - .Any(); - } - catch (Exception e) - { - Console.Error.WriteLine(e.Message); - // deal with the exception... - } +## Troubleshooting - return false; - } +Some key concepts to consider: +* Processing time is related to both the input message size, template, and logic contained in the template. If your template is taking a long time to execute make sure you don't have any unnecessary loops. +* The output of the template is expected to be JSON when the target is FHIR. +* When converting data to FHIR, [post processing](https://github.com/microsoft/FHIR-Converter/blob/main/src/Microsoft.Health.Fhir.Liquid.Converter/OutputProcessors/PostProcessor.cs) is performed. If you are seeing unexpected results, double check the post processing logic. +* If you want a deeper understanding on how data is converted, look at the functional tests found [here](https://github.com/microsoft/FHIR-Converter/blob/main/src/Microsoft.Health.Fhir.Liquid.Converter.FunctionalTests/ConvertDataTemplateCollectionProviderFunctionalTests.cs) - public bool IsIdAbsentResource(Resource resource) - { - try - { - return string.IsNullOrWhiteSpace(resource.Id); - } - catch (Exception e) - { - Console.Error.WriteLine(e.Message); - // deal with the exception... - } - return false; - } -} -``` +Detailed troubleshooting options for your deployed FHIR converter API can be found [here](docs/how-to-guides/troubleshoot.md). -## Reference documentation +## Previous Versions +Detailed documentation of prior Converter release is covered in the table below. -- [Filters summary](docs/Filters-and-Tags.md) -- [Snippet concept](docs/SnippetConcept.md) +| Version | Summary | +| ----- | ----- | +| [5.x Liquid](https://github.com/microsoft/FHIR-Converter/tree/e49b56f165e5607726063c681e90a28e68e39133) | Liquid engine release covers:
1. HL7v2, CCDA, and JSON to FHIR transformations.
2. Command Line utility.
3. VS Code authoring extension.
4. FHIR Service $convert integration.
5. ACR template storage. | +| [3.x Handlebars](https://github.com/microsoft/FHIR-Converter/tree/handlebars) | Previous handlebars base solution. No longer supported. See full comparision [here](https://github.com/microsoft/FHIR-Converter/tree/e49b56f165e5607726063c681e90a28e68e39133?tab=readme-ov-file#fhir-converter). ## External resources - [DotLiquid wiki](https://github.com/dotliquid/dotliquid/wiki) +- [Liquid wiki](https://github.com/Shopify/liquid/wiki) - [HL7 Community 2-To-FHIR-Project](https://confluence.hl7.org/display/OO/2-To-FHIR+Project) ## Contributing diff --git a/docs/Filters-and-Tags.md b/docs/Filters-and-Tags.md index ff723f5bc..d37b878c6 100644 --- a/docs/Filters-and-Tags.md +++ b/docs/Filters-and-Tags.md @@ -1,7 +1,5 @@ # Filters and Tags -⚠ **This document applies to the Liquid engine. Follow [this](https://github.com/microsoft/FHIR-Converter/tree/handlebars) link for the documentation of Handlebars engine.** - ## Filters By default, Liquid provides a set of [standard filters](https://github.com/Shopify/liquid/wiki/Liquid-for-Designers#standard-filters) to assist template creation. @@ -59,6 +57,7 @@ If these filters do not meet your needs, you can also write your own filters. | format_as_date_time | Converts valid HL7v2 and C-CDA datetime to a valid FHIR datetime format. The input datetime format is datetime or partial datetime without hyphens: YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ]. For example, the input 20040629175400000 will have the output 2004-06-29T17:54:00.000Z. Provides parameters to handle different time zones: preserve, utc, local. The default method is preserve. | `{{ PID.29.Value \| format_as_date_time: 'utc' }}` | | now | Provides the current time in a specific format. The default format is yyyy-MM-ddTHH:mm:ss.FFFZ. | `{{ '' \| now: 'dddd, dd MMMM yyyy HH:mm:ss' }}` | | add_seconds | Adds double seconds on a valid [FHIR datetime](http://hl7.org/fhir/R4/datatypes.html) (e.g.2021-01-01T00:00:00Z). Provides parameters to handle different time zones: preserve, utc, local. The default method is preserve. | `{{ "2021-01-01T00:00:00Z" \| add_seconds:100.1, 'utc' }}` | +| date | This is the standard [Liquid date filter](https://github.com/Shopify/liquid/wiki/Liquid-for-Designers#standard-filters). In the underlying DotLiquid implementation, .NET date formats are used ([standard](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings) & [custom](https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings)). | `{{ msg.date \| date: "yyyy-MM-ddTHH:mm:ss.fff" }}` | DateTime filters usage examples: diff --git a/docs/HL7v2-templates.md b/docs/HL7v2-templates.md index 1b21201e1..49163c707 100644 --- a/docs/HL7v2-templates.md +++ b/docs/HL7v2-templates.md @@ -1,7 +1,5 @@ # HL7v2 Templates -*This document applies to the Liquid engine only. Follow [this](https://github.com/microsoft/FHIR-Converter/tree/handlebars) link for the documentation on Handlebars engine.* - For HL7v2 to FHIR conversion, we provide a total of **57 HL7v2 conversion templates**. Here, you can find a detailed information about each template, such as the mapped FHIR resource types, segments, and extensions. ## Templates diff --git a/docs/SnippetConcept.md b/docs/SnippetConcept.md index 40c14a4f9..93a7dba4d 100644 --- a/docs/SnippetConcept.md +++ b/docs/SnippetConcept.md @@ -1,7 +1,5 @@ # Snippet Concept -⚠ **This document applies to the Liquid engine. Follow [this](https://github.com/microsoft/FHIR-Converter/tree/handlebars) link for the documentation of Handlebars engine.** - **Snippets** are helpful when creating templates for the FHIR Converter. They are "snippets of templates" that you can reference when writing your own templates, preventing you from having to rewrite the same code over again. Within the FHIR converter release, there are seven types of snippets: **Resources**, **References**, **Data Type**, **Code Systems**, **Sections**, **Utils** and **Value Set**. The following sections will describe the purpose of each category of released snippets and give you things to consider when you are creating your own snippets. diff --git a/docs/TemplateManagementCLI.md b/docs/TemplateManagementCLI.md deleted file mode 100644 index df409107a..000000000 --- a/docs/TemplateManagementCLI.md +++ /dev/null @@ -1,108 +0,0 @@ -The $convert-data operation in the FHIR Server for Azure takes templateCollectionReference parameter, which refers to an [OCI image ](https://github.com/opencontainers/image-spec) on [Azure Container Registry (ACR)](https://azure.microsoft.com/en-us/services/container-registry/). It is the image containing Liquid templates to use for conversion. - -The Template Management CLI tool is mean to pull, push, and manage the templates on the ACR. - -Template OCI image is a layer based structure similar to docker image and uses [overlayfs](https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html?highlight=overlayfs) concept to organize templates. For custom templates, we use two layers image structure to organize template collection: base layer and user layer (The user layer could be extended to multi-layers in the future if necessary). Base layer packs Microsoft published templates and user layer packs all modified templates from users. Each layer will be compressed into "*.tar.gz" file before pushing to ACR. -# Using Template Management CLI - -The command-line tool can be used to pull and push a template collection from/to Azure Container Registry. Currently, we support Windows and MacOS for the CLI tool. - -## Prerequisites -* Azure container registry - Create a container registry in your Azure subscription if you do not have one. This is the registry where you want to keep your Liquid templates. You can use the [Azure portal](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-get-started-portal) or the [Azure CLI](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-get-started-azure-cli). - - -## Authentication - -Before pull & push operations, azure authentication is required for private registries. Customers can directly use individual login with Azure AD through [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/authenticate-azure-cli) or use identity (individual identity or Azure AD [service principal identity](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-auth-service-principal)) to sign in the registry. - -### Login Using Azure CLI - -To use individual login with Azure AD, you need a local installation of the Azure CLI. The latest version is recommended. Run `az --version` to find the version. If you need to install or upgrade, see [Install Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli). - -You should first sign in to the Azure CLI with your identity, and then use the Azure CLI command `az acr login` to access the registry. Make sure you have permissions on the registry in order to pull/push. -``` -> az login -> az acr login --name -``` - -### Login Using Identity (individual or service principal indentity) - - -* Docker login - -To use docker login, you should install docker first. -``` -> docker login -u -p -``` -* Oras Login - -The [Oras](https://github.com/deislabs/oras) executable tools (oras.exe for windows and oras-osx for mac) are packed in our repo, users can directly use it for login as follows. - -``` -> login -u -p -``` ->Note: macOS users need to add execute permission for oras-osx file before using. ->``` ->chmod +x oras-osx ->``` - -If using service principal's identity for authentication, you need to create a [service principal](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-auth-service-principal) to access your registry. Ensure that the service principal is assigned a role such as AcrPush so that it has permissions to push and pull artifacts. -## Push -To push a template collection, the command is: - -``` -push InputTemplateFolder [ -n | --NewBaseLayer] -``` -| Value | index |Optionality | Description | -| ----- | ----- | ----- |----- | -| ImageReference |0| Required | Image reference: \\/\:\ (Here is the [reference](https://docs.docker.com/engine/reference/commandline/tag/#extended-description) for the valid format.)| -|InputTemplateFolder | 1 |Required |Input template folder. | - -| Option | Name | Optionality | Default | Description | -| ----- | ----- | ----- |----- |----- | -| -n | BuildNewBaseLayer | Optional | false | Ignore previous base layer and build new layer. | - -Example command to push a collection of templates to ACR image from a folder: - -``` ->.\Microsoft.Health.Fhir.Liquid.Converter.Tool.exe push testacr.azurecr.io/templatetest:default myInputFolder -``` -When pushing templates, all files except files in hidden image folder ("./.image/") will be packed as new template image. If the folder is unpacked from a previous template image, our tool will pack all user modified files into the user layer and then push all layers to ACR (The base layer is in hidden folder "./.image/"). If customers use -n as parameter, all templates will be packed together and be pushed as one layer to ACR. - ->[Note!]: As for template OCI image, entry templates should be present directly in the root folder. - -After successfully pushing an image, relevant information including layers' digests and image digest will output to users. Here is an output example, users should remember the image digest which exactly indexes an image: - -``` -Uploading 4085e9f97630 layer2.tar.gz -Uploading 4157f847ecb1 layer1.tar.gz -Pushed testacr.azurecr.io/templatetest:default -Digest: sha256:412ea84f1bb1a9d98345efb7b427ba89616ec29ac332d543eff9a2161ca12a58 -``` - -## Pull -For pull operation, the command is - -``` -pull [ -f | --ForceOverride] -``` - -| Value | index |Optionality | Description | -| ----- | ----- | ----- |----- | -| ImageReference |0| Required | Image reference: \\/\@\ or \\/\\:\ | -|OutputTemplateFolder | 1 |Required | Output template folder. | - -| Option | Name | Optionality | Default | Description | -| ----- | ----- | ----- |----- |----- | -| -f | ForceOverride | Optional | false | Force to override the output folder. | - -Example usage of pulling an image of templates in a folder: - -``` ->.\Microsoft.Health.Fhir.Liquid.Converter.Tool.exe pull testacr.azurecr.io/templatetest@sha256:412ea84f1bb1a9d98345efb7b427ba89616ec29ac332d543eff9a2161ca12a58 myOutputFolder -``` - -After a collection of templates is pulled, a hidden folder ".image/" which contains information of metadata and layers is also created in the output folder. Users shouldn't modify this hidden folder which may lead to unexpected results. - -Image tags are mutable and could be overwritten unintentionally. We recommend you write down the image digest and use the immutable image digest as the template reference. Users should remember the image digest when pushing or find digest from ACR, since it won't be searched by our tool for now. - diff --git a/docs/concepts/resource-id-generation.md b/docs/concepts/resource-id-generation.md new file mode 100644 index 000000000..ade8f54f9 --- /dev/null +++ b/docs/concepts/resource-id-generation.md @@ -0,0 +1,21 @@ +# Resource ID generation + +The default templates provided with the Converter computes Resource IDs using the input data fields. In order to preserve the generated Resource IDs, the default templates provide create **PUT requests**, instead of POST requests in the generated bundles. + +For **HL7v2 to FHIR conversion**, [HL7v2 DotLiquid templates](data/Templates/Hl7v2/ID) help generate FHIR resource IDs from HL7v2 messages. An ID generation template does three things: 1) extract identifiers from the input segment or field; 2) combine the identifers with resource type and base ID (optional) as hash seed; 3) compute hash as output ID. + +For **C-CDA to FHIR conversion**, [C-CDA DotLiquid templates](data/Templates/Ccda/Utils) generate FHIR resource IDs in two ways: 1) [ID generation template](data/Templates/Ccda/Utils/_GenerateId.liquid) helps generate Patient ID and Practitioner ID; 2) the resource IDs for other resources are generated from the resource object directly. + +For **JSON to FHIR conversion**, there is no standardized JSON input message types unlike HL7v2 messages or C-CDA documents. Therefore, instead of default templates we provide you with some sample JSON DotLiquid templates that you can use as a starting guide for your custom JSON conversion templates. You can decide how to generate the resource IDs according to your own inputs, and use our sample templates as a reference. + +For **FHIR STU3 to R4 conversion**, the Resource ID from STU3 resource is copied over to corresponding R4 resource. + +The Converter introduces a concept of "base resource/base ID". Base resources are independent entities, like Patient, Organization, Device, etc, whose IDs are defined as base ID. Base IDs could be used to generate IDs for other resources that relate to them. It helps enrich the input for hash and thus reduce ID collision. +For example, a Patient ID is used as part of hash input for an AllergyIntolerance ID, as this resource is closely related with a specific patient. + +Below is an example where an AllergyIntolerance ID is generated, using ID/AllergyIntolerance template, AL1 segment and patient ID as its base ID. +The syntax is `{% evaluate [id] using [template] [variables] -%}`. + +```liquid +{% evaluate allergyIntoleranceId using 'ID/AllergyIntolerance' AL1: al1Segment, baseId: patientId -%} +``` \ No newline at end of file diff --git a/docs/concepts/validation-and-postprocessing.md b/docs/concepts/validation-and-postprocessing.md new file mode 100644 index 000000000..343763ec5 --- /dev/null +++ b/docs/concepts/validation-and-postprocessing.md @@ -0,0 +1,63 @@ +# Resource validation and post-processing + +The output of converter depends on the templates as well as the quality and richness of input messages. Therefore, it is important that you review and validate the Converter output before using those in production. + +In general, you can use [HL7 FHIR validator](https://wiki.hl7.org/Using_the_FHIR_Validator) to validate a FHIR resource. You may be able to fix some of the conversion issues by appropriately changing the templates. For other issues, you may need to have a post-processing step in your pipeline. + +In some cases, due to lack of field level data in the incoming messages, the Converter may produce resources without useful information or even without ID. You can use `Hl7.Fhir.R4` .NET library to filter such resources in your pipeline. Here is the sample code for such purpose. + +```C# +using Hl7.Fhir.Model; +using Hl7.Fhir.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; + +public class PostProcessor +{ + private readonly FhirJsonParser _parser = new FhirJsonParser(); + + public IEnumerable FilterResources(IEnumerable fhirResources) + { + return fhirResources + .Select(fhirResource => _parser.Parse(fhirResource)) + .Where(resource => !IsEmptyResource(resource)) + .Where(resource => !IsIdAbsentResource(resource)); + } + + public bool IsEmptyResource(Resource resource) + { + try + { + var fhirResource = resource.ToJObject(); + var properties = fhirResource.Properties().Select(property => property.Name); + // an empty resource contains no properties other than "resourceType" and "id" + return !properties + .Where(property => !property.Equals("resourceType")) + .Where(property => !property.Equals("id")) + .Any(); + } + catch (Exception e) + { + Console.Error.WriteLine(e.Message); + // deal with the exception... + } + + return false; + } + + public bool IsIdAbsentResource(Resource resource) + { + try + { + return string.IsNullOrWhiteSpace(resource.Id); + } + catch (Exception e) + { + Console.Error.WriteLine(e.Message); + // deal with the exception... + } + return false; + } +} +``` \ No newline at end of file diff --git a/docs/deploy/Deploy-DependentResources.bicep b/docs/deploy/Deploy-DependentResources.bicep new file mode 100644 index 000000000..226e5f521 --- /dev/null +++ b/docs/deploy/Deploy-DependentResources.bicep @@ -0,0 +1,124 @@ +/* +This template deploys the following: +* Storage account (if enableTemplateStoreIntegration is set to true) +* Storage account container (if enableTemplateStoreIntegration is set to true) +* Key vault (if deployKeyVault is set to true) +* User assigned identity with Key Vault Secrets User role on the Key Vault (if deployKeyVault is set to true) +* Role assignment for the user assigned identity to access the Key Vault (if deployKeyVault is set to true) +*/ + +@description('Location where the storage account is deployed. For list of Azure regions where Blob Storage is available, see [Products available by region](https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/?products=storage).') +@allowed([ + 'australiacentral' + 'australiaeast' + 'australiasoutheast' + 'brazilsouth' + 'canadacentral' + 'canadaeast' + 'centralindia' + 'centralus' + 'chinaeast2' + 'chinanorth2' + 'chinanorth3' + 'eastasia' + 'eastus' + 'eastus2' + 'francecentral' + 'germanywestcentral' + 'italynorth' + 'japaneast' + 'japanwest' + 'koreacentral' + 'northcentralus' + 'northeurope' + 'norwayeast' + 'polandcentral' + 'qatarcentral' + 'southafricanorth' + 'southcentralus' + 'southeastasia' + 'southindia' + 'swedencentral' + 'switzerlandnorth' + 'uaenorth' + 'uksouth' + 'ukwest' + 'westcentralus' + 'westeurope' + 'westus' + 'westus2' + 'westus3' +]) +param location string + +@description('If set to true, a storage account and blob container will be deployed with the specified names for storing custom templates.') +param deployTemplateStore bool + +@description('Name of the storage account to be deployed.') +param templateStorageAccountName string = '' + +@description('Name of the storage account container to be deployed.') +param templateStorageAccountContainerName string = '' + +@description('If set to true, a key vault and user assigned managed identity will be deployed with the specified names.') +param deployKeyVault bool + +@description('Name of the key vault to be deployed.') +param keyVaultName string = '' + +@description('Name of the user-assigned managed identity to be deployed for accessing the key vault.') +param keyVaultUserAssignedIdentityName string = '' + +resource templateStorageAccountCreated 'Microsoft.Storage/storageAccounts@2022-09-01' = if (deployTemplateStore) { + name: deployTemplateStore ? templateStorageAccountName : 'default' + location: location + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: {} +} + +resource templateStorageAccount 'Microsoft.Storage/storageAccounts/blobServices@2022-09-01' = if (deployTemplateStore) { + name: 'default' + parent: templateStorageAccountCreated +} + +resource templateStorageAccountContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = if (deployTemplateStore) { + name: deployTemplateStore ? templateStorageAccountContainerName : 'default' + parent: templateStorageAccount +} + +resource keyVault 'Microsoft.KeyVault/vaults@2021-04-01-preview' = if (deployKeyVault) { + name: deployKeyVault ? keyVaultName : 'default' + location: location + properties: { + sku: { + family: 'A' + name: 'standard' + } + tenantId: subscription().tenantId + enableRbacAuthorization: true + } +} + +resource keyVaultUserAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = if (deployKeyVault) { + name: deployKeyVault ? keyVaultUserAssignedIdentityName : 'default' + location: location +} + +var kvSecretUserRole = '4633458b-17de-408a-b874-0445c86b69e6' // Key Vault Secrets User role +resource keyVaultSecretsUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (deployKeyVault) { + name: guid(resourceGroup().id, keyVaultUserAssignedIdentity.id, kvSecretUserRole) + scope: keyVault + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', kvSecretUserRole) + principalId: deployKeyVault ? keyVaultUserAssignedIdentity.properties.principalId : 'default' + principalType: 'ServicePrincipal' + } +} + +output templateStorageAccountName string = deployTemplateStore ? templateStorageAccountCreated.name : '' +output templateStorageAccountContainerName string = deployTemplateStore ? templateStorageAccountContainer.name : '' +output keyVaultName string = deployKeyVault ? keyVault.name : '' +output keyVaultUAMIName string = deployKeyVault ? keyVaultUserAssignedIdentity.name : '' diff --git a/docs/deploy/Deploy-FhirConverterService.bicep b/docs/deploy/Deploy-FhirConverterService.bicep new file mode 100644 index 000000000..bf81de842 --- /dev/null +++ b/docs/deploy/Deploy-FhirConverterService.bicep @@ -0,0 +1,253 @@ +/* +This template deploys the following: +* A container app running the FHIR-Converter +* Role assignment for the container app to read custom templates from the storage container (if the template storage account and container names are specified) +*/ + +@description('Location where the resources are deployed. Note that a Container App can only be provisioned in the same region as its parent Container Apps Environment. For list of Azure regions where Container Apps is available, see [Products available by region](https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/?products=container-apps)') +@allowed([ + 'australiaeast' + 'brazilsouth' + 'canadacentral' + 'canadaeast' + 'centralindia' + 'centralus' + 'chinanorth3' + 'eastasia' + 'eastus' + 'eastus2' + 'francecentral' + 'germanywestcentral' + 'japaneast' + 'koreacentral' + 'northcentralus' + 'northeurope' + 'norwayeast' + 'southafricanorth' + 'southcentralus' + 'southeastasia' + 'swedencentral' + 'switzerlandnorth' + 'uaenorth' + 'uksouth' + 'ukwest' + 'westcentralus' + 'westeurope' + 'westus' + 'westus2' + 'westus3' +]) +param location string + +@description('The name of the container app running the FHIR-Converter service.') +param appName string + +@description('The name of the container apps environment where the app will run.') +param envName string + +@description('Name of storage account containing custom templates. Leave blank if using default templates.') +param templateStorageAccountName string = '' + +@description('Name of the container in the storage account containing custom templates. Leave blank if using default templates.') +param templateStorageAccountContainerName string = '' + +@description('Name of the key vault containing the application insights connection string secret.') +param keyVaultName string = '' + +@description('Name of the user-assigned managed identity to be used by the container app to access key vault secrets.') +param keyVaultUAMIName string = '' + +@description('Minimum possible number of replicas per revision as the container app scales.') +param minReplicas int = 0 + +@description('Maximum possible number of replicas per revision as the container app scales.') +param maxReplicas int = 30 + +@description('CPU usage limit in cores.') +param cpuLimit string = '1.0' + +@description('Memory usage limit in Gi.') +param memoryLimit string = '2Gi' + +@description('If set to true, security will be enabled on the API endpoint.') +param securityEnabled bool = false + +@description('List of audiences that the authentication token is intended for.') +param securityAuthenticationAudiences array = [] + +@description('Issuing authority of the JWT token.') +param securityAuthenticationAuthority string = '' + +@description('Tag of the image to deploy. To see available image versions, visit the [FHIR Converter MCR page](https://mcr.microsoft.com/en-us/product/healthcareapis/fhir-converter/tags)') +param imageTag string + +@description('Timestamp to append to container name. Defaults to time of deployment.') +param timestamp string = utcNow('yyyyMMddHHmmss') + +@description('The ID of the user-assigned managed identity to be used by the container app to access application insights.') +param applicationInsightsUAMIName string = '' + +@description('The name of the secret in the key vault containing the application insights connection string.') +param applicationInsightsConnectionStringSecretName string = '' + +// Get the container apps environment +resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' existing = { + name: envName +} + +var configureApplicationInsights = !empty(applicationInsightsUAMIName) + +resource applicationInsightsUAMI 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = if (configureApplicationInsights) { + name: applicationInsightsUAMIName +} + +// Security configuration +var securityEnabledConfigName = 'ConvertService__Security__Enabled' +var securityAuthenticationAudiencesConfigNamePrefix = 'ConvertService__Security__Authentication__Audiences__' +var securityAuthenticationAuthorityConfigName = 'ConvertService__Security__Authentication__Authority' + +var securityEnabledConfiguration = [ + { + name: securityEnabledConfigName + value: string(securityEnabled) + } +] + +var securityAuthenticationAuthorityConfig = [ + { + name: securityAuthenticationAuthorityConfigName + value: securityAuthenticationAuthority + } +] + +var securityAuthenticationAudiencesConfig = [for (audience, i) in securityAuthenticationAudiences: { + name: '${securityAuthenticationAudiencesConfigNamePrefix}${i}' + value: audience +}] + +var securityConfiguration = concat(securityEnabledConfiguration, securityEnabled ? concat(securityAuthenticationAuthorityConfig, securityAuthenticationAudiencesConfig) : []) + +// Template hosting configuration +var integrateTemplateStore = !empty(templateStorageAccountName) && !empty(templateStorageAccountContainerName) +var storageEnvironmentSuffix = az.environment().suffixes.storage +var blobTemplateHostingConfigurationName = 'TemplateHosting__StorageAccountConfiguration__ContainerUrl' +var blobTemplateHostingConfigurationValue = 'https://${templateStorageAccountName}.blob.${storageEnvironmentSuffix}/${templateStorageAccountContainerName}' +var blobTemplateHostingConfiguration = integrateTemplateStore ? [ + { + name: blobTemplateHostingConfigurationName + value: blobTemplateHostingConfigurationValue + } +] : [] + +// Application insights configuration +var applicationInsightsConnectionStringConfigurationName = 'ConvertService__Telemetry__AzureMonitor__ApplicationInsightsConnectionString' +var applicationInsightsUAMIClientIdConfigurationName = 'ConvertService__Telemetry__AzureMonitor__ManagedIdentityClientId' +var telemetryConfiguration = configureApplicationInsights ? [ + { + name: applicationInsightsConnectionStringConfigurationName + secretRef: applicationInsightsConnectionStringSecretName + } + { + name: applicationInsightsUAMIClientIdConfigurationName + value: applicationInsightsUAMI.properties.clientId + } +] : [] + +// Environment Variables for Container App +var envConfiguration = concat(securityConfiguration, telemetryConfiguration, blobTemplateHostingConfiguration) + +var imageName = 'healthcareapis/fhir-converter' + +// Configure identities +var applicationInsightsUAMIResourceId = configureApplicationInsights ? applicationInsightsUAMI.id : '' +var keyVaultUAMIResourceId = resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', keyVaultUAMIName) +var userAssignedIdentities = configureApplicationInsights ? { + '${applicationInsightsUAMIResourceId}' : {} + '${keyVaultUAMIResourceId}' : {} +} : {} + +var akvEnvironmentSuffix = az.environment().suffixes.keyvaultDns +var applicationInsightsConnStringAKVSecretUrl = 'https://${keyVaultName}${akvEnvironmentSuffix}/secrets/${applicationInsightsConnectionStringSecretName}' + +resource containerApp 'Microsoft.App/containerApps@2023-05-01' = { + name: appName + location: location + identity: (configureApplicationInsights) ? { + type: 'SystemAssigned, UserAssigned' + userAssignedIdentities: userAssignedIdentities + } : { + type: 'SystemAssigned' + } + properties:{ + managedEnvironmentId: containerAppsEnvironment.id + configuration: { + ingress: { + targetPort: 8080 + external: true + } + secrets: configureApplicationInsights ? [ + { + name: applicationInsightsConnectionStringSecretName + keyVaultUrl: applicationInsightsConnStringAKVSecretUrl + identity: keyVaultUAMIResourceId + } + ] : [] + } + template: { + containers: [ + { + image: 'mcr.microsoft.com/${imageName}:${imageTag}' + name: 'fhir-converter-${timestamp}' + env: envConfiguration + resources: { + cpu: json(cpuLimit) + memory: memoryLimit + } + } + ] + scale: { + minReplicas: minReplicas + maxReplicas: maxReplicas + } + } + } + tags: { + fhirConverterEnvName: envName + fhirConverterAppName: appName + fhirConverterImageName: imageName + fhirConverterImageVersion: imageTag + } +} + +// Reference the existing storage account +resource templateStorageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' existing = if (integrateTemplateStore) { + name: templateStorageAccountName +} + +// Reference the existing blob service +resource templateBlobService 'Microsoft.Storage/storageAccounts/blobServices@2022-09-01' existing = if (integrateTemplateStore) { + name: 'default' + parent: templateStorageAccount +} + +// Reference the existing container +resource templateStorageAccountContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' existing = if (integrateTemplateStore) { + name: templateStorageAccountContainerName + parent: templateBlobService +} + +var roleAssignmentName = guid(templateStorageAccountContainer.id, appName, storageBlobDataReaderRoleDefinitionId) +var storageBlobDataReaderRoleDefinitionId = resourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1') +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (integrateTemplateStore) { + name: guid(roleAssignmentName) + scope: templateStorageAccountContainer + properties: { + principalId: containerApp.identity.principalId + principalType: 'ServicePrincipal' + roleDefinitionId: storageBlobDataReaderRoleDefinitionId + } +} + +// Output +output containerAppFQDN string = containerApp.properties.configuration.ingress.fqdn +output containerAppLatestRevisionName string = containerApp.properties.latestRevisionName diff --git a/docs/deploy/Deploy-FhirConverterService.ps1 b/docs/deploy/Deploy-FhirConverterService.ps1 new file mode 100644 index 000000000..f7bf8f574 --- /dev/null +++ b/docs/deploy/Deploy-FhirConverterService.ps1 @@ -0,0 +1,151 @@ +param +( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [ValidateLength(3,9)] + [ValidateScript({ + if ("$_" -cmatch "(^([a-z]|\d)+$)") { + return $true + } + else { + throw "Service name must be lowercase and numbers" + return $false + } + })] + [string]$serviceName, + + [Parameter(Mandatory = $true)] + [ValidateSet( + 'australiaeast', + 'brazilsouth', + 'canadacentral', + 'canadaeast', + 'centralindia', + 'centralus', + 'chinanorth3', + 'eastasia', + 'eastus', + 'eastus2', + 'francecentral', + 'germanywestcentral', + 'japaneast', + 'koreacentral', + 'northcentralus', + 'northeurope', + 'norwayeast', + 'southafricanorth', + 'southcentralus', + 'southeastasia', + 'swedencentral', + 'switzerlandnorth', + 'uaenorth', + 'uksouth', + 'westeurope', + 'westus', + 'westus2', + 'westus3' + )] + [string]$location, + + [Parameter(Mandatory = $true)] + [string]$containerAppImageTag, + + [string]$timestamp = (Get-Date -Format "yyyyMMddHHmmss"), + + [string]$resourceGroupName = "$($serviceName)-rg", + + [string]$containerAppEnvName = "$($serviceName)-app-env", + + [string]$containerAppName = "$($serviceName)-app", + + [int]$minReplicas = 0, + + [int]$maxReplicas = 30, + + [string]$cpuLimit = '1.0', + + [string]$memoryLimit = '2Gi', + + [bool]$templateStoreIntegrationEnabled = $false, + + [string]$templateStorageAccountName = "$($serviceName)templatestorage", + + [string]$templateStorageAccountContainerName = "$($serviceName)templatecontainer", + + [bool]$applicationInsightsEnabled=$true, + + [string]$keyVaultName = "$($serviceName)-kv", + + [string]$keyVaultUserAssignedIdentityName = "$($serviceName)-kv-identity", + + [bool]$securityEnabled = $false, + + [string[]]$securityAuthenticationAudiences = @(), + + [string]$securityAuthenticationAuthority = "" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# Get current Az context +try { + Write-Host "Get current Az context..." + az account show +} +catch { + throw "Please log in with az login cmdlet before proceeding." +} + +# Get current account context - User/Service Principal +$azAccountId = az account show --query user.name --output tsv +$azAccountType = az account show --query user.type --output tsv +if ($azAccountType -eq "user") { + Write-Host "Current account context is user: $($azAccountId)." +} +elseif ($azAccountType -eq "servicePrincipal") { + Write-Host "Current account context is service principal: $($azAccountId)." +} +else { + Write-Host "Current context is account of type '$($azAccountType)' with id of '$($azAccountId)." + throw "Running as an unsupported account type. Please use either a 'User' or 'Service Principal' to run this command." +} + +# Validate params +if ($securityEnabled -and ((-not $securityAuthenticationAudiences) -or (-not $securityAuthenticationAuthority))) +{ + Write-Error "If securityEnabled is set, then securityAuthenticationAudiences and securityAuthenticationAuthority should be provided." +} + +Write-Host "Deploying FHIR converter service..." + +$templateFile = "FhirConverter-SingleAzureDeploy.bicep" +$securityAuthenticationAudiencesArray = "['" + ($securityAuthenticationAudiences -join "','") + "']" + +az deployment sub create ` + --location $location ` + --template-file $templateFile ` + --name "$($serviceName)-$($templateFile)-$($timestamp)" ` + --parameters ` + serviceName=$serviceName ` + location=$location ` + containerAppImageTag=$containerAppImageTag ` + timestamp=$timestamp ` + resourceGroupName=$resourceGroupName ` + containerAppEnvName=$containerAppEnvName ` + containerAppName=$containerAppName ` + minReplicas=$minReplicas ` + maxReplicas=$maxReplicas ` + cpuLimit=$cpuLimit ` + memoryLimit=$memoryLimit ` + applicationInsightsEnabled=$applicationInsightsEnabled ` + templateStoreIntegrationEnabled=$templateStoreIntegrationEnabled ` + templateStorageAccountName=$templateStorageAccountName ` + templateStorageAccountContainerName=$templateStorageAccountContainerName ` + keyVaultName=$keyVaultName ` + keyVaultUserAssignedIdentityName=$keyVaultUserAssignedIdentityName ` + securityEnabled=$securityEnabled ` + securityAuthenticationAudiences=$securityAuthenticationAudiencesArray ` + securityAuthenticationAuthority=$securityAuthenticationAuthority + +Write-Host "Deployment complete." \ No newline at end of file diff --git a/docs/deploy/Deploy-Infrastructure.bicep b/docs/deploy/Deploy-Infrastructure.bicep new file mode 100644 index 000000000..6898dea77 --- /dev/null +++ b/docs/deploy/Deploy-Infrastructure.bicep @@ -0,0 +1,135 @@ +/* +This template deploys the following: +* Azure Log Analytics workspace +* Azure Application Insights (if deployApplicationInsights is set to true) +* A Key Vault secret containing the connection string to the Application Insights instance (if deployApplicationInsights is set to true) +* A user-assigned managed identity granted the "Monitoring Metrics Publisher" role to authenticate with Application Insights (if deployApplicationInsights is set to true) +* Azure Container Apps environment +*/ + +@description('Location where the resources are deployed. For list of Azure regions where the below resources are available, see [Products available by region](https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/?products=monitor,container-apps).') +@allowed([ + 'australiaeast' + 'brazilsouth' + 'canadacentral' + 'canadaeast' + 'centralindia' + 'centralus' + 'chinanorth3' + 'eastasia' + 'eastus' + 'eastus2' + 'francecentral' + 'germanywestcentral' + 'japaneast' + 'koreacentral' + 'northcentralus' + 'northeurope' + 'norwayeast' + 'southafricanorth' + 'southcentralus' + 'southeastasia' + 'swedencentral' + 'switzerlandnorth' + 'uaenorth' + 'uksouth' + 'ukwest' + 'westeurope' + 'westus' + 'westus2' + 'westus3' +]) +param location string + +@description('Name of the container apps environment.') +param envName string + +@description('If set to true, Application Insights logs and metrics collection will be enabled for the container app.') +param deployApplicationInsights bool + +@description('The name of the Key Vault to store the Application Insights connection string secret.') +param keyVaultName string = 'default' + +// Deploy log analytics workspace +var logAnalyticsWorkspaceName = '${envName}-logsws' +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2020-03-01-preview' = { + name: logAnalyticsWorkspaceName + location: location + properties: any({ + retentionInDays: 30 + features: { + searchVersion: 1 + } + sku: { + name: 'PerGB2018' + } + }) +} + +// Deploy application insights for receiving azure monitor telemetry +var applicationInsightsName = '${envName}-ai' +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = if (deployApplicationInsights) { + name: deployApplicationInsights ? applicationInsightsName : 'default' + location: location + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalyticsWorkspace.id + DisableLocalAuth: true + } +} + +resource keyVault 'Microsoft.KeyVault/vaults@2021-04-01-preview' existing = if (deployApplicationInsights) { + name: keyVaultName +} + +var applicationInsightsConnectionStringSecretName = '${applicationInsightsName}-connection-string' +resource applicationInsightsConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2021-04-01-preview' = if (deployApplicationInsights) { + parent: keyVault + name: applicationInsightsConnectionStringSecretName + properties: { + value: deployApplicationInsights ? applicationInsights.properties.ConnectionString : 'default' + } +} + +// Create user-assigned managed identity to authenticate with Application Insights +var applicationInsightsUAMIName = '${applicationInsightsName}-mi' +resource applicationInsightsUAMI 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = if (deployApplicationInsights) { + name: applicationInsightsUAMIName + location: location +} + +// Grant Monitoring Metrics Publisher role to applicationInsightsUAMI on applicationInsights +var monitoringMetricsPublisherRoleDefinition = '3913510d-42f4-4e42-8a64-420c390055eb' +resource monitoringMetricsPublisherRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (deployApplicationInsights) { + name: guid(resourceGroup().id, applicationInsights.id, monitoringMetricsPublisherRoleDefinition) + scope: deployApplicationInsights ? applicationInsights : resourceGroup() + properties: { + principalId: deployApplicationInsights ? applicationInsightsUAMI.properties.principalId : 'default' + principalType: 'ServicePrincipal' + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', monitoringMetricsPublisherRoleDefinition) + } +} + +// Deploy the container app environment +// https://github.com/Azure/azure-rest-api-specs/blob/Microsoft.App-2022-03-01/specification/app/resource-manager/Microsoft.App/preview/2022-01-01-preview/ManagedEnvironments.json +var containerAppEnvironmentName = envName +resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = { + name: containerAppEnvironmentName + location: location + properties: { + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalyticsWorkspace.properties.customerId + sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey + } + } + } +} + +output containerAppEnvironmentName string = containerAppEnvironment.name +output containerAppEnvironmentId string = containerAppEnvironment.id +output logAnalyticsWorkspaceName string = logAnalyticsWorkspace.name +output applicationInsightsUAMIName string = deployApplicationInsights ? applicationInsightsUAMI.name : '' +output applicationInsightsConnStringSecretName string = deployApplicationInsights ? applicationInsightsConnectionStringSecret.name : '' diff --git a/docs/deploy/FhirConverter-SingleAzureDeploy.bicep b/docs/deploy/FhirConverter-SingleAzureDeploy.bicep new file mode 100644 index 000000000..561967d51 --- /dev/null +++ b/docs/deploy/FhirConverter-SingleAzureDeploy.bicep @@ -0,0 +1,172 @@ +/* +This is the single-touch deployment template for deploying the following: +* dependent resources (key vault and/or template storage account) +* infrastructure for the container app +* the container app running the FHIR-Converter service +*/ + +targetScope = 'subscription' + +@minLength(3) +@maxLength(9) +@description('Used as the prefix to name provisioned resources where a custom name is not provided. Should be alphanumeric, at least 3 characters and no more than 9 characters.') +param serviceName string + +@description('Location where the resources are deployed.') +@allowed([ + 'australiaeast' + 'brazilsouth' + 'canadacentral' + 'canadaeast' + 'centralindia' + 'centralus' + 'chinanorth3' + 'eastasia' + 'eastus' + 'eastus2' + 'francecentral' + 'germanywestcentral' + 'japaneast' + 'koreacentral' + 'northcentralus' + 'northeurope' + 'norwayeast' + 'southafricanorth' + 'southcentralus' + 'southeastasia' + 'swedencentral' + 'switzerlandnorth' + 'uaenorth' + 'uksouth' + 'westeurope' + 'westus' + 'westus2' + 'westus3' +]) +param location string + +@description('The tag of the image to pull from MCR. To see available image tags, visit the [FHIR Converter MCR page](https://mcr.microsoft.com/en-us/product/healthcareapis/fhir-converter/tags)') +param containerAppImageTag string + +@description('Timestamp used to generate unique deployment names. Defaults to utcNow') +param timestamp string = utcNow('yyyyMMddHHmmss') + +@description('Name of the resource group to deploy the resources to. If the resource group does not already exist, a new resource group will be provisioned with the given name or, if a name is not provided, with an autogenerated name based on serviceName.') +param resourceGroupName string = '${serviceName}-rg' + +@description('Name of the container app environment. If a name is not provided, an autogenerated name based on serviceName will be used.') +param containerAppEnvName string = '${serviceName}-app-env' + +@description('Name of the container app to run the FHIR Converter service. If a name is not provided, an autogenerated name based on serviceName will be used.') +param containerAppName string = '${serviceName}-app' + +@description('Minimum number of replicas for the container app.') +param minReplicas int = 0 + +@description('Maximum number of replicas for the container app.') +param maxReplicas int = 30 + +@description('CPU limit for the container app.') +param cpuLimit string = '1.0' + +@description('Memory limit for the container app.') +param memoryLimit string = '2Gi' + +@description('Set to true to enable deployment of and integration with a storage account for custom templates.') +param templateStoreIntegrationEnabled bool = false + +@description('Name of storage account containing custom templates. If a name is not provided and enableTemplateStoreIntegration is true, an autogenerated name based on serviceName will be used.') +param templateStorageAccountName string = '${serviceName}templatestorage' + +@description('Name of storage account container containing custom templates. If a name is not provided and enableTemplateStoreIntegration is true, an autogenerated name based on serviceName will be used.') +param templateStorageAccountContainerName string = '${serviceName}templatecontainer' + +@description('If set to true, Application Insights logs and metrics collection will be enabled for the container app.') +param applicationInsightsEnabled bool = true + +@description('Name of the key vault to hold the application insights connection string as a secret. If a name is not provided, an autogenerated name based on serviceName will be used.') +param keyVaultName string = '${serviceName}-kv' + +@description('Name of the user-assigned managed identity to be deployed for accessing the key vault. If a name is not provided, an autogenerated name based on serviceName will be used.') +param keyVaultUserAssignedIdentityName string = '${serviceName}-kv-identity' + +@description('If set to true, security requirements will be enabled on the API endpoint. This is strongly recommended.') +param securityEnabled bool = false + +@description('Audiences for the api authentication.') +param securityAuthenticationAudiences array = [] + +@description('Authority for the api authentication.') +param securityAuthenticationAuthority string = '' + +var deploymentTemplateVersion = '1' + +resource resourceGroup 'Microsoft.Resources/resourceGroups@2020-06-01' = { + name: resourceGroupName + location: location + tags: { + fhirConverterDeploymentTemplateVersion: deploymentTemplateVersion + } +} + +// Deploy a keyVault if it is needed +var deployKeyVault = applicationInsightsEnabled + +// Deploy key vault and/or template storage account as required +module dependentResourceDeploy 'Deploy-DependentResources.bicep' = if (templateStoreIntegrationEnabled || deployKeyVault) { + name: 'dependentResourceDeploy_${timestamp}' + scope: resourceGroup + params: { + location: location + deployTemplateStore: templateStoreIntegrationEnabled + templateStorageAccountName: templateStorageAccountName + templateStorageAccountContainerName: templateStorageAccountContainerName + deployKeyVault: deployKeyVault + keyVaultName: keyVaultName + keyVaultUserAssignedIdentityName: keyVaultUserAssignedIdentityName + } +} + +// Deploy the infrastructure for the container app +module convertInfrastructureDeploy 'Deploy-Infrastructure.bicep' = { + name: 'convertInfrastructureDeploy_${timestamp}' + scope: resourceGroup + params: { + location: location + envName: containerAppEnvName + deployApplicationInsights: applicationInsightsEnabled + keyVaultName: keyVaultName + } +} + +// Deploy the container app +module fhirConverterDeploy 'Deploy-FhirConverterService.bicep' = { + name: 'fhirConverterDeploy_${timestamp}' + scope: resourceGroup + params: { + location: location + imageTag: containerAppImageTag + appName: containerAppName + envName: convertInfrastructureDeploy.outputs.containerAppEnvironmentName + minReplicas: minReplicas + maxReplicas: maxReplicas + cpuLimit: cpuLimit + memoryLimit: memoryLimit + securityEnabled: securityEnabled + securityAuthenticationAudiences: securityAuthenticationAudiences + securityAuthenticationAuthority: securityAuthenticationAuthority + templateStorageAccountName: templateStoreIntegrationEnabled ? dependentResourceDeploy.outputs.templateStorageAccountName : '' + templateStorageAccountContainerName: templateStoreIntegrationEnabled ? dependentResourceDeploy.outputs.templateStorageAccountContainerName : '' + keyVaultName: deployKeyVault ? dependentResourceDeploy.outputs.keyVaultName : '' + keyVaultUAMIName: deployKeyVault ? dependentResourceDeploy.outputs.keyVaultUAMIName : '' + applicationInsightsUAMIName: applicationInsightsEnabled ? convertInfrastructureDeploy.outputs.applicationInsightsUAMIName: '' + applicationInsightsConnectionStringSecretName: applicationInsightsEnabled ? convertInfrastructureDeploy.outputs.applicationInsightsConnStringSecretName : '' + } + dependsOn: [ + dependentResourceDeploy + convertInfrastructureDeploy + ] +} + +output fhirConverterApiEndpoint string = fhirConverterDeploy.outputs.containerAppFQDN +output resourceGroupName string = resourceGroup.name diff --git a/docs/deploy/FhirConverter-SingleAzureDeploy.json b/docs/deploy/FhirConverter-SingleAzureDeploy.json new file mode 100644 index 000000000..0c2d7c8f3 --- /dev/null +++ b/docs/deploy/FhirConverter-SingleAzureDeploy.json @@ -0,0 +1,986 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.26.54.24096", + "templateHash": "3360735534239771915" + } + }, + "parameters": { + "serviceName": { + "type": "string", + "minLength": 3, + "maxLength": 9, + "metadata": { + "description": "Used as the prefix to name provisioned resources where a custom name is not provided. Should be alphanumeric, at least 3 characters and no more than 9 characters." + } + }, + "location": { + "type": "string", + "allowedValues": [ + "australiaeast", + "brazilsouth", + "canadacentral", + "canadaeast", + "centralindia", + "centralus", + "chinanorth3", + "eastasia", + "eastus", + "eastus2", + "francecentral", + "germanywestcentral", + "japaneast", + "koreacentral", + "northcentralus", + "northeurope", + "norwayeast", + "southafricanorth", + "southcentralus", + "southeastasia", + "swedencentral", + "switzerlandnorth", + "uaenorth", + "uksouth", + "westeurope", + "westus", + "westus2", + "westus3" + ], + "metadata": { + "description": "Location where the resources are deployed." + } + }, + "containerAppImageTag": { + "type": "string", + "metadata": { + "description": "The tag of the image to pull from MCR. To see available image tags, visit the [FHIR Converter MCR page](https://mcr.microsoft.com/en-us/product/healthcareapis/fhir-converter/tags)" + } + }, + "timestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyyMMddHHmmss')]", + "metadata": { + "description": "Timestamp used to generate unique deployment names. Defaults to utcNow" + } + }, + "resourceGroupName": { + "type": "string", + "defaultValue": "[format('{0}-rg', parameters('serviceName'))]", + "metadata": { + "description": "Name of the resource group to deploy the resources to. If the resource group does not already exist, a new resource group will be provisioned with the given name or, if a name is not provided, with an autogenerated name based on serviceName." + } + }, + "containerAppEnvName": { + "type": "string", + "defaultValue": "[format('{0}-app-env', parameters('serviceName'))]", + "metadata": { + "description": "Name of the container app environment. If a name is not provided, an autogenerated name based on serviceName will be used." + } + }, + "containerAppName": { + "type": "string", + "defaultValue": "[format('{0}-app', parameters('serviceName'))]", + "metadata": { + "description": "Name of the container app to run the FHIR Converter service. If a name is not provided, an autogenerated name based on serviceName will be used." + } + }, + "minReplicas": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Minimum number of replicas for the container app." + } + }, + "maxReplicas": { + "type": "int", + "defaultValue": 30, + "metadata": { + "description": "Maximum number of replicas for the container app." + } + }, + "cpuLimit": { + "type": "string", + "defaultValue": "1.0", + "metadata": { + "description": "CPU limit for the container app." + } + }, + "memoryLimit": { + "type": "string", + "defaultValue": "2Gi", + "metadata": { + "description": "Memory limit for the container app." + } + }, + "templateStoreIntegrationEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Set to true to enable deployment of and integration with a storage account for custom templates." + } + }, + "templateStorageAccountName": { + "type": "string", + "defaultValue": "[format('{0}templatestorage', parameters('serviceName'))]", + "metadata": { + "description": "Name of storage account containing custom templates. If a name is not provided and enableTemplateStoreIntegration is true, an autogenerated name based on serviceName will be used." + } + }, + "templateStorageAccountContainerName": { + "type": "string", + "defaultValue": "[format('{0}templatecontainer', parameters('serviceName'))]", + "metadata": { + "description": "Name of storage account container containing custom templates. If a name is not provided and enableTemplateStoreIntegration is true, an autogenerated name based on serviceName will be used." + } + }, + "applicationInsightsEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "If set to true, Application Insights logs and metrics collection will be enabled for the container app." + } + }, + "keyVaultName": { + "type": "string", + "defaultValue": "[format('{0}-kv', parameters('serviceName'))]", + "metadata": { + "description": "Name of the key vault to hold the application insights connection string as a secret. If a name is not provided, an autogenerated name based on serviceName will be used." + } + }, + "keyVaultUserAssignedIdentityName": { + "type": "string", + "defaultValue": "[format('{0}-kv-identity', parameters('serviceName'))]", + "metadata": { + "description": "Name of the user-assigned managed identity to be deployed for accessing the key vault. If a name is not provided, an autogenerated name based on serviceName will be used." + } + }, + "securityEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "If set to true, security requirements will be enabled on the API endpoint. This is strongly recommended." + } + }, + "securityAuthenticationAudiences": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Audiences for the api authentication." + } + }, + "securityAuthenticationAuthority": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Authority for the api authentication." + } + } + }, + "variables": { + "deploymentTemplateVersion": "1", + "deployKeyVault": "[parameters('applicationInsightsEnabled')]" + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2020-06-01", + "name": "[parameters('resourceGroupName')]", + "location": "[parameters('location')]", + "tags": { + "fhirConverterDeploymentTemplateVersion": "[variables('deploymentTemplateVersion')]" + } + }, + { + "condition": "[or(parameters('templateStoreIntegrationEnabled'), variables('deployKeyVault'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('dependentResourceDeploy_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "deployTemplateStore": { + "value": "[parameters('templateStoreIntegrationEnabled')]" + }, + "templateStorageAccountName": { + "value": "[parameters('templateStorageAccountName')]" + }, + "templateStorageAccountContainerName": { + "value": "[parameters('templateStorageAccountContainerName')]" + }, + "deployKeyVault": { + "value": "[variables('deployKeyVault')]" + }, + "keyVaultName": { + "value": "[parameters('keyVaultName')]" + }, + "keyVaultUserAssignedIdentityName": { + "value": "[parameters('keyVaultUserAssignedIdentityName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.26.54.24096", + "templateHash": "18404091950647462215" + } + }, + "parameters": { + "location": { + "type": "string", + "allowedValues": [ + "australiacentral", + "australiaeast", + "australiasoutheast", + "brazilsouth", + "canadacentral", + "canadaeast", + "centralindia", + "centralus", + "chinaeast2", + "chinanorth2", + "chinanorth3", + "eastasia", + "eastus", + "eastus2", + "francecentral", + "germanywestcentral", + "italynorth", + "japaneast", + "japanwest", + "koreacentral", + "northcentralus", + "northeurope", + "norwayeast", + "polandcentral", + "qatarcentral", + "southafricanorth", + "southcentralus", + "southeastasia", + "southindia", + "swedencentral", + "switzerlandnorth", + "uaenorth", + "uksouth", + "ukwest", + "westcentralus", + "westeurope", + "westus", + "westus2", + "westus3" + ], + "metadata": { + "description": "Location where the storage account is deployed. For list of Azure regions where Blob Storage is available, see [Products available by region](https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/?products=storage)." + } + }, + "deployTemplateStore": { + "type": "bool", + "metadata": { + "description": "If set to true, a storage account and blob container will be deployed with the specified names for storing custom templates." + } + }, + "templateStorageAccountName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the storage account to be deployed." + } + }, + "templateStorageAccountContainerName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the storage account container to be deployed." + } + }, + "deployKeyVault": { + "type": "bool", + "metadata": { + "description": "If set to true, a key vault and user assigned managed identity will be deployed with the specified names." + } + }, + "keyVaultName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the key vault to be deployed." + } + }, + "keyVaultUserAssignedIdentityName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the user-assigned managed identity to be deployed for accessing the key vault." + } + } + }, + "variables": { + "kvSecretUserRole": "4633458b-17de-408a-b874-0445c86b69e6" + }, + "resources": [ + { + "condition": "[parameters('deployTemplateStore')]", + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2022-09-01", + "name": "[if(parameters('deployTemplateStore'), parameters('templateStorageAccountName'), 'default')]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": {} + }, + { + "condition": "[parameters('deployTemplateStore')]", + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2022-09-01", + "name": "[format('{0}/{1}', if(parameters('deployTemplateStore'), parameters('templateStorageAccountName'), 'default'), 'default')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', if(parameters('deployTemplateStore'), parameters('templateStorageAccountName'), 'default'))]" + ] + }, + { + "condition": "[parameters('deployTemplateStore')]", + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2022-09-01", + "name": "[format('{0}/{1}/{2}', if(parameters('deployTemplateStore'), parameters('templateStorageAccountName'), 'default'), 'default', if(parameters('deployTemplateStore'), parameters('templateStorageAccountContainerName'), 'default'))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', if(parameters('deployTemplateStore'), parameters('templateStorageAccountName'), 'default'), 'default')]" + ] + }, + { + "condition": "[parameters('deployKeyVault')]", + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2021-04-01-preview", + "name": "[if(parameters('deployKeyVault'), parameters('keyVaultName'), 'default')]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "family": "A", + "name": "standard" + }, + "tenantId": "[subscription().tenantId]", + "enableRbacAuthorization": true + } + }, + { + "condition": "[parameters('deployKeyVault')]", + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[if(parameters('deployKeyVault'), parameters('keyVaultUserAssignedIdentityName'), 'default')]", + "location": "[parameters('location')]" + }, + { + "condition": "[parameters('deployKeyVault')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.KeyVault/vaults/{0}', if(parameters('deployKeyVault'), parameters('keyVaultName'), 'default'))]", + "name": "[guid(resourceGroup().id, resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', if(parameters('deployKeyVault'), parameters('keyVaultUserAssignedIdentityName'), 'default')), variables('kvSecretUserRole'))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('kvSecretUserRole'))]", + "principalId": "[if(parameters('deployKeyVault'), reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', if(parameters('deployKeyVault'), parameters('keyVaultUserAssignedIdentityName'), 'default')), '2023-01-31').principalId, 'default')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', if(parameters('deployKeyVault'), parameters('keyVaultName'), 'default'))]", + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', if(parameters('deployKeyVault'), parameters('keyVaultUserAssignedIdentityName'), 'default'))]" + ] + } + ], + "outputs": { + "templateStorageAccountName": { + "type": "string", + "value": "[if(parameters('deployTemplateStore'), if(parameters('deployTemplateStore'), parameters('templateStorageAccountName'), 'default'), '')]" + }, + "templateStorageAccountContainerName": { + "type": "string", + "value": "[if(parameters('deployTemplateStore'), if(parameters('deployTemplateStore'), parameters('templateStorageAccountContainerName'), 'default'), '')]" + }, + "keyVaultName": { + "type": "string", + "value": "[if(parameters('deployKeyVault'), if(parameters('deployKeyVault'), parameters('keyVaultName'), 'default'), '')]" + }, + "keyVaultUAMIName": { + "type": "string", + "value": "[if(parameters('deployKeyVault'), if(parameters('deployKeyVault'), parameters('keyVaultUserAssignedIdentityName'), 'default'), '')]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('resourceGroupName'))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('convertInfrastructureDeploy_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "envName": { + "value": "[parameters('containerAppEnvName')]" + }, + "deployApplicationInsights": { + "value": "[parameters('applicationInsightsEnabled')]" + }, + "keyVaultName": { + "value": "[parameters('keyVaultName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.26.54.24096", + "templateHash": "5993618804873826652" + } + }, + "parameters": { + "location": { + "type": "string", + "allowedValues": [ + "australiaeast", + "brazilsouth", + "canadacentral", + "canadaeast", + "centralindia", + "centralus", + "chinanorth3", + "eastasia", + "eastus", + "eastus2", + "francecentral", + "germanywestcentral", + "japaneast", + "koreacentral", + "northcentralus", + "northeurope", + "norwayeast", + "southafricanorth", + "southcentralus", + "southeastasia", + "swedencentral", + "switzerlandnorth", + "uaenorth", + "uksouth", + "ukwest", + "westeurope", + "westus", + "westus2", + "westus3" + ], + "metadata": { + "description": "Location where the resources are deployed. For list of Azure regions where the below resources are available, see [Products available by region](https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/?products=monitor,container-apps)." + } + }, + "envName": { + "type": "string", + "metadata": { + "description": "Name of the container apps environment." + } + }, + "deployApplicationInsights": { + "type": "bool", + "metadata": { + "description": "If set to true, Application Insights logs and metrics collection will be enabled for the container app." + } + }, + "keyVaultName": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "The name of the Key Vault to store the Application Insights connection string secret." + } + } + }, + "variables": { + "logAnalyticsWorkspaceName": "[format('{0}-logsws', parameters('envName'))]", + "applicationInsightsName": "[format('{0}-ai', parameters('envName'))]", + "applicationInsightsConnectionStringSecretName": "[format('{0}-connection-string', variables('applicationInsightsName'))]", + "applicationInsightsUAMIName": "[format('{0}-mi', variables('applicationInsightsName'))]", + "monitoringMetricsPublisherRoleDefinition": "3913510d-42f4-4e42-8a64-420c390055eb", + "containerAppEnvironmentName": "[parameters('envName')]" + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2020-03-01-preview", + "name": "[variables('logAnalyticsWorkspaceName')]", + "location": "[parameters('location')]", + "properties": { + "retentionInDays": 30, + "features": { + "searchVersion": 1 + }, + "sku": { + "name": "PerGB2018" + } + } + }, + { + "condition": "[parameters('deployApplicationInsights')]", + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[if(parameters('deployApplicationInsights'), variables('applicationInsightsName'), 'default')]", + "location": "[parameters('location')]", + "kind": "web", + "properties": { + "Application_Type": "web", + "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsWorkspaceName'))]", + "DisableLocalAuth": true + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsWorkspaceName'))]" + ] + }, + { + "condition": "[parameters('deployApplicationInsights')]", + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-04-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), variables('applicationInsightsConnectionStringSecretName'))]", + "properties": { + "value": "[if(parameters('deployApplicationInsights'), reference(resourceId('Microsoft.Insights/components', if(parameters('deployApplicationInsights'), variables('applicationInsightsName'), 'default')), '2020-02-02').ConnectionString, 'default')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/components', if(parameters('deployApplicationInsights'), variables('applicationInsightsName'), 'default'))]" + ] + }, + { + "condition": "[parameters('deployApplicationInsights')]", + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[variables('applicationInsightsUAMIName')]", + "location": "[parameters('location')]" + }, + { + "condition": "[parameters('deployApplicationInsights')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, resourceId('Microsoft.Insights/components', if(parameters('deployApplicationInsights'), variables('applicationInsightsName'), 'default')), variables('monitoringMetricsPublisherRoleDefinition'))]", + "properties": { + "principalId": "[if(parameters('deployApplicationInsights'), reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('applicationInsightsUAMIName')), '2023-01-31').principalId, 'default')]", + "principalType": "ServicePrincipal", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('monitoringMetricsPublisherRoleDefinition'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/components', if(parameters('deployApplicationInsights'), variables('applicationInsightsName'), 'default'))]", + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('applicationInsightsUAMIName'))]" + ] + }, + { + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2023-05-01", + "name": "[variables('containerAppEnvironmentName')]", + "location": "[parameters('location')]", + "properties": { + "appLogsConfiguration": { + "destination": "log-analytics", + "logAnalyticsConfiguration": { + "customerId": "[reference(resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsWorkspaceName')), '2020-03-01-preview').customerId]", + "sharedKey": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsWorkspaceName')), '2020-03-01-preview').primarySharedKey]" + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsWorkspaceName'))]" + ] + } + ], + "outputs": { + "containerAppEnvironmentName": { + "type": "string", + "value": "[variables('containerAppEnvironmentName')]" + }, + "containerAppEnvironmentId": { + "type": "string", + "value": "[resourceId('Microsoft.App/managedEnvironments', variables('containerAppEnvironmentName'))]" + }, + "logAnalyticsWorkspaceName": { + "type": "string", + "value": "[variables('logAnalyticsWorkspaceName')]" + }, + "applicationInsightsUAMIName": { + "type": "string", + "value": "[if(parameters('deployApplicationInsights'), variables('applicationInsightsUAMIName'), '')]" + }, + "applicationInsightsConnStringSecretName": { + "type": "string", + "value": "[if(parameters('deployApplicationInsights'), variables('applicationInsightsConnectionStringSecretName'), '')]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('resourceGroupName'))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('fhirConverterDeploy_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "imageTag": { + "value": "[parameters('containerAppImageTag')]" + }, + "appName": { + "value": "[parameters('containerAppName')]" + }, + "envName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', format('convertInfrastructureDeploy_{0}', parameters('timestamp'))), '2022-09-01').outputs.containerAppEnvironmentName.value]" + }, + "minReplicas": { + "value": "[parameters('minReplicas')]" + }, + "maxReplicas": { + "value": "[parameters('maxReplicas')]" + }, + "cpuLimit": { + "value": "[parameters('cpuLimit')]" + }, + "memoryLimit": { + "value": "[parameters('memoryLimit')]" + }, + "securityEnabled": { + "value": "[parameters('securityEnabled')]" + }, + "securityAuthenticationAudiences": { + "value": "[parameters('securityAuthenticationAudiences')]" + }, + "securityAuthenticationAuthority": { + "value": "[parameters('securityAuthenticationAuthority')]" + }, + "templateStorageAccountName": "[if(parameters('templateStoreIntegrationEnabled'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', format('dependentResourceDeploy_{0}', parameters('timestamp'))), '2022-09-01').outputs.templateStorageAccountName.value), createObject('value', ''))]", + "templateStorageAccountContainerName": "[if(parameters('templateStoreIntegrationEnabled'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', format('dependentResourceDeploy_{0}', parameters('timestamp'))), '2022-09-01').outputs.templateStorageAccountContainerName.value), createObject('value', ''))]", + "keyVaultName": "[if(variables('deployKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', format('dependentResourceDeploy_{0}', parameters('timestamp'))), '2022-09-01').outputs.keyVaultName.value), createObject('value', ''))]", + "keyVaultUAMIName": "[if(variables('deployKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', format('dependentResourceDeploy_{0}', parameters('timestamp'))), '2022-09-01').outputs.keyVaultUAMIName.value), createObject('value', ''))]", + "applicationInsightsUAMIName": "[if(parameters('applicationInsightsEnabled'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', format('convertInfrastructureDeploy_{0}', parameters('timestamp'))), '2022-09-01').outputs.applicationInsightsUAMIName.value), createObject('value', ''))]", + "applicationInsightsConnectionStringSecretName": "[if(parameters('applicationInsightsEnabled'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', format('convertInfrastructureDeploy_{0}', parameters('timestamp'))), '2022-09-01').outputs.applicationInsightsConnStringSecretName.value), createObject('value', ''))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.26.54.24096", + "templateHash": "2888348515167810107" + } + }, + "parameters": { + "location": { + "type": "string", + "allowedValues": [ + "australiaeast", + "brazilsouth", + "canadacentral", + "canadaeast", + "centralindia", + "centralus", + "chinanorth3", + "eastasia", + "eastus", + "eastus2", + "francecentral", + "germanywestcentral", + "japaneast", + "koreacentral", + "northcentralus", + "northeurope", + "norwayeast", + "southafricanorth", + "southcentralus", + "southeastasia", + "swedencentral", + "switzerlandnorth", + "uaenorth", + "uksouth", + "ukwest", + "westcentralus", + "westeurope", + "westus", + "westus2", + "westus3" + ], + "metadata": { + "description": "Location where the resources are deployed. Note that a Container App can only be provisioned in the same region as its parent Container Apps Environment. For list of Azure regions where Container Apps is available, see [Products available by region](https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/?products=container-apps)" + } + }, + "appName": { + "type": "string", + "metadata": { + "description": "The name of the container app running the FHIR-Converter service." + } + }, + "envName": { + "type": "string", + "metadata": { + "description": "The name of the container apps environment where the app will run." + } + }, + "templateStorageAccountName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of storage account containing custom templates. Leave blank if using default templates." + } + }, + "templateStorageAccountContainerName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the container in the storage account containing custom templates. Leave blank if using default templates." + } + }, + "keyVaultName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the key vault containing the application insights connection string secret." + } + }, + "keyVaultUAMIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the user-assigned managed identity to be used by the container app to access key vault secrets." + } + }, + "minReplicas": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Minimum possible number of replicas per revision as the container app scales." + } + }, + "maxReplicas": { + "type": "int", + "defaultValue": 30, + "metadata": { + "description": "Maximum possible number of replicas per revision as the container app scales." + } + }, + "cpuLimit": { + "type": "string", + "defaultValue": "1.0", + "metadata": { + "description": "CPU usage limit in cores." + } + }, + "memoryLimit": { + "type": "string", + "defaultValue": "2Gi", + "metadata": { + "description": "Memory usage limit in Gi." + } + }, + "securityEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "If set to true, security will be enabled on the API endpoint." + } + }, + "securityAuthenticationAudiences": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "List of audiences that the authentication token is intended for." + } + }, + "securityAuthenticationAuthority": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Issuing authority of the JWT token." + } + }, + "imageTag": { + "type": "string", + "metadata": { + "description": "Tag of the image to deploy. To see available image versions, visit the [FHIR Converter MCR page](https://mcr.microsoft.com/en-us/product/healthcareapis/fhir-converter/tags)" + } + }, + "timestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyyMMddHHmmss')]", + "metadata": { + "description": "Timestamp to append to container name. Defaults to time of deployment." + } + }, + "applicationInsightsUAMIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The ID of the user-assigned managed identity to be used by the container app to access application insights." + } + }, + "applicationInsightsConnectionStringSecretName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the secret in the key vault containing the application insights connection string." + } + } + }, + "variables": { + "copy": [ + { + "name": "securityAuthenticationAudiencesConfig", + "count": "[length(parameters('securityAuthenticationAudiences'))]", + "input": { + "name": "[format('{0}{1}', variables('securityAuthenticationAudiencesConfigNamePrefix'), copyIndex('securityAuthenticationAudiencesConfig'))]", + "value": "[parameters('securityAuthenticationAudiences')[copyIndex('securityAuthenticationAudiencesConfig')]]" + } + } + ], + "configureApplicationInsights": "[not(empty(parameters('applicationInsightsUAMIName')))]", + "securityEnabledConfigName": "ConvertService__Security__Enabled", + "securityAuthenticationAudiencesConfigNamePrefix": "ConvertService__Security__Authentication__Audiences__", + "securityAuthenticationAuthorityConfigName": "ConvertService__Security__Authentication__Authority", + "securityEnabledConfiguration": [ + { + "name": "[variables('securityEnabledConfigName')]", + "value": "[string(parameters('securityEnabled'))]" + } + ], + "securityAuthenticationAuthorityConfig": [ + { + "name": "[variables('securityAuthenticationAuthorityConfigName')]", + "value": "[parameters('securityAuthenticationAuthority')]" + } + ], + "securityConfiguration": "[concat(variables('securityEnabledConfiguration'), if(parameters('securityEnabled'), concat(variables('securityAuthenticationAuthorityConfig'), variables('securityAuthenticationAudiencesConfig')), createArray()))]", + "integrateTemplateStore": "[and(not(empty(parameters('templateStorageAccountName'))), not(empty(parameters('templateStorageAccountContainerName'))))]", + "storageEnvironmentSuffix": "[environment().suffixes.storage]", + "blobTemplateHostingConfigurationName": "TemplateHosting__StorageAccountConfiguration__ContainerUrl", + "blobTemplateHostingConfigurationValue": "[format('https://{0}.blob.{1}/{2}', parameters('templateStorageAccountName'), variables('storageEnvironmentSuffix'), parameters('templateStorageAccountContainerName'))]", + "blobTemplateHostingConfiguration": "[if(variables('integrateTemplateStore'), createArray(createObject('name', variables('blobTemplateHostingConfigurationName'), 'value', variables('blobTemplateHostingConfigurationValue'))), createArray())]", + "applicationInsightsConnectionStringConfigurationName": "ConvertService__Telemetry__AzureMonitor__ApplicationInsightsConnectionString", + "applicationInsightsUAMIClientIdConfigurationName": "ConvertService__Telemetry__AzureMonitor__ManagedIdentityClientId", + "imageName": "healthcareapis/fhir-converter", + "applicationInsightsUAMIResourceId": "[if(variables('configureApplicationInsights'), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('applicationInsightsUAMIName')), '')]", + "keyVaultUAMIResourceId": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('keyVaultUAMIName'))]", + "userAssignedIdentities": "[if(variables('configureApplicationInsights'), createObject(format('{0}', variables('applicationInsightsUAMIResourceId')), createObject(), format('{0}', variables('keyVaultUAMIResourceId')), createObject()), createObject())]", + "akvEnvironmentSuffix": "[environment().suffixes.keyvaultDns]", + "applicationInsightsConnStringAKVSecretUrl": "[format('https://{0}{1}/secrets/{2}', parameters('keyVaultName'), variables('akvEnvironmentSuffix'), parameters('applicationInsightsConnectionStringSecretName'))]", + "roleAssignmentName": "[guid(resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', parameters('templateStorageAccountName'), 'default', parameters('templateStorageAccountContainerName')), parameters('appName'), variables('storageBlobDataReaderRoleDefinitionId'))]", + "storageBlobDataReaderRoleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1')]" + }, + "resources": [ + { + "type": "Microsoft.App/containerApps", + "apiVersion": "2023-05-01", + "name": "[parameters('appName')]", + "location": "[parameters('location')]", + "identity": "[if(variables('configureApplicationInsights'), createObject('type', 'SystemAssigned, UserAssigned', 'userAssignedIdentities', variables('userAssignedIdentities')), createObject('type', 'SystemAssigned'))]", + "properties": { + "managedEnvironmentId": "[resourceId('Microsoft.App/managedEnvironments', parameters('envName'))]", + "configuration": { + "ingress": { + "targetPort": 8080, + "external": true + }, + "secrets": "[if(variables('configureApplicationInsights'), createArray(createObject('name', parameters('applicationInsightsConnectionStringSecretName'), 'keyVaultUrl', variables('applicationInsightsConnStringAKVSecretUrl'), 'identity', variables('keyVaultUAMIResourceId'))), createArray())]" + }, + "template": { + "containers": [ + { + "image": "[format('mcr.microsoft.com/{0}:{1}', variables('imageName'), parameters('imageTag'))]", + "name": "[format('fhir-converter-{0}', parameters('timestamp'))]", + "env": "[concat(variables('securityConfiguration'), if(variables('configureApplicationInsights'), createArray(createObject('name', variables('applicationInsightsConnectionStringConfigurationName'), 'secretRef', parameters('applicationInsightsConnectionStringSecretName')), createObject('name', variables('applicationInsightsUAMIClientIdConfigurationName'), 'value', reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('applicationInsightsUAMIName')), '2023-01-31').clientId)), createArray()), variables('blobTemplateHostingConfiguration'))]", + "resources": { + "cpu": "[json(parameters('cpuLimit'))]", + "memory": "[parameters('memoryLimit')]" + } + } + ], + "scale": { + "minReplicas": "[parameters('minReplicas')]", + "maxReplicas": "[parameters('maxReplicas')]" + } + } + }, + "tags": { + "fhirConverterEnvName": "[parameters('envName')]", + "fhirConverterAppName": "[parameters('appName')]", + "fhirConverterImageName": "[variables('imageName')]", + "fhirConverterImageVersion": "[parameters('imageTag')]" + } + }, + { + "condition": "[variables('integrateTemplateStore')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}/containers/{2}', parameters('templateStorageAccountName'), 'default', parameters('templateStorageAccountContainerName'))]", + "name": "[guid(variables('roleAssignmentName'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.App/containerApps', parameters('appName')), '2023-05-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal", + "roleDefinitionId": "[variables('storageBlobDataReaderRoleDefinitionId')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.App/containerApps', parameters('appName'))]" + ] + } + ], + "outputs": { + "containerAppFQDN": { + "type": "string", + "value": "[reference(resourceId('Microsoft.App/containerApps', parameters('appName')), '2023-05-01').configuration.ingress.fqdn]" + }, + "containerAppLatestRevisionName": { + "type": "string", + "value": "[reference(resourceId('Microsoft.App/containerApps', parameters('appName')), '2023-05-01').latestRevisionName]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', format('convertInfrastructureDeploy_{0}', parameters('timestamp')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', format('dependentResourceDeploy_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('resourceGroupName'))]" + ] + } + ], + "outputs": { + "fhirConverterApiEndpoint": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', format('fhirConverterDeploy_{0}', parameters('timestamp'))), '2022-09-01').outputs.containerAppFQDN.value]" + }, + "resourceGroupName": { + "type": "string", + "value": "[parameters('resourceGroupName')]" + } + } +} \ No newline at end of file diff --git a/docs/how-to-guides/configuration-settings.md b/docs/how-to-guides/configuration-settings.md new file mode 100644 index 000000000..44ccae978 --- /dev/null +++ b/docs/how-to-guides/configuration-settings.md @@ -0,0 +1,103 @@ +# Configure FHIR converter service settings + +This how-to-guide explains how to configure settings for the FHIR converter service, if the [default configuration](deployment-options.md#default-settings) does not suit your requirements. + +## Authentication + +To ensure restricted access to your FHIR converter APIs, allowing only tokens issued from within your tenant to be able to interact with the APIs, the FHIR converter service can be configured with authentication settings enabled. Refer [Enable Authentication](enable-authentication.md) for detailed instructions on configuring your FHIR converter service with authentication settings. + +The following are sample deployment commands for configuring authentication, using deployment option 2 or 3 (as described in [Deployment options to set up FHIR converter service in Azure](deployment-options.md)): + +Option 2 (bicep): +``` +az deployment sub create --location --name --template-file FhirConverter-SingleAzureDeploy.bicep --parameters securityEnabled=true securityAuthenticationAudiences="['','','']" securityAuthenticationAuthority="" +``` + +Option 3 (PowerShell): +```PowerShell +./Deploy-FhirConverterService.ps1 -securityEnabled $true -securityAuthenticationAudiences @('','','') -securityAuthenticationAuthority "" +``` + +## Template store integration + +The FHIR converter APIs come pre-packaged with [default Liquid templates](https://github.com/microsoft/FHIR-Converter/tree/main/data/Templates) for the supported conversion scenarios. + +However, to allow the ability to use custom Liquid templates for custom transformation requirements (see [Customize templates](customize-templates.md) to learn more about how to customize templates), your FHIR converter service can be configured to integrate with your template store. + +Refer [Enable template store integration](enable-template-store-integration.md) for detailed instructions on configuring your FHIR converter service with your custom template store. + +The following are sample deployment commands for configuring template store integration, using deployment option 2 or 3, as described in [Deployment options to set up FHIR converter service in Azure](deployment-options.md): + +Option 2 (bicep): +``` +az deployment sub create --location --name --template-file FhirConverter-SingleAzureDeploy.bicep --parameters templateStoreIntegrationEnabled=true templateStorageAccountName="" templateStorageAccountContainerName="" +``` + +Option 3 (PowerShell): +```PowerShell +./Deploy-FhirConverterService.ps1 -templateStoreIntegrationEnabled $true -templateStorageAccountName "" -templateStorageAccountContainerName "" +``` + +## Monitoring + +The FHIR converter service emits custom logs and metrics to provide information on your service and invocation of the conversion APIs, that could be used for insights or troubleshooting. + +These can be viewed using Azure Container App's Azure Monitor Log Analytics. +Refer [Monitoring](monitoring.md) for more information on how to view logs and metrics emitted by your FHIR converter service. + +Additionally, your FHIR converter service can be configured with an Application Insights resource, which allows you to visualize the custom metrics emitted in graphical format. +If you deployed the service using the quickstart deployment options, Application Insights is deployed by default and configured with your FHIR converter service. +Alternatively, to provide your own Application Insights resource to collect the telemetry for your service, (**TODO** instructions). + +Refer [Application Insights Overview](https://docs.microsoft.com/azure/azure-monitor/app/app-insights-overview) to learn how to customize Application Insights for your requirements. + +Application Insights is enabled by default when deploying the FHIR converter using any of the 3 deployment options described in [Deployment options to set up FHIR converter service in Azure](deployment-options.md). The following are sample deployment commands for **disabling** Application Insights, using deployment option 2 or 3: + +Option 2 (bicep): +``` +az deployment sub create --location --name --template-file FhirConverter-SingleAzureDeploy.bicep --parameters applicationInsightsEnabled=false +-Container_name>" +``` + +Option 3 (PowerShell): +```PowerShell +./Deploy-FhirConverterService.ps1 -applicationInsightsEnabled $false +``` + +## Additional configurations + +### Azure Container App + +The provided [deployment options](deployment-options.md) set up the FHIR converter service to run on an Azure Container App, which is configured with basic settings intended for testing. +Azure Container Apps offers various configurable options for your app, that you can update to better suit your requirements: + +* To manage hardware requirements that meet your workload requirements, refer [Workload profiles](https://learn.microsoft.com/en-us/azure/container-apps/workload-profiles-overview). +* To manage automatic scaling of your service, refer [Scaling & performance](https://learn.microsoft.com/en-us/azure/container-apps/scale-app?pivots=azure-cli). +* To manage ingress of your service and advanced networking configurations, refer [Networking, ingress, and network security](https://learn.microsoft.com/en-us/azure/container-apps/networking?tabs=workload-profiles-env%2Cazure-cli). + +The following are sample deployment commands for customizing your Container App settings, using deployment options 2 and 3, as described in [Deployment options to set up FHIR converter service in Azure](deployment-options.md): + +Option 2 (bicep): +``` +az deployment sub create --location --name --template-file FhirConverter-SingleAzureDeploy.bicep --parameters containerAppName="" minReplicas="" maxReplicas="" cpuLimit="" memoryLimit="memory_unit" +-Container_name>" +``` + +Option 3 (PowerShell): +```PowerShell +./Deploy-FhirConverterService.ps1 -containerAppName "" -minReplicas "" -maxReplicas "" -cpuLimit "" -memoryLimit "" +``` + +## Summary + +In this how-to-guide, you learned how to configure your FHIR converter service in Azure, with your desired settings. + +Once the service is set up, you can use the endpoint corresponding to the application url of your Container App running the web service. + +To get started using your newly deployed FHIR converter service, refer to the following documents: + +* [Customize Liquid templates](customize-templates.md) +* [Use FHIR converter APIs](use-convert-web-apis.md) +* [Monitor FHIR converter service](monitoring.md) +* [Troubleshooting guide](troubleshoot.md) diff --git a/docs/how-to-guides/customize-templates.md b/docs/how-to-guides/customize-templates.md new file mode 100644 index 000000000..3298f6f79 --- /dev/null +++ b/docs/how-to-guides/customize-templates.md @@ -0,0 +1,138 @@ +# Customize Liquid Templates +This how-to-guide shows you how to customize the liquid templates for the FHIR Converter service. + +The default templates for the FHIR Converter service are located [here](https://github.com/microsoft/FHIR-Converter/tree/main/data/), and are a recommended starting point for creating customized templates. + +The templates use the liquid template language, which is documented [here](https://shopify.github.io/liquid/). + +## Transforming data to FHIR + +### HL7v2 to FHIR +When creating a custom template for HL7v2 to FHIR, it may be simplest to start creating a template that generates a FHIR bundle with only the FHIR resource(s) needed for your use case. The default templates iterate through the HL7v2 segments and make a best effort to generate FHIR resources as applicable, but for starting a custom template for an HL7v2 ADT_A01 message, for example, you may want to focus on generating resources like Patient, Encounter, and Coverage, and then later add additional resources as needed. + +This [page](https://github.com/microsoft/FHIR-Converter/blob/main/docs/HL7v2-templates.md) contains information on what segments are mapped for each HL7v2 message type when using the default templates. You may want to decide on what is necessary to include in the output FHIR bundle, and then remove any content from the default template that is not necessary for your use case. + +In addition, there are some pages dedicated to HL7v2 to FHIR reusable snippets, and HL7v2 to FHIR important points, which may be useful when creating custom templates. + - [Snippet concepts](https://github.com/microsoft/FHIR-Converter/blob/shared/convert-api-documentation-update/docs/SnippetConcept.md) + - [HL7v2 to FHIR important points](https://github.com/microsoft/FHIR-Converter/blob/shared/convert-api-documentation-update/docs/HL7v2-ImportantPoints.md) + +### CCD to FHIR +The default templates are a good starting point for creating a custom CCD transformation template. A FHIR bundle is created by iterating over the common CCD sections in this [template](https://github.com/microsoft/FHIR-Converter/blob/main/data/Templates/Ccda/CCD.liquid). As a starting point, you can start with the CCD sections that are needed and then modify the subtemplates referenced by this root template. + +### JSON to FHIR +There are some [sample templates](https://github.com/microsoft/FHIR-Converter/tree/main/data/Templates/Json) that map JSON values to a Patient resource. + +## Transforming from FHIR to HL7v2 +The documentation for HL7v2 message structures can be found on https://www.hl7.org/. + +When transforming FHIR to HL7v2, the templates are written such that the input FHIR resource is transformed into a JSON representation of an HL7v2 message. When the /convertToHl7v2 API is called, the API will transform the input FHIR resource into the JSON representation of an HL7 message. But before the result is returned to the user, the JSON representation of an HL7v2 message is transformed into the normal HL7v2 format with line breaks, and is returned to the user. + +To represent HL7v2 in JSON, the transformation uses an array called messageDefinition, which is an ordered array of HL7v2 message segments. + +Below is an overview of how an HL7v2 message is represented in JSON. Each HL7v2 segment is an object, where the key of the object is the name of the segment. Each HL7v2 segment object has keys which represent the HL7v2 field numbers and the HL7v2 field values. +```json +{ + "messageDefinition": [ + { + "MSH": + { + "fieldNumber": "data resolution code" + }, + "PID": + { + "fieldNumber": "data resolution code", + "fieldNumber": "data resolution code", + "fieldNumber": "data resolution code" + }, + "PV1": + { + "fieldNumber": "data resolution code" + } + } + ] +} +``` + +Based on this example JSON representation of an HL7v2 message above, this is a sample transformation that iterates over Observation resources in the FHIR Bundle, and creates HL7v2 ORU^R01 messages for each observation. Note: for demonstration purposes only a few HL7v2 fields are populated. + +```json +{ + "messageDefinition": [ + { + "FHS": { + "2": "^~\\&", + "3": "TestSystem" + } + }, + { + "BHS": { + "2": "^~\\&", + "3": "TestSystem" + } + }, + + {% for entry in msg.entry %} + {% if entry.resource.resourceType == "Observation" %} + + {% assign patient_reference = msg | to_json_string | get_object: "$.entry[?(@resource.resourceType == 'Patient')].resource" %} + + { + "MSH": { + "2": "^~\\&", + "3": "TestSystem", + "4": "", + "5": "TransformationAgent", + "6": "", + "7": "123", + "8": "", + "9": "ORU^R01", + "10": "1", + "11": "T", + "12": "2.5", + } + }, + { + "PID": { + "3": {{ patient_reference | evaluate: "$.identifier[0].value" }} + } + }, + { + "OBR": { + "2": "{{entry.resource.identifier[0].value}}", + "3": "{{entry.resource.code.coding[0].code}}^{{entry.resource.code.coding[0].display}}^LN", + + {% if entry.resource.status == "final" %} + "25": "F" + {% endif %} + } + }, + { + "OBX": { + "5": "{{entry.resource.valueQuantity.value}}", + "6": "{{entry.resource.valueQuantity.unit}}", + "7": "{{entry.resource.referenceRange[0].low.value}}-{{entry.resource.referenceRange[0].high.value}}", + "8": "{{entry.resource.interpretation[0].coding[0].code}}" + } + }, + {% endif %} + {% endfor %} + + { + "BTS": { + "1": "1" + } + }, + { + "FTS": { + "1": "1" + } + }] +} +``` + +## Liquid Filters +Since the templates are written in the liquid templating language, there are a number of [liquid filters available](https://shopify.github.io/liquid/) which can assist with modifying string values. + +There are also some useful [filters and tags](https://github.com/microsoft/FHIR-Converter/blob/b3e36d918bb67de8d3775d55b1159ee26492bde2/docs/Filters-and-Tags.md) definited in the FHIR-Converter project that can be used in templates as well. + + diff --git a/docs/how-to-guides/deployment-options.md b/docs/how-to-guides/deployment-options.md new file mode 100644 index 000000000..438422ddc --- /dev/null +++ b/docs/how-to-guides/deployment-options.md @@ -0,0 +1,170 @@ +# Deployment options to set up FHIR converter service in Azure + +This article details various deployment options for provisioning a FHIR converter service in Azure using the [MCR container image](https://mcr.microsoft.com/en-us/product/healthcareapis/fhir-converter/tags). + +The following Azure resources will be provisioned once the deployment has completed: + +* 1 x Container Apps Environment +* 1 x Azure Container App +* 1 x Log Analytics Workspace +* 1 x App Insights +* 1 x Storage Account +* 1 x Key Vault + +![Resources](../images/provisioned-resources.png) + +## Prerequisites + +To run any of these deployment options, the following items must be set up before execution: + +* Contributor and User Access Administrator OR Owner permissions on your Azure subscription + +For local deployments (Options 2 and 3), the following additional steps must be performed: + +* Install the [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) module + +* Log into your Azure account: + +```PowerShell +az login +``` + +* If you have more than one subscription, select the subscription you would like to deploy to: + +```PowerShell +az account set --subscription +``` + +* Clone this repo and navigate to the Bicep deployment folder: + +```PowerShell +git clone https://github.com/microsoft/FHIR-Converter.git +cd docs/deploy +``` + +## Deployment + +### Deployment settings + +The deployment options below provide a quickstart version which will set up your service with the default configuration, which is typically intended for testing or initial setup. The deployment options also allow for specifying specific configurations as needed for your service, during deployment. + +Note: You are also able to update the service configuration post initial deployment, by redeploying with the updated settings. + +#### Default settings + +* **Security** - Security settings for the API endpoints are disabled by default. It is **strongly recommended** to enable security for your FHIR converter service. +* **Template store integration** - Template store integration is disabled by default. When template store integration is disabled, a Storage Account will not be provisioned with the deployment. +* **Default templates only** - When template store integration is disabled, the FHIR converter service will only have access to the provided default templates, with no access to custom templates. +* **Application Insights** - Application Insights is enabled by default. Application Insights will receive application logs and metrics for the FHIR converter service to be used for debugging/monitoring. + +See [Parameters](#parameters) for details on the parameters that can be use to configure the deployment. + +#### Configurable settings + +See [Parameters](#parameters) for details on the parameters that can be used to configure the deployment. To learn more about the various options available to customize your service, and to configure the settings of your FHIR converter service in Azure, refer [Configure FHIR converter service settings](configuration-settings.md). + +#### Parameters + +The table below outlines the parameters that can be configured through any of the 3 deployment options: + +| Parameter | Type | Required | Description | Default Value | +| --- | --- | --- | --- | --- | +| serviceName | string | Yes | Used to generate a name for each of the resources provisioned wherever a name is not specified. | N/A | +| containerAppImageTag | string | Yes | The tag of the FHIR converter image version to be pulled from MCR. Visit the [FHIR converter MCR page](https://mcr.microsoft.com/en-us/product/healthcareapis/fhir-converter/tags) to see available image tags. | N/A | +| location | string | Yes | The Azure region where the resources will be deployed. See [region availability](https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/?products=key-vault,monitor,storage,container-apps) for the relevant resources to select a valid location. | N/A | +| timestamp | string | No | A timestamp to append to each deployment name to make it unique. | current date-time in 'yyyyMMddHHmmss' format | +| resourceGroupName | string | No | The name of the Resource Group where the resources will be deployed. | serviceName + "-rg" | +| containerAppEnvName | string | No | The name of the Container Apps environment. | serviceName + "-app-env" | +| containerAppName | string | No | The name of the Azure Container App to run the FHIR converter. | serviceName + "-app" | +| minReplicas | int | No | The minimum number of replicas for the Azure Container App. | 0 | +| maxReplicas | int | No | The maximum number of replicas for the Azure Container App. | 30 | +| cpuLimit | string | No | The CPU limit for the Azure Container App. | "1.0" | +| memoryLimit | string | No | The memory limit for the Azure Container App. | "2Gi" | +| templateStoreIntegrationEnabled | boolean | No | Enable or disable template store integration for the FHIR converter service. If disabled, only default templates can be used for conversion. If enabled, the necessary storage resources will be deployed only custom templates can be used. | false | +| templateStorageAccountName | string | No | The name of the Storage Account to store custom templates. | serviceName + "templatestorage" | +| templateStorageAccountContainerName | string | No | The name of the Storage Blob Container to store custom templates. | serviceName + "templatecontainer" | +| applicationInsightsEnabled | boolean | No | Enable or disable Application Insights deployment for your service. If enabled, the necessary resources for telemetry export will be deployed. Application metrics and request logs will be unavailable for any time period where Application Insights is disabled. | true | +| keyVaultName | string | No | The name of the Key Vault to store secrets. | serviceName + "-kv" | +| keyVaultUserAssignedIdentityName | string | No | The name of the User-Assigned Managed Identity to be used to access secrets in the Key Vault. | serviceName + "-kv-identity" | +| securityEnabled | boolean | No | Enable or disable security settings for the APIs. | false | +| securityAuthenticationAudiences | array of strings | No | The audiences for the API authentication. If securityEnabled is set to true, this parameter must have a value provided. | empty array | +| securityAuthenticationAuthority | string | No | The authority for the API authentication. If securityEnabled is set to true, this parameter must have a value provided. | empty string | + +### Deployment options + +#### Option 1: Single-click Deploy to Azure via ARM template generated from Bicep Template + +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2FFHIR-Converter%2Fshared%2Fconvert-api-documentation-update%2Fdocs%2Fdeploy%2FFhirConverter-SingleAzureDeploy.json) + +Click the button above to provision the FHIR converter via Azure Portal. This button deploys a [remote ARM template](https://github.com/microsoft/FHIR-Converter/blob/shared/convert-api-documentation-update/docs/deploy/FhirConverter-SingleAzureDeploy.json) generated from the [single Bicep template](https://github.com/microsoft/FHIR-Converter/blob/shared/convert-api-documentation-update/docs/deploy/FhirConverter-SingleAzureDeploy.bicep) entry point for provisioning all necessary Azure resources and role assignments in Option 2. + +Refer to [Configurable Settings](#configurable-settings) for more information on the required parameters to be provided and the default values used for optional parameters. + +#### Option 2: Deploy a single Bicep file locally + +Deploy the [Single Deploy Bicep Template](../deploy/FhirConverter-SingleAzureDeploy.bicep) with the default settings described above by running the following command: + +``` +az deployment sub create --location --name --template-file FhirConverter-SingleAzureDeploy.bicep +``` + +See the following command for sample syntax to use when customizing parameter values in your deployment: +``` +az deployment sub create --location --name --template-file FhirConverter-SingleAzureDeploy.bicep --parameters containerAppName="" +``` + +To see a description of a given parameter after being prompted to provide a value, type '?'. Refer to [Configurable Settings](#configurable-settings) for more information on the required parameters to be provided and the default values used for optional parameters. + +#### Option 3: Execute a single PowerShell deployment script locally + +Run the following command to run the PowerShell deployment script: + +```PowerShell +./Deploy-FhirConverterService.ps1 +``` + +See the following command for sample syntax to use when customizing parameter values in your deployment: + +```PowerShell +./Deploy-FhirConverterService.ps1 -containerAppName "" +``` + +This [PowerShell deployment script](../deploy/Deploy-FhirConverterService.ps1) sets up all necessary Azure resources for running the FHIR converter service by deploying Bicep templates via Azure CLI commands. + +Refer to [Configurable Settings](#configurable-settings) for more information on the required parameters to be provided and the default values used for optional parameters. + +### Redeployment scenarios + +The following scenarios will require a redeployment of your service using any one of the above options: + +* Update container image tag - If you intend to update your service to use the latest container image tag available, a redeployment will set up your service to pull the correct image tag specified. + +* Updated settings - If you choose to update any configuration, a redeployment is required for the changes to take effect. Some examples are: + + * Enable or disable authentication + * To update authentication audience/authority. + * Switch to default templates or custom templates usage. + * To update the Storage Account to pull custom templates from. + * Enable or disable Application Insights telemetry. + +* Custom template collection update - If you add/update any custom template in your Storage Account, a redeployment is required for the service to pick up the latest template collection and use that for conversion. You can redeploy using one of the options above, or manually restart your revision in the Azure Portal. To do the latter, go to your Container App's page in the Azure Portal, navigate to the "Revisions and replicas" blade under "Application", click on your revision name, and at the top of the "Revision Details" pane on the right, click "Restart". + +### Additional Deployment Notes + +* To view the progress of a deployment, navigate to the resource group in Azure Portal and select the 'Deployments' tab under 'Settings' in the left panel. + +* Container App supports [zero downtime deployment](https://learn.microsoft.com/en-us/azure/container-apps/revisions#zero-downtime-deployment). + + In case of updates to the service (image tag, configuration, etc.), a new container revision is created. If there are any issues in setting up the new revision, the endpoint is still available to use using the prior successfully provisioned revision. You can check the status and logs of the failing revision to debug issues with the deployment. Refer [Revisions](https://learn.microsoft.com/en-us/azure/container-apps/revisions) for more information. + +## Summary + +In this how-to-guide, you learned how to deploy your FHIR converter service in Azure. + +Once the deployment is complete, you can use the Azure Portal to navigate to the newly created Azure Container App to see the details of your service. +The default URL to access your FHIR converter service will be the application url of your Container App of the format:`https://...azurecontainerapps.io`. + +To get started with your newly deployed FHIR converter service, refer to the following documents: + +* [Configure FHIR converter service settings](configuration-settings.md) +* [Use FHIR converter APIs](use-convert-web-apis.md) diff --git a/docs/how-to-guides/enable-authentication.md b/docs/how-to-guides/enable-authentication.md new file mode 100644 index 000000000..9114b5bee --- /dev/null +++ b/docs/how-to-guides/enable-authentication.md @@ -0,0 +1,121 @@ +# Azure Active Directory Authentication + +This How-to-guide shows you how to configure the authentication settings for the FHIR converter service through Azure. This is needed to ensure restricted access to your FHIR converter APIs, allowing only tokens issued from within your tenant to be able to interact with the APIs. + +To complete this configuration, you will: + +1. **Create a resource application in Azure AD**: This resource application will be a representation of the FHIR converter service that can be used to authenticate and obtain tokens. In order for an application to interact with Azure AD, it needs to be registered. +1. **Provide app registration details to your FHIR converter web service**: Once the resource application is registered, you will set the authentication configuration of your FHIR converter web service. This ensures that any client that is able to authenticate with the above resource application will be able to access your FHIR converter APIs. +1. **Create a service client application in Azure AD**: Client application registrations are Azure AD representations of applications that can be used to authenticate and obtain tokens. A service client is intended to be used by an application to obtain an access token without interactive authentication of a user. It will have certain application permissions and use an application secret (password) when obtaining access tokens. +1. **Retrieve Access Token via Insomnia or Azure CLI**: With your service client application enabled, you can obtain an access token to authenticate your application. + +## Prerequisites + +1. This tutorial requires an Azure AD tenant. If you have not created a tenant, see [Create a new tenant in Azure Active Directory](https://docs.microsoft.com/azure/active-directory/fundamentals/active-directory-access-create-new-tenant). + +## Authentication Settings Overview + +The configurable authentication settings are : + +```json +{ + "ConvertService" : { + "Security": { + "Enabled": true, + "Authentication": { + "Audience": "", + "Authority": "" + } + } + } +} +``` + +| Element | Description | +| -------------------------- | --- | +| Enabled | Whether or not the service has any security enabled. | +| Authentication:Audience | Identifies the recipient that the token is intended for. | +| Authentication:Authority | The issuer of the jwt token. | + +## Authentication with Azure AD + +### Create a Resource Application in Azure AD for your FHIR converter service + +1. Sign into the [Azure Portal](https://ms.portal.azure.com/). +2. Select **Azure Active Directory** > **App Registrations** > **New registration**: + 1. Enter a **Name** for your app registration. + 2. To restrict access to APIs to only your tenant, select **Accounts in this organizational directory only (*tenant name* only - Single tenant)**. + 3. Select **Register**. +3. Select **Expose an API**. + 1. Application ID URI - **Add**. You can specify a URI or use the generated App ID URI. Select **Save**. + 2. Select **Add a Scope**: + 1. In **Scope name**, enter *user_impersonation*. + 2. In the text boxes, add an admin consent display name and admin consent description you want users to see on the consent page. For example, *access my app*. + +### Set the Authentication configuration of your FHIR converter service + +1. If you have deployed the FHIR converter service to Azure, provide the configuration: + * If you are using the [deployment options](deployment-options.md) to deploy your service, use the following parameters: + + (**TODO** add instructions for options) + + * Alternatively, you can directly provide the configuration via environment variables in your Azure Container App running the FHIR converter service by editing the container: + 1. **ConvertService__Security__Enabled** - True + 2. **ConvertService__Security__Authentication__Audiences__0** - the **Application ID URI** created above. + 3. **ConvertService__Security__Authentication__Authority** - the tenant your application exists in, for example: ```https://login.microsoftonline.com/.onmicrosoft.com``` or ```https://login.microsoftonline.com/```. + + Refer [Configure environment variables](https://learn.microsoft.com/en-us/azure/container-apps/environment-variables?tabs=portal) for more information. + + ![convertsecurityconfigaca](../images/convert-security-config.png) + +### Create a Service Client Application + +1. Select **Azure Active Directory** > **App Registrations** > **New registration**: + 1. Enter a **Name** for your service client. You can provide a **URI** but it typically will not be used. + 1. Select **Register**. +1. Copy the **Application (client) ID** and the **Directory (tenant) ID** for later. +1. Select **API Permissions** to provide your service client permission to your resource application: + 1. Select **Add a permission**. + 1. Under **My APIs**, select the resource application you created above for your FHIR converter service. + 1. Under **Select Permissions**, select the application roles from the ones that you defined on the resource application. + 1. Select **Add permissions**. +1. Select **Certificates & secrets** to generate a secret for obtaining tokens: + 1. Select **New client secret**. + 1. Provide a **Description** and duration of the secret. Select **Add**. + 1. Copy the secret once it has been created. It will only be displayed once in the portal. + +### Get Access Token + +#### Using Azure CLI + +1. First, update the application you create above to have access to the Azure CLI: + 1. Select **Expose an API** > **Add a Client Application**. + 1. For **Client ID**, provide the client ID of Azure CLI: **04b07795-8ddb-461a-bbee-02f9e1bf7b46**. *Note this is available at the [Azure CLI Github Repository](https://github.com/Azure/azure-cli/blob/24e0b9ef8716e16b9e38c9bb123a734a6cf550eb/src/azure-cli-core/azure/cli/core/_profile.py#L65)*. + 1. Select your **Application ID URI** under **Authorized Scopes**. + 1. Select **Add Application**. +1. [Install Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest). +1. Login to Azure: ```az account``` +1. Request access token using the **Application ID URI** set above: ```az account get-access-token --resource=``` + +#### Using Insomnia + +1. [Install Insomnia](https://insomnia.rest/). +1. Create a new **Post** Request or use the **OAuth 2** option in the Auth tab for the API request you wish to authenticate with and provide the access token for, with the following form-data: + 1. URL: ```//oauth2/token``` where **Authority** is the tenant your application exists in, configured above, and **Tenant ID** is from your Azure App Registration. + 1. If using Azure Active Directory V2 then instead use URL: ```//oauth2/v2.0/token```. + 1. *client_id*: the **Client ID** for your Service Client. + 1. *grant_type*: "client_credentials" + 1. *client_secret*: the **Client secret** for your Service Client. + 1. *resource*: the **Application ID URI** for your Resource Application. + 1. If using Azure Active Directory V2 then instead of setting *resource*, set *scope*: ```/.default``` where Application ID URI is for your Resource Application. +1. Select **Send** (or **Fetch Tokens** in case you are using the Auth tab in the API request) to retrieve the access token. + +## Summary + +In this How-to Guide, you learned how to configure the authentication settings for the FHIR converter service using AAD as the identity provider. + +To get started with your FHIR converter service, refer to the following documents: + +* [Use FHIR converter APIs](use-convert-web-apis.md) +* [Monitor FHIR converter service](monitoring.md) +* [Troubleshooting guide](troubleshoot.md) diff --git a/docs/how-to-guides/enable-template-store-integration.md b/docs/how-to-guides/enable-template-store-integration.md new file mode 100644 index 000000000..4f65c031b --- /dev/null +++ b/docs/how-to-guides/enable-template-store-integration.md @@ -0,0 +1,97 @@ +# Template Store Integration + +This how-to-guide shows you how to configure the template store for the FHIR converter service in Azure. This is needed to support the ability to use custom Liquid templates for your conversion requests. + +The service currently supports integration with Azure Storage Accounts to pull custom templates hosted within the blob container. + +If you are using the quickstart version of the [deployment options](deployment-options.md#default-settings) with default settings, your FHIR converter service will be automatically configured to pull templates from a newly created Storage Account. + +Alternatively, to configure a different/pre-existing storage account, follow the steps in this document. + +## Template store settings overview + +The configurable template store settings are : + +```json +{ + "TemplateHosting": { + "StorageAccountConfiguration": { + "ContainerUrl": "" + } + } +} +``` + +| Element | Description | +| -------------------------- | --- | +| StorageAccountConfiguration:ContainerUrl | The URL of the storage account blob container containing the Liquid templates. | + +## Configure storage account details + +### Prerequisites + +To configure your template store with your FHIR converter service, you need to have an Azure Storage Account created with a blob container. Refer [Create a Storage Account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-create?tabs=azure-portal) for instructions to create one. + +The custom Liquid templates need to be uploaded to the storage account blob container that will be configured with the service. +For guidance on how to create custom Liquid templates, refer [Customize templates](customize-templates.md). + +### Grant permissions to the storage account + +In order for the service to be able to load the custom templates from the storage account, the Azure Container App running the service needs to be granted appropriate permissions to read from the storage account. + +1. Enable managed identity on your Azure Container App. + * Your container app can be granted either a system-assigned identity or a user-assigned identity. + + Refer [Managed Identities in Azure Container Apps](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity?tabs=portal%2Cdotnet) for more information. + +![Convert identity](../images/convert-identity.png) + +1. Assign the identity created above,[`Storage Blob Data Reader`](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/storage#storage-blob-data-reader) role priveleges on the storage account container being configured with the service. + + Refer [Assign an Azure role for access to blob data](https://learn.microsoft.com/en-us/azure/storage/blobs/assign-azure-role-data-access?tabs=portal) for more information. + +![Convert template store permissions](../images/convert-template-store-permissions.png) + +### Set the template store configuration of your FHIR converter service + +1. If you have deployed the FHIR converter service to Azure, provide the configuration: + * Use the deployment option (**TODO** insert instructions) + + * Alternatively, you can directly provide the configuration via environment variables in your Azure Container App running the FHIR converter service by editing the container: + 1. **TemplateHosting__StorageAccountConfiguration__ContainerUrl** - the blob container URL hosting the custom templates. + + Refer [Configure environment variables](https://learn.microsoft.com/en-us/azure/container-apps/environment-variables?tabs=portal) for more information. + + ![Convert template store config](../images/convert-template-store-config.png) + +### Verify template store health check + +To verify your FHIR converter service is set up correctly to pull the custom templates from the configured storage account, use the below health check endpoint: + +**GET `https:///health/check`** + +Sample response body + +```json +{ + "overallStatus": "Healthy", + "details": [ + { + "name": "TemplateStoreHealthCheck", + "status": "Healthy", + "description": "Sucessfully connected to blob template store.", + "data": {} + } + ] +} +``` + +## Summary + +In this how-to-guide, you learned how to configure the template store settings for the FHIR converter service to be able to use custom Liquid templates for conversion. + +To get started with your FHIR converter service, refer to the following documents: + +* [Customize Liquid templates](customize-templates.md) +* [Use FHIR converter APIs](use-convert-web-apis.md) +* [Monitor FHIR converter service](monitoring.md) diff --git a/docs/how-to-guides/monitoring.md b/docs/how-to-guides/monitoring.md new file mode 100644 index 000000000..f115117a5 --- /dev/null +++ b/docs/how-to-guides/monitoring.md @@ -0,0 +1,127 @@ +# Monitoring + +The FHIR converter APIs generate custom logs and metrics when invoked, which can be utilized for gaining insights or aiding in troubleshooting. + +If you have deployed your FHIR converter service using the provided [deployment options](deployment-options.md), a Log Analytics workspace and Application Insights resource will be created by default. The authentication with the Appplication Insights instance is set up such that only telemetry from authorized container apps reaches the Appplication Insights instance before being forwarded to the Log Analytics workspace. + +## Telemetry + +In the context of the FHIR converter service, telemetry data is categorized into three main types: Requests, Traces, and Metrics. + +### Requests + +The request telemetry provides detailed information about the API requests made to the FHIR converter service. This includes data such as the time of the request, the endpoint hit, the HTTP method used, the status code returned, the duration of the request and the operation id associated with the request. + +This data can be used to monitor the overall health of the service, identify patterns in usage, and detect potential issues. For example, a high number of 4xx or 5xx status codes could indicate a problem with the service or the requests being made. + +### Traces + +Trace telemetry provides detailed diagnostic information during the execution of the FHIR converter API. This includes data such as the trace message with the precise time stamp, and the operation id associated with the trace. + +Trace data can be used to debug and diagnose issues with the service. For example, if an API request fails, you can use the trace data to determine the sequence of events that led to the failure and identify the root cause. + +### Metrics + +Metrics telemetry provides quantitative data about the operation of the FHIR converter service, which can be used to monitor the performance and usage of the service. +The following custom metrics are emitted upon usage of FHIR converter APIs: + +| Name | Description | Dimensions | +| ----------------------- | ------------------------------------------------------------|---------------------| +| RequestCount | Total number of API requests made. | | +| RequestSucceeded | Total number of successful API requests. | | +| RequestFailed | Total number of failed API requests. | | +| RequestSuccessLatency | Total latency of a successful API request. | | +| RequestFailedLatency | Total latency of a failed API request. | | +| InputDataByteSize | Size of the InputData in bytes. | | +| InputDataType | The InputDataFormat specified in the API request. | Name | +| OutputDataType | The OutputDataFormat of the result of the API request. | Name | +| RootTemplate | The RootTemplateName specified in the API request. | Name | +| ErrorCount | Total number of errors encountered during the API requests. | Name, ErrorCategory | + +## Accessing Telemetry + +### Log Analytics + +Telemetry can be accessed from [Log Analytics](https://learn.microsoft.com/azure/container-apps/log-monitoring?tabs=bash#query-log-with-log-analytics), either via the Log Analytics workspace resource created, or from the Azure Monitor Logs integrated within your Azure Container App. + +* You can query enriched request, metric, and trace logs under the **LogManagement** category in the Logs blade, using the ```AppRequests```, ```AppMetrics```, and ```AppTraces``` tables, respectively. + + For more in-depth debugging, each log in the ```AppTraces``` table can be associated to a request in the ```AppRequests``` table through a matching ```OperationId``` value. + + ![AppRequests](../images/convert-loganalyticsrequests.png) + ![AppTraces](../images/convert-apptraces.png) + ![AppMetrics](../images/convert-appmetrics.png) + + * Sample KQL queries for trace and request telemetry: + + ```KQL + // get the operation_id and result of each request + AppRequests + | where TimeGenerated > ago(12hours) + | project TimeGenerated, Name, ResultCode, OperationId + + // get the error details of a failed request + AppTraces + | where OperationId == "" + | where Message contains "Convert operation failed" + + // get the latency of each step of the convert operation for a given request + AppTraces + | where OperationId == "" + | where Properties contains "Metric" and Properties contains "Duration" + | project OperationId, Metric = tostring(Properties.Metric), Latency = tostring(Properties.Duration) + ``` + + * Sample KQL queries for metrics: + + > [!Note] + > Azure Monitor aggregates metrics, so entries in th `AppMetrics` table cannot each be associated with an individual request - [more info](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/metrics-aggregation-explained) + + ```KQL + // get the number of total requests, successful requests, and failed requests + AppMetrics + | where TimeGenerated > ago(1hour) + | where Name == "RequestCount" or Name == "RequestSucceeded" or Name == "RequestFailed" + | summarize Count = count() by Name + + // get the total number of failed requests by exception type, where the error is a client error + AppMetrics + | where TimeGenerated > ago(6hour) + | where tostring(Properties.ErrorCategory) == "ClientError" + | summarize Count = count() by ExceptionType = tostring(Properties.Name) + ``` + +* You can query the raw console logs and system logs under the **CustomLogs** category in the Logs blade, using using the tables `ContainerAppConsoleLogs_CL` and `ContainerAppSystemlogs_CL`, respectively. + +### Application Insights + +Application Insights provides various experiences to gather insights on the performance, reliability, and quality of your applications. Refer [this](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview) to learn more about all the different monitoring views and additional telemetry available. + +* Custom service metrics - To view the metrics emitted by your FHIR converter service in a graphical format, when creating graphs in the Metrics blade: + * Metric Namespace: Select `Log-based metrics` + * Metric: Scroll to the `Custom` category and select the metric. + * Aggregration: Choose the type of aggregration you want to view the metric data with. + * Apply splitting: For metrics that have dimensions, you can `Apply splitting` on the specific dimension to get more granular information for the metric. + + ![Metrics](../images/convert-appinsights-metrics.png) + ![Metrics](../images/convert-azuremonitormetrics.png) + +### Log Stream + +The raw console logs emitted by the FHIR converter service as well as the system logs emitted by the container app, can be viewed using the built-in [Log Stream](https://learn.microsoft.com/azure/container-apps/log-streaming?tabs=bash) in Azure Container App. + + ![Log Stream](../images/convert-logstream.png) + +### Additional information + +* Azure Container Apps provides several built-in observability features to give insights on your container app's health and performance. For example, this can be used to monitor CPU usage, memory usage, traffic bottlenecks; diagnose issues with revision deployment, responsiveness, etc. + + Refer [this](https://learn.microsoft.com/en-us/azure/container-apps/observability) for more information. + +## Summary + +In this how-to-guide, you learned how to monitor your FHIR converter service in Azure and gain insights into the usage of the APIs. + +To troubleshoot any errors reported by the service, refer to the following documents: + +* [Troubleshooting guide](troubleshoot.md) diff --git a/docs/how-to-guides/setup-convert-service.md b/docs/how-to-guides/setup-convert-service.md new file mode 100644 index 000000000..05330889a --- /dev/null +++ b/docs/how-to-guides/setup-convert-service.md @@ -0,0 +1,40 @@ +# Set up FHIR converter service + +The FHIR converter APIs are packaged as a containerized application and made available as an image in [Microsoft Container Registry](https://github.com/microsoft/containerregistry). + +This how-to-guide details instructions on how to deploy the FHIR converter as a web service in Azure using [this container image](https://mcr.microsoft.com/en-us/product/healthcareapis/fhir-converter/tags) and configure the service with your desired settings, to enable it for your conversion requests. + +## 1. Deploy FHIR converter service in Azure + +To deploy your FHIR converter service in Azure using the MCR artifact, see [Deployment options to set up FHIR converter service in Azure](deployment-options.md). + +## 2. Configure FHIR converter service settings + +The quickstart version of the deployment options will set up your service with the default configuration, which is ideal for testing or initial setup. +To learn more about the various options available to customize your service to meet your needs, and to configure the settings of your FHIR converter service in Azure, see [Configure FHIR converter service settings](configuration-settings.md). + +## 3. [Optional] Configure custom Liquid templates + +The FHIR converter APIs come pre-packaged with [default Liquid templates](../../data/Templates) for the supported conversion scenarios. +However, to support custom transformation requirements, the APIs also have the capability to use custom templates provided for conversion. +To learn more about how to customize Liquid templates to use for your conversion requests, see [Customize templates](customize-templates.md). + +These templates need to be uploaded to the template store configured with your FHIR converter service. Refer [Enable template store integration](enable-template-store-integration.md) for detailed instructions on configuring the service to use custom templates. + +## 4. Verify FHIR converter service health + +Once you have set up your service, you can check its health status by using the health check endpoint. The health status indicates if the service is configured correctly, is running, and is available to service requests. Refer to [Health check](use-convert-web-apis.md#health-check) for more information. + +In case of any issues with the setup, refer [Troubleshooting guide](troubleshoot.md) for information on how to debug and resolve the issue. + +## Summary + +In this how-to-guide, you learned how to set up your FHIR converter service in Azure using the MCR container image. + +Once the setup is complete, you can use the endpoint corresponding to the application url of your Container App running the web service. + +To get started with your FHIR converter service, refer to the following documents: + +* [Use FHIR converter APIs](use-convert-web-apis.md) +* [Monitor FHIR converter service](monitoring.md) +* [Troubleshooting guide](troubleshoot.md) diff --git a/docs/how-to-guides/troubleshoot.md b/docs/how-to-guides/troubleshoot.md new file mode 100644 index 000000000..991b6a33d --- /dev/null +++ b/docs/how-to-guides/troubleshoot.md @@ -0,0 +1,277 @@ +# FHIR Converter Troubleshooting Guide + +This how-to-guide provides information and steps to help you troubleshoot issues you may encounter while using the FHIR Converter. + +Errors encountered during setup or when starting the web service, will be surfaced in the application logs. Errors encountered during the conversion request, will be returned in the API response body as well as surfaced in the application logs. + +## Error response body + +If the conversion operation fails, the API request fails with a 4xx or 5xx HTTP status code with a `x-ms-error-code` response header and the response body providing information on the error. + +All error response bodies are returned in JSON format following the below structure: + +```json +{ + "error": { + "code": "", + "message": "", + "innerError": { + "code": "", + "message": "" + } + } +} +``` + +| Property | | Type | Description | +|-------------|---------|---------| -------------------------------------------| +| code | | String | One of a server-defined set of error codes.
This value matches the `x-ms-error-code` response header value. | +| message | | String | A high-level description of the error. | +| innerError | | Object | An object containing more specific information than the top-level error details. | +| | code | String | A more specific error code than the top-level error code. | +| | message | String | A detailed description of the error providing granular information on the cause of error. | + +> [!Note] +> +> * The top-level error code which matches the `x-ms-error-code` response header value, are bound by the API contract; this means that a change to the error code that is returned for a given set of conditions will not change without an API version update. +> * No new top-level error codes will be added to the API without a version update. +> * All remaining fields in the response body are *not* bound to the API contract, which means that these values may change without an API version update and are intended for end-user debugging, not to be relied upon by the client code. +> * The top-level error code and message are guaranteed to be populated, but the InnerError will be empty for 500-level responses. + +## Debugging with application logs + +Application logs can provide detailed debugging information on the errors encountered either during the set up of the service, or during the FHIR converter API request operations. + +Error logs conform to the following format: + +```text +Failed conversion request with HTTP Status {HTTP status}; TOP-LEVEL-ERROR: {error code} - {top-level error message}.; INNER ERROR: Code: {inner error code}, Message: {inner error code}, Exception: {the outer exception that causes the Inner Error}; InnerException: {the inner exception of the Inner Error} - Code: {error code of the inner exception}, Message: {inner exception message}. +``` + +The top-level error information in the error log aligns with the top-level error fields in the response body. If the inner error details are present in the response body, they align with the inner error details of the log message, though the log message will likely provide more details than the response body. + +See the [Monitoring](monitoring.md) document for more information on accessing these logs and sample queries that may be helpful for debugging. + +## Top-Level Error Codes + +This section describes potential causes for each top-level error code, root causes, and recommended resolutions. + +### IncompatibleDataError + +This error is observed if the template used for conversion and the input data being converted are technically valid, but the converted result produced is poorly formed. + +#### InnerError code: JsonParsingError + +**Root Cause:** For convertToFhir requests, the output produced by the template cannot be parsed into valid JSON format. For convertToHl7v2 requests, the InputData value cannot be parsed into valid JSON format. + +**Troubleshooting:** +In the case of a convertToFhir request, ensure that the template is properly formatted to generate a valid JSON object. Refer to the provided [sample templates](../../data/Templates) as valid examples or see more information on template authoring [here](customize-templates.md). In the case of a convertToHl7v2 request, ensure that the InputData string is of a format that can be correctly parsed into a JSON object. + +#### InnerError code: JsonMergingError + +**Root Cause:** The conversion produced an output payload that can be parsed to a JSON object, but this JSON object does not have the expected structure or contains invalid data. + +**Troubleshooting:** +Ensure that the template structures the data to align with the expected output format and does not contain any invalid data. Refer to the provided [sample templates](../../data/Templates) and [sample InputData](../../data/SampleData) for valid examples or see more information on template authoring [here](customize-templates.md). + +### InternalServerError + +An unexpected internal server error has occurred. Please see the application logs for more details and/or retry the request. + +### InvalidInputData + +The InputData value is not null or empty, but is invalid or cannot be parsed. + +#### InnerError code: InputParsingError + +**Root Cause:** InputData parsing logic is specific to the expected format. If you are encountering this error, it is likely the result of a mismatch between the InputData and the specified InputDataFormat. + +**Troubleshooting:** Ensure that the InputDataFormat field value in the request body aligns with the format of the InputData field. For more information on authoring the request body, view the sample request bodies in the [Use FHIR converter APIs](use-convert-web-apis) document and the sample InputData [here](../../data/SampleData). + +#### InnerError code: InvalidInputDataContent + +**Root Cause:** The content of the InputData string passed by the user is invalid. + +**Troubleshooting:** Examples of known causes for this error for an Hl7v2 to FHIR request are missing or duplicate Hl7v2 separators, an invalid Hl7v2 message, or an invalid Hl7v2 escape character. See the logs for specific details on why the InputData value for the failed request is invalid. You can find examples of valid InputData [here](../../data/SampleData). + +### InvalidRequestBody + +The request body does not match the required format. See the [Use FHIR converter APIs](use-convert-web-apis) document for examples of valid request bodies. + +#### InnerError: InvalidInputDataRequestValue + +**Root Cause:** The InputData field is required but is missing or empty. + +**Troubleshooting:** Examine the request body to ensure that the InputData field is present and is not null or empty. + +#### InnerError: InvalidInputDataFormat + +**Root Cause:** The InputDataFormat field is required but is missing or empty, or is not one of the accepted values. + +**Troubleshooting:** Examine the request body to ensure that the InputDataFormat field is present and is not null or empty. If the field is present, for convertToFhir requests, this value must be one of `Hl7v2`, `Ccda`, `Json`, or `Fhir_STU3`. For convertToHl7v2 requests, this value must be `Fhir`. Note that these values are case-sensitive. + +#### InnerError: InvalidRootTemplate + +**Root Cause:** The RootTemplateName field is required but is missing or empty. + +**Troubleshooting:** Examine the request body to ensure that the RootTemplateName field is present and is not null or empty. + +#### InnerError: InvalidRequestBody + +**Root Cause:** The request body failed validation for some reason other than those listed above. + +**Troubleshooting:** Examine the request body to ensure that all required fields are present and that the values are correctly formatted. If the request body appears to be correct, see the application logs for more details on why the request body failed validation. + +### InvalidTemplate + +The template content or name is invalid. + +#### InnerError: InvalidFilter + +**Root Cause:** An error was encountered during the convert operation while using a filter referenced by the conversion template. + +**Troubleshooting:** Examples of known causes of this error include usage of an invalid date-time format, an invalid hexadecimal number, and invalid time-zone handling. See the application logs for specific details of the error and examine any filters referenced by the conversion template to determine the source of the error. See more information on using filters [here](customize-templates.md). After addressing the issue in the template filter, upload the updated filter to the storage account, restart the container, and retry the request. + +#### InnerError: InvalidTemplateContent + +**Root Cause:** Some aspect of the template content is invalid. + +**Troubleshooting:** See the application logs for specific details of the error and examine the conversion template (both root template and templates referenced by the root template) to determine the source of the error. See more information on template authoring [here](customize-templates.md). After addressing the issue, upload the updated template to the storage account, restart the container, and retry the request. + +#### InnerError: TemplateNotFound + +**Root Cause:** The template name or path specified in the RootTemplateName field of the request body could not be found. + +**Troubleshooting:** Ensure that this value matches the value necessary to access the desired template. For default template requests, this should be only the template name. For example, to access the [ADT_A01](../../data/Templates/Hl7v2/ADT_A01.liquid) default template, the RootTemplateName field should be set to `ADT_A01`. For custom requests, this will be the name of the blob file containing the Liquid template in the storage account configured with the service. In the Azure portal, inspect your storage account to ensure that the provided `RootTemplateName` value matches the blob file name. Note that the `RootTemplateName` field should **not** contain the Storage Blob URI. See more information on writing valid request bodies to access custom templates in the [Use FHIR converter APIs](use-convert-web-apis) document. + +### TemplateCollectionError + +The service encountered an error while attempting to load the template collection. + +#### InnerError code: DependencyResourceAuthFailed + +**Root Cause:** If using custom templates, this is likely due to the service not having a user-assigned managed identity with the "Storage Blob Data Reader" role assignment granted by the storage account containing the template. + +**Troubleshooting:** Ensure that the container app's system-assigned managed identity is granted the "Storage Blob Data Reader" role assignment by the storage account. To verify that this is configured correctly, navigate to the Storage Account, click the "Access Control (IAM)" blade, select "Role Assignments" and ensure that the system-assigned managed identity exists under the "Storage Blob Data Reader" role. If it does not, add this Role Assignment. See more information on Azure Role Assignments [here](https://learn.microsoft.com/en-US/Azure/role-based-access-control/role-assignments). + +After any updates to the template store integration configuration, you will need to restart your container before retrying the request. See the [Enable Template Store Integration](enable-template-store-integration.md) document for more information on configuring the service to pull custom templates from a storage account, and consider using one of the [provided deployment options](deployment-options.md) to ensure that the service is configured correctly. + +#### InnerError code: DependencyResourceNotFound + +**Root Cause:** If using custom templates, this likely means that your service is configured with an incorrect blob container URL. + +**Troubleshooting:** Ensure that your Container App is configured with the correct URL of the blob container containing your templates. To verify, navigate to your Container App. In the Overview blade, click on the "view JSON" button at the top right. In the `properties.template.containers` section, you should see your configured blob container URL as the value of the `TemplateHosting__StorageAccountConfiguration__ContainerUrl` environment variable. Compare this with the URL of your blob container; to find this, navigate to your storage account, then to the "Containers" blade under "Data storage", then select your container. Within your container page, navigate to the "Properties" blade under "Settings", and you should see the blob container URL at the top. If they do not match, update the blob container URL in your container app (see more details on configuring template store integration [here](enable-template-store-integration.md)), restart the Container, and retry the request. + +#### InnerError code: TemplateCollectionSizeExceedsLimit + +**Root Cause:** The template collection uploaded to the storage account exceeds the allowed size limit. + +**Troubleshooting:** The response body and error log should indicate the maximum allowed template collection size. Remove templates from the storage account so that the collection size aligns with the limit, restart the container, and then retry the request. + +### TimeoutError + +The convert operation timed out. + +#### InnerError: CancellationError +**Root Cause:** The convert operation was cancelled, likely because it took longer than the allowed time. + +**Troubleshooting:** First, attempt to identify which step of the convert operation is timing out. The application logs provide the latency of each individual step of the convert operation. + +The following query may be helpful in viewing the latencies of the convert operation steps: + +```KQL +AppTraces +| where TimeGenerated > ago(3hours) +| where Properties contains +| project TimeGenerated, Metric = tostring(Properties.Metric), Latency = tostring(Properties.Duration), OperationId +``` + +Replace the `latency_metric_name` in the query above with the metric of interest from the list below. Compare the latency to that of successful requests to identify any step(s) running longer than normal: + +* *InputDeserializationDuration*: If the long-running step is `InputDeserializationDuration`, this could be the result of the `InputData` value being too large. Retry the request with a smaller value. +* *TemplateRetrievalDuration*: If the long-running step is `TemplateRetrievalDuration`, this could be the result of the template collection having too many individual templates, resulting in the search timing out. Reduce the number of templates in the collection, restart the container, and retry the request. +* *TemplateRenderDuration*: If the long-running step is `TemplateRenderDuration`, this is likely due to the template being too large. Retry the request with a smaller template. +* *PostProcessDuration*: If the long-running step is `PostProcessDuration`, this could be the result of the convert operation producing an output payload that is too large. Retry the request with an InputData value with a reduced number of elements to be convert, or with a template that produces an output with fewer elements. + +#### InnerError: TimeoutError +**Root Cause:** This is likely a result of the template rendering step timing out. + +**Troubleshooting:** If you have had previously successful requests with smaller templates, you can use the query under "InnerError: CancellationError" above with `latency_metric_name` set to `TemplateRenderDuration` to compare the latency of the Template Rendering step for your successful requests vs. your failed requests. If the template rendering timeout seems to be the likely cause, re-attempt the request with a smaller template. + +## 400 - Bad request + +### Top-level error code + +#### ApiVersionUnspecified + +**Root Cause(s):** + +* The request URL is missing the `api-version` query parameter or has an empty value provided. + * Troubleshooting: + * Provide the `api-version` query parameter in the request URL. Refer [API versions](use-convert-web-apis.md#api-versions) for more information. + +#### InvalidApiVersion + +**Root Cause(s):** + +* The `api-version` query parameter value specified is not supported. + * Troubleshooting: + * Provide the `api-version` query parameter value in the request URL with any supported api-version listed in the response header `api-supported-versions`. Refer [API versions](use-convert-web-apis.md#api-versions) for more information. + +## 401 - Unauthorized + +This captures errors if the client making the request is not authenticated with the service. +Ensure the steps outlined in [Azure Active Directory Authentication](enable-authentication.md) guide were followed. + +**Root Cause(s):** + +* Missing/invalid `Authorization` request header provided in the request. + * Troubleshooting: + * Provide the `Authorization` request header containing valid bearer token. Refer [Access token](enable-authentication.md#get-access-token) for more information. + +* Client ID used to get credentials was not granted API permissions required by the service. + * Troubleshooting: + * Add the required API permissions in the client application and then request the bearer token. Refer [Create a Client Application](enable-authentication.md#create-a-service-client-application) for more information. + +* The token used was intended for a different audience. This happens if the scope used when getting the access token is incorrect. + * Troubleshooting: + * Ensure the scope value provided when getting the access token is in the list of audiences configured with the service. Refer [Access token](enable-authentication.md#get-access-token) for more information. + +## 404 - Not Found + +This error is usually received if the request is made against an invalid endpoint. + +**Root Cause(s):** + +* The requested API route does not exist. + * Troubleshooting: + * Verify the API route is correct. + Refer [FHIR converter APIs](use-convert-web-apis.md#apis) to get the exact API names. + +* The service URL does not exist. + * Troubleshooting: + * Verify the service URL provided in the request URL corresponds to the actual service endpoint. Refer [Endpoint](use-convert-web-apis.md#fhir-converter-endpoint) to learn how to get this URL. + +## 405 - Method Not Allowed + +**Root Cause(s):** + +* The service URL used for the request is an HTTP endpoint. + * Troubleshooting: + * Update the service URL used in the request to start with `https://`. + +## 415 - Unsupported Media Type + +**Root Cause(s):** + +* The request body provided is not in the supported JSON format. + * Troubleshooting: + * Ensure the request body is provided as a JSON object in the request. + +## 500 - Server Error + +This indicates something went wrong with the service and encountered an unrecoverable error during the conversion due to an unexpected scenario. + +The application logs will provide more details on the error encountered. + +Please contact the team for further diagnosis, by creating an [issue](https://github.com/microsoft/FHIR-Converter/labels/mcr-fhir-converter). diff --git a/docs/how-to-guides/use-convert-web-apis.md b/docs/how-to-guides/use-convert-web-apis.md new file mode 100644 index 000000000..9b0f67589 --- /dev/null +++ b/docs/how-to-guides/use-convert-web-apis.md @@ -0,0 +1,210 @@ +# Use FHIR converter APIs + +This how-to-guide explains how to use the FHIR converter APIs using the service deployed to Azure. + +## Prerequisites + +In order to use the FHIR converter APIs, you must have an instance of the FHIR converter service deployed to Azure. + +Refer [Set up FHIR converter Service](setup-convert-service.md) for detailed instructions to create a web service to target your conversion requests to. + +## FHIR converter endpoint + +If you have set up your FHIR converter service using the provided [deployment options](deployment-options.md), an Azure Container App is deployed to run the FHIR converter container and serves as the web service that clients can send conversion requests to. + +### Service URL + +The service URL to target corresponds to the application URL of the Azure Container App running the service. + +![convert-serviceurl](../images/convert-serviceurl.png) + +### API versions + +The version of the REST API must be explicitly specified in the request URL in a query parameter `api-version`, as shown in the following example: + +`https:///?api-version=` + +#### Supported versions + +A list of supported versions for the requested API is returned as a response header `api-supported-versions`. + +Currently the supported versions are: + +* 2024-05-01-preview + +### APIs + +#### - Swagger + +The swagger document for the supported versions can be found at the following url: + +**GET `https:////swagger.yaml`** + +#### - Health check + +The health status of the service which indicates if the service is configured correctly and is running and available to service requests, can be queried using the following API: + +**GET `https:///health/check`** + +* Sample response body + +```json + { + "overallStatus": "Healthy", + "details": [ + { + "name": "TemplateStoreHealthCheck", + "status": "Healthy", + "description": "Sucessfully connected to blob template store.", + "data": {} + } + ] + } + +``` + +#### - Convert to FHIR R4 + +Supports conversion of legacy healthcare formats such as Hl7v2, C-CCDA, Json and FHIR STU3 to FHIR R4 format. + +* API Route: `convertToFhir` + +* Request headers: + + * Authorization + + If your service was set up with authentication enabled, you need to provide the bearer token in the authorization header. + + Refer [get access token](enable-authentication.md#get-access-token) to authenticate with your FHIR converter service which has security enabled, i.e., restricted access to APIs using the provided security configuration (audience and authority). + +* Request parameters + +| Name | Optionality | Default | Description | Accepted values | +| ----- | ----- | ----- |----- |----- | +| InputDataFormat | Required | - | Type of data input. | `Hl7v2`, `Ccda`, `Json`, `Fhir_STU3` | +| RootTemplateName | Required | - | Name of root template to be used for conversion. | For use of **default templates**, this will be the name of template provided in [here](https://github.com/microsoft/FHIR-Converter/tree/main/data/Templates) for each of the supported data formats.

**HL7v2 to FHIR** (57 templates): "ADT_A01", "ADT_A02", "ADT_A03", "ADT_A04", "ADT_A05", "ADT_A06". "ADT_A07", "ADT_A08", "ADT_A09", "ADT_A10", "ADT_A11", "ADT_A13", "ADT_A14", "ADT_A15", "ADT_A16", "ADT_A25", "ADT_A26", "ADT_A27", "ADT_A28", "ADT_A29", "ADT_A31", "ADT_A40", "ADT_A41", "ADT_A45", "ADT_A47", "ADT_A60", "BAR_P01", "BAR_P02", "BAR_B12", "DFT_P03", "DFT_P11", "MDM_T01", "MDM_T02", "MDM_T05", "MDM_T06", "MDM_T09", "MDM_T10", "OMG_O19" "OML_O21", "ORM_O01", "ORU_R01", "OUL_R22", "OUL_R23", "OUL_R24", "RDE_O11", "RDE_O25", "RDS_O13", "REF_I12", "REF_I14", "SIU_S12", "SIU_S13", "SIU_S14", "SIU_S15", "SIU_S16", "SIU_S17", "SIU_S26", "VXU_V04"

**C-CDA to FHIR** (9 templates): "CCD", "ConsultationNote", "DischargeSummary", "HistoryandPhysical", "OperativeNote", "ProcedureNote", "ProgressNote", "ReferralNote", "TransferSummary"

**JSON to FHIR**: "Stu3ChargeItem", "ExamplePatient"
(*These JSON templates are sample templates for use, not default templates that adhere to any pre-defined JSON message types. JSON does not have any standardized message types, unlike HL7v2 messages or C-CDA documents. Therefore, instead of "default" templates we provide you with some sample templates that you can use as a starting guide for you to modify and customize.*)

**FHIR STU3 to R4**: Name of the root template that is the same as the STU3 resource name e.g., "Patient", "Observation", "Organization". Some of the STU3 resources are renamed or removed from R4. Please refer to [Resource differences and constraints for STU3 to R4](docs/Stu3R4-resources-differences.md).

For use of **custom templates**, this will be the name of the blob file containing the Liquid template in the storage account configured with this service. The path to the blob file relative to the blob container must be specified.
For instance, if the template named "ADT_A01" exists in a folder named "Hl7v2" in the container, the value should include the folder path - "Hl7v2/ADT_A01" | +| InputDataString | Required | - | Input data content to be converted in string format. | String representation of the data to be converted. | + +* Response body + +The API response is a json object which contains the converted FHIR bundle under the `result` property. + +* Sample: + + **POST `https:///convertToFhir?api-version=`** + + Request Body + + ```json + { + "InputDataFormat": "Hl7v2", + "RootTemplateName": "ADT_A01", + "InputDataString": "MSH|^~\\&|SIMHOSP|SFAC|RAPP|RFAC|20200508131015||ADT^A01|517|T|2.3|||AL||44|ASCII\nEVN|A01|20200508131015|||C005^Whittingham^Sylvia^^^Dr^^^DRNBR^PRSNL^^^ORGDR|\nPID|1|3735064194^^^SIMULATOR MRN^MRN|3735064194^^^SIMULATOR MRN^MRN~2021051528^^^NHSNBR^NHSNMBR||" + } + ``` + + Response Body + + ```json + { + "result": { + "resourceType": "Bundle", + "type": "batch", + "timestamp": "2020-05-08T13:10:15Z", + "identifier": { + "value": "517" + }, + "id": "7dcb7d92-7a75-3d65-42f9-0f790afac4db", + "entry": [ + { + "fullUrl": "urn:uuid:aa521dd9-b613-0210-a661-82ce17e38fb3", + "resource": { + "resourceType": "MessageHeader", + "id": "aa521dd9-b613-0210-a661-82ce17e38fb3", + "source": { + "name": "SIMHOSP", + "_endpoint": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + } + }, + .... + .... + }, + } + ] + } + } + ``` + +#### - Convert to HL7v2 + +Supports conversion of FHIR R4 data to HL7v2 format. + +* API Route: `convertToHl7v2` + +* Request headers: + + * Authorization + + If your service was set up with authentication enabled, you need to provide the bearer token in the authorization header. + + Refer [get access token](enable-authentication.md#get-access-token) to authenticate with your FHIR converter service which has security enabled, i.e., restricted access to APIs using the provided security configuration (audience and authority). + +* Request parameters + +| Name | Optionality | Default | Description | Accepted values | +| ----- | ----- | ----- |----- |----- | +| InputDataFormat | Required | - | Type of data input. | `Fhir` | +| RootTemplateName | Required | - | Name of root template to be used for conversion. | For use of **default templates**, this will be the name of template provided in [here](https://github.com/microsoft/FHIR-Converter/tree/main/data/Templates) for each of the supported data formats.

**FHIR to HL7v2** : **TODO add references**(*These JSON templates are sample templates for use, not default templates that adhere to any pre-defined JSON message types. JSON does not have any standardized message types, unlike HL7v2 messages or C-CDA documents. Therefore, instead of "default" templates we provide you with some sample templates that you can use as a starting guide for you to modify and customize.*).

For use of **custom templates**, this will be the name of the blob file containing the Liquid template in the storage account configured with this service. The path to the blob file relative to the blob container must be specified.
For instance, if the template named "BundleToHL7v2" exists in a folder named "Fhir" in the container, the value should include the folder path - "Fhir/BundleToHL7v2" | +| InputDataString | Required | - | Input data content to be converted in string format. | String representation of the data to be converted. | + +* Response body + +The API response is a json object which contains the converted HL7v2 message under the `result` property. + +* Sample: + + **POST `https:///convertToHl7v2?api-version=`** + + Request Body + + ```json + { + "InputDataFormat": "Fhir", + "RootTemplateName": "Fhir/BundleToHL7v2", + "InputDataString": "{\"resourceType\":\"Bundle\",\"id\":\"bundle-response-medsallergies\",\"type\":\"batch-response\",\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"example\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2018-11-12T03:35:20.715Z\"},\"identifier\":[{\"use\":\"usual\",\"type\":{\"coding\":[{\"system\":\"http://terminology.hl7.org/CodeSystem/v2-0203\",\"code\":\"MR\"}]},\"system\":\"urn:oid:1.2.36.146.595.217.0.1\",\"value\":\"12345\",\"period\":{\"start\":\"2001-05-06\"},\"assigner\":{\"display\":\"AcmeHealthcare\"}}],\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Chalmers\",\"given\":[\"Peter\",\"James\"]},{\"use\":\"usual\",\"given\":[\"Jim\"]},{\"use\":\"maiden\",\"family\":\"Windsor\",\"given\":[\"Peter\",\"James\"],\"period\":{\"end\":\"2002\"}}],\"telecom\":[{\"use\":\"home\"},{\"system\":\"phone\",\"value\":\"(03)55556473\",\"use\":\"work\",\"rank\":1},{\"system\":\"phone\",\"value\":\"(03)34105613\",\"use\":\"mobile\",\"rank\":2},{\"system\":\"phone\",\"value\":\"(03)55558834\",\"use\":\"old\",\"period\":{\"end\":\"2014\"}}],\"gender\":\"male\",\"birthDate\":\"1974-12-25\",\"_birthDate\":{\"extension\":[{\"url\":\"http://hl7.org/fhir/StructureDefinition/patient-birthTime\",\"valueDateTime\":\"1974-12-25T14:35:45-05:00\"}]},\"deceasedBoolean\":false,\"address\":[{\"use\":\"home\",\"type\":\"both\",\"text\":\"534ErewhonStPeasantVille,Rainbow,Vic3999\",\"line\":[\"534ErewhonSt\"],\"city\":\"PleasantVille\",\"district\":\"Rainbow\",\"state\":\"Vic\",\"postalCode\":\"3999\",\"period\":{\"start\":\"1974-12-25\"}}],\"contact\":[{\"relationship\":[{\"coding\":[{\"system\":\"http://terminology.hl7.org/CodeSystem/v2-0131\",\"code\":\"N\"}]}],\"name\":{\"family\":\"duMarché\",\"_family\":{\"extension\":[{\"url\":\"http://hl7.org/fhir/StructureDefinition/humanname-own-prefix\",\"valueString\":\"VV\"}]},\"given\":[\"Bénédicte\"]},\"telecom\":[{\"system\":\"phone\",\"value\":\"+33(237)998327\"}],\"address\":{\"use\":\"home\",\"type\":\"both\",\"line\":[\"534ErewhonSt\"],\"city\":\"PleasantVille\",\"district\":\"Rainbow\",\"state\":\"Vic\",\"postalCode\":\"3999\",\"period\":{\"start\":\"1974-12-25\"}},\"gender\":\"female\",\"period\":{\"start\":\"2012\"}}],\"managingOrganization\":{\"reference\":\"Organization/1\"}}},{\"resource\":{\"resourceType\":\"Observation\",\"id\":\"f001\",\"identifier\":[{\"use\":\"official\",\"system\":\"http://www.bmc.nl/zorgportal/identifiers/observations\",\"value\":\"6323\"}],\"status\":\"final\",\"code\":{\"coding\":[{\"system\":\"http://loinc.org\",\"code\":\"15074-8\",\"display\":\"Glucose[Moles/volume]inBlood\"}]},\"subject\":{\"reference\":\"Patient/f001\",\"display\":\"P.vandeHeuvel\"},\"effectiveDateTime\":\"2013-04-02T09:30:10+01:00\",\"issued\":\"2013-04-03T15:30:10+01:00\",\"performer\":[{\"reference\":\"Practitioner/f005\",\"display\":\"A.Langeveld\"}],\"valueQuantity\":{\"value\":6.3,\"unit\":\"mmol/l\",\"system\":\"http://unitsofmeasure.org\",\"code\":\"mmol/L\"},\"interpretation\":[{\"coding\":[{\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation\",\"code\":\"H\",\"display\":\"High\"}]}],\"referenceRange\":[{\"low\":{\"value\":3.1,\"unit\":\"mmol/l\",\"system\":\"http://unitsofmeasure.org\",\"code\":\"mmol/L\"},\"high\":{\"value\":6.2,\"unit\":\"mmol/l\",\"system\":\"http://unitsofmeasure.org\",\"code\":\"mmol/L\"}}]}},{\"resource\":{\"resourceType\":\"Observation\",\"id\":\"f001\",\"identifier\":[{\"use\":\"official\",\"system\":\"http://www.bmc.nl/zorgportal/identifiers/observations\",\"value\":\"6324\"}],\"status\":\"final\",\"code\":{\"coding\":[{\"system\":\"http://loinc.org\",\"code\":\"11111-1\",\"display\":\"Another test\"}]},\"subject\":{\"reference\":\"Patient/f001\",\"display\":\"P.vandeHeuvel\"},\"effectiveDateTime\":\"2013-04-02T09:30:10+01:00\",\"issued\":\"2013-04-03T15:30:10+01:00\",\"performer\":[{\"reference\":\"Practitioner/f005\",\"display\":\"A.Langeveld\"}],\"valueQuantity\":{\"value\":8.0,\"unit\":\"mmol/l\",\"system\":\"http://unitsofmeasure.org\",\"code\":\"mmol/L\"},\"interpretation\":[{\"coding\":[{\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation\",\"code\":\"H\",\"display\":\"High\"}]}],\"referenceRange\":[{\"low\":{\"value\":3.1,\"unit\":\"mmol/l\",\"system\":\"http://unitsofmeasure.org\",\"code\":\"mmol/L\"},\"high\":{\"value\":6.2,\"unit\":\"mmol/l\",\"system\":\"http://unitsofmeasure.org\",\"code\":\"mmol/L\"}}]}}]}" + } + ``` + + Response Body + + ```json + { + "result": "FHS|^~\\&|TestSystem|\nBHS|^~\\&|TestSystem|\nMSH|^~\\&|TestSystem||TransformationAgent||123||ORU^R01|1|T|2.5|\nPID|||12345|\nOBR||6323|15074-8^Glucose[Moles/volume]inBlood^LN||||||||||||||||||||||F|\nOBX|||||6.3|mmol/l|3.1-6.2|H|\nMSH|^~\\&|TestSystem||TransformationAgent||123||ORU^R01|1|T|2.5|\nPID|||12345|\nOBR||6324|11111-1^Another test^LN||||||||||||||||||||||F|\nOBX|||||8|mmol/l|3.1-6.2|H|\nBTS|1|\nFTS|1|\n" + } + ``` + +### Samples + +* [Insomnia](https://insomnia.rest/) is a tool to debug and test APIs. Once you download it, refer [API Collection Examples](../samples/APICollectionSamples) to try some test conversion requests. + + * Import [Insomnia-Examples.json](../samples/APICollectionSamples/Insomnia-Examples.json) template in Insomnia. + * Provide the values for the "Base Environment" variables: + * `serviceUrl` with your service endpoint. Refer [Service URL](#service-url). + * `api-version` with the api version to target for your requests. Refer [API versions](#api-versions). + +* Refer [sample messages](../samples/Messages/) for sample healthcare messages to test with. + +## Summary + +In this how-to-guide, you learned how to use the FHIR converter APIs against the service endpoint set up in Azure, to be able to perform health data conversions. + +To monitor or troubleshoot your service, refer to the following documents: + +* [Monitor FHIR converter service](monitoring.md) +* [Troubleshooting guide](troubleshoot.md) diff --git a/docs/images/convert-api-summary.png b/docs/images/convert-api-summary.png new file mode 100644 index 000000000..cd0ec809c Binary files /dev/null and b/docs/images/convert-api-summary.png differ diff --git a/docs/images/convert-appinsights-metrics.png b/docs/images/convert-appinsights-metrics.png new file mode 100644 index 000000000..15475dce4 Binary files /dev/null and b/docs/images/convert-appinsights-metrics.png differ diff --git a/docs/images/convert-appmetrics.png b/docs/images/convert-appmetrics.png new file mode 100644 index 000000000..8e8f1d780 Binary files /dev/null and b/docs/images/convert-appmetrics.png differ diff --git a/docs/images/convert-apptraces.png b/docs/images/convert-apptraces.png new file mode 100644 index 000000000..8c64e5d06 Binary files /dev/null and b/docs/images/convert-apptraces.png differ diff --git a/docs/images/convert-azuremonitormetrics.png b/docs/images/convert-azuremonitormetrics.png new file mode 100644 index 000000000..4c5a92f45 Binary files /dev/null and b/docs/images/convert-azuremonitormetrics.png differ diff --git a/docs/images/convert-identity.png b/docs/images/convert-identity.png new file mode 100644 index 000000000..474f84a7c Binary files /dev/null and b/docs/images/convert-identity.png differ diff --git a/docs/images/convert-loganalyticsrequests.png b/docs/images/convert-loganalyticsrequests.png new file mode 100644 index 000000000..befbf65d3 Binary files /dev/null and b/docs/images/convert-loganalyticsrequests.png differ diff --git a/docs/images/convert-logstream.png b/docs/images/convert-logstream.png new file mode 100644 index 000000000..6889f0d6d Binary files /dev/null and b/docs/images/convert-logstream.png differ diff --git a/docs/images/convert-security-config.png b/docs/images/convert-security-config.png new file mode 100644 index 000000000..bf8ca614e Binary files /dev/null and b/docs/images/convert-security-config.png differ diff --git a/docs/images/convert-serviceurl.png b/docs/images/convert-serviceurl.png new file mode 100644 index 000000000..a1f9645a4 Binary files /dev/null and b/docs/images/convert-serviceurl.png differ diff --git a/docs/images/convert-setup.png b/docs/images/convert-setup.png new file mode 100644 index 000000000..ecbbf3fc4 Binary files /dev/null and b/docs/images/convert-setup.png differ diff --git a/docs/images/convert-template-store-config.png b/docs/images/convert-template-store-config.png new file mode 100644 index 000000000..43134a595 Binary files /dev/null and b/docs/images/convert-template-store-config.png differ diff --git a/docs/images/convert-template-store-permissions.png b/docs/images/convert-template-store-permissions.png new file mode 100644 index 000000000..6ad6ccdc8 Binary files /dev/null and b/docs/images/convert-template-store-permissions.png differ diff --git a/docs/images/provisioned-resources.png b/docs/images/provisioned-resources.png new file mode 100644 index 000000000..ca112677d Binary files /dev/null and b/docs/images/provisioned-resources.png differ diff --git a/docs/samples/APICollectionSamples/Insomnia-Examples.json b/docs/samples/APICollectionSamples/Insomnia-Examples.json new file mode 100644 index 000000000..6d4abf4f7 --- /dev/null +++ b/docs/samples/APICollectionSamples/Insomnia-Examples.json @@ -0,0 +1,182 @@ +{ + "_type": "export", + "__export_format": 4, + "__export_date": "2024-05-06T20:06:24.850Z", + "__export_source": "insomnia.desktop.app:v9.1.0", + "resources": [ + { + "_id": "req_3a5d4da604064cc8af644e9d6ee25ae7", + "parentId": "wrk_5b2338cd66814889ae6591e7a513166f", + "modified": 1714757095694, + "created": 1714757054880, + "url": "{{ _.serviceUrl }}/health/check", + "name": "health", + "description": "", + "method": "GET", + "body": {}, + "preRequestScript": "", + "parameters": [], + "headers": [ + { + "name": "User-Agent", + "value": "insomnia/9.1.0" + } + ], + "authentication": {}, + "metaSortKey": -1714757054880, + "isPrivate": false, + "pathParameters": [], + "settingStoreCookies": true, + "settingSendCookies": true, + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingRebuildPath": true, + "settingFollowRedirects": "global", + "_type": "request" + }, + { + "_id": "wrk_5b2338cd66814889ae6591e7a513166f", + "parentId": null, + "modified": 1714756439852, + "created": 1714756439852, + "name": "Convert G2 Test", + "description": "", + "scope": "collection", + "_type": "workspace" + }, + { + "_id": "req_fae6ca4ffa3f4f6199c992f543779a87", + "parentId": "wrk_5b2338cd66814889ae6591e7a513166f", + "modified": 1714756940754, + "created": 1714756786070, + "url": "{{ _.serviceUrl }}/convertToFhir?api-version={{ _['api-version'] }}", + "name": "convertToFhir", + "description": "", + "method": "POST", + "body": { + "mimeType": "application/json", + "text": "{\n \"InputDataFormat\": \"Hl7v2\",\n \"RootTemplateName\": \"ADT_A01\",\n \"InputDataString\": \"MSH|^~\\\\&|SIMHOSP|SFAC|RAPP|RFAC|20200508131015||ADT^A01|517|T|2.3|||AL||44|ASCII\\nEVN|A01|20200508131015|||C005^Whittingham^Sylvia^^^Dr^^^DRNBR^PRSNL^^^ORGDR|\\nPID|1|3735064194^^^SIMULATOR MRN^MRN|3735064194^^^SIMULATOR MRN^MRN~2021051528^^^NHSNBR^NHSNMBR||\"\n}" + }, + "preRequestScript": "", + "parameters": [], + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "User-Agent", + "value": "insomnia/9.1.0" + } + ], + "authentication": {}, + "metaSortKey": -1714756786071, + "isPrivate": false, + "pathParameters": [], + "settingStoreCookies": true, + "settingSendCookies": true, + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingRebuildPath": true, + "settingFollowRedirects": "global", + "_type": "request" + }, + { + "_id": "req_6243b4e80e9b48579a6c5f9b04ca5406", + "parentId": "wrk_5b2338cd66814889ae6591e7a513166f", + "modified": 1714757044967, + "created": 1714756982326, + "url": "{{ _.serviceUrl }}/convertToHl7v2?api-version={{ _['api-version'] }}", + "name": "convertToHl7v2", + "description": "", + "method": "POST", + "body": { + "mimeType": "application/json", + "text": "{\n \"inputDataFormat\": \"Fhir\",\n \"RootTemplateName\": \"FhirToHl7v2/BundleToHl7v2\",\n \"InputDataString\": \"{\\\"resourceType\\\":\\\"Bundle\\\",\\\"id\\\":\\\"bundle-response-medsallergies\\\",\\\"type\\\":\\\"batch-response\\\",\\\"entry\\\":[{\\\"resource\\\":{\\\"resourceType\\\":\\\"Patient\\\",\\\"id\\\":\\\"example\\\",\\\"meta\\\":{\\\"versionId\\\":\\\"1\\\",\\\"lastUpdated\\\":\\\"2018-11-12T03:35:20.715Z\\\"},\\\"identifier\\\":[{\\\"use\\\":\\\"usual\\\",\\\"type\\\":{\\\"coding\\\":[{\\\"system\\\":\\\"http://terminology.hl7.org/CodeSystem/v2-0203\\\",\\\"code\\\":\\\"MR\\\"}]},\\\"system\\\":\\\"urn:oid:1.2.36.146.595.217.0.1\\\",\\\"value\\\":\\\"12345\\\",\\\"period\\\":{\\\"start\\\":\\\"2001-05-06\\\"},\\\"assigner\\\":{\\\"display\\\":\\\"AcmeHealthcare\\\"}}],\\\"active\\\":true,\\\"name\\\":[{\\\"use\\\":\\\"official\\\",\\\"family\\\":\\\"Chalmers\\\",\\\"given\\\":[\\\"Peter\\\",\\\"James\\\"]},{\\\"use\\\":\\\"usual\\\",\\\"given\\\":[\\\"Jim\\\"]},{\\\"use\\\":\\\"maiden\\\",\\\"family\\\":\\\"Windsor\\\",\\\"given\\\":[\\\"Peter\\\",\\\"James\\\"],\\\"period\\\":{\\\"end\\\":\\\"2002\\\"}}],\\\"telecom\\\":[{\\\"use\\\":\\\"home\\\"},{\\\"system\\\":\\\"phone\\\",\\\"value\\\":\\\"(03)55556473\\\",\\\"use\\\":\\\"work\\\",\\\"rank\\\":1},{\\\"system\\\":\\\"phone\\\",\\\"value\\\":\\\"(03)34105613\\\",\\\"use\\\":\\\"mobile\\\",\\\"rank\\\":2},{\\\"system\\\":\\\"phone\\\",\\\"value\\\":\\\"(03)55558834\\\",\\\"use\\\":\\\"old\\\",\\\"period\\\":{\\\"end\\\":\\\"2014\\\"}}],\\\"gender\\\":\\\"male\\\",\\\"birthDate\\\":\\\"1974-12-25\\\",\\\"_birthDate\\\":{\\\"extension\\\":[{\\\"url\\\":\\\"http://hl7.org/fhir/StructureDefinition/patient-birthTime\\\",\\\"valueDateTime\\\":\\\"1974-12-25T14:35:45-05:00\\\"}]},\\\"deceasedBoolean\\\":false,\\\"address\\\":[{\\\"use\\\":\\\"home\\\",\\\"type\\\":\\\"both\\\",\\\"text\\\":\\\"534ErewhonStPeasantVille,Rainbow,Vic3999\\\",\\\"line\\\":[\\\"534ErewhonSt\\\"],\\\"city\\\":\\\"PleasantVille\\\",\\\"district\\\":\\\"Rainbow\\\",\\\"state\\\":\\\"Vic\\\",\\\"postalCode\\\":\\\"3999\\\",\\\"period\\\":{\\\"start\\\":\\\"1974-12-25\\\"}}],\\\"contact\\\":[{\\\"relationship\\\":[{\\\"coding\\\":[{\\\"system\\\":\\\"http://terminology.hl7.org/CodeSystem/v2-0131\\\",\\\"code\\\":\\\"N\\\"}]}],\\\"name\\\":{\\\"family\\\":\\\"duMarché\\\",\\\"_family\\\":{\\\"extension\\\":[{\\\"url\\\":\\\"http://hl7.org/fhir/StructureDefinition/humanname-own-prefix\\\",\\\"valueString\\\":\\\"VV\\\"}]},\\\"given\\\":[\\\"Bénédicte\\\"]},\\\"telecom\\\":[{\\\"system\\\":\\\"phone\\\",\\\"value\\\":\\\"+33(237)998327\\\"}],\\\"address\\\":{\\\"use\\\":\\\"home\\\",\\\"type\\\":\\\"both\\\",\\\"line\\\":[\\\"534ErewhonSt\\\"],\\\"city\\\":\\\"PleasantVille\\\",\\\"district\\\":\\\"Rainbow\\\",\\\"state\\\":\\\"Vic\\\",\\\"postalCode\\\":\\\"3999\\\",\\\"period\\\":{\\\"start\\\":\\\"1974-12-25\\\"}},\\\"gender\\\":\\\"female\\\",\\\"period\\\":{\\\"start\\\":\\\"2012\\\"}}],\\\"managingOrganization\\\":{\\\"reference\\\":\\\"Organization/1\\\"}}},{\\\"resource\\\":{\\\"resourceType\\\":\\\"Observation\\\",\\\"id\\\":\\\"f001\\\",\\\"identifier\\\":[{\\\"use\\\":\\\"official\\\",\\\"system\\\":\\\"http://www.bmc.nl/zorgportal/identifiers/observations\\\",\\\"value\\\":\\\"6323\\\"}],\\\"status\\\":\\\"final\\\",\\\"code\\\":{\\\"coding\\\":[{\\\"system\\\":\\\"http://loinc.org\\\",\\\"code\\\":\\\"15074-8\\\",\\\"display\\\":\\\"Glucose[Moles/volume]inBlood\\\"}]},\\\"subject\\\":{\\\"reference\\\":\\\"Patient/f001\\\",\\\"display\\\":\\\"P.vandeHeuvel\\\"},\\\"effectiveDateTime\\\":\\\"2013-04-02T09:30:10+01:00\\\",\\\"issued\\\":\\\"2013-04-03T15:30:10+01:00\\\",\\\"performer\\\":[{\\\"reference\\\":\\\"Practitioner/f005\\\",\\\"display\\\":\\\"A.Langeveld\\\"}],\\\"valueQuantity\\\":{\\\"value\\\":6.3,\\\"unit\\\":\\\"mmol/l\\\",\\\"system\\\":\\\"http://unitsofmeasure.org\\\",\\\"code\\\":\\\"mmol/L\\\"},\\\"interpretation\\\":[{\\\"coding\\\":[{\\\"system\\\":\\\"http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation\\\",\\\"code\\\":\\\"H\\\",\\\"display\\\":\\\"High\\\"}]}],\\\"referenceRange\\\":[{\\\"low\\\":{\\\"value\\\":3.1,\\\"unit\\\":\\\"mmol/l\\\",\\\"system\\\":\\\"http://unitsofmeasure.org\\\",\\\"code\\\":\\\"mmol/L\\\"},\\\"high\\\":{\\\"value\\\":6.2,\\\"unit\\\":\\\"mmol/l\\\",\\\"system\\\":\\\"http://unitsofmeasure.org\\\",\\\"code\\\":\\\"mmol/L\\\"}}]}},{\\\"resource\\\":{\\\"resourceType\\\":\\\"Observation\\\",\\\"id\\\":\\\"f001\\\",\\\"identifier\\\":[{\\\"use\\\":\\\"official\\\",\\\"system\\\":\\\"http://www.bmc.nl/zorgportal/identifiers/observations\\\",\\\"value\\\":\\\"6324\\\"}],\\\"status\\\":\\\"final\\\",\\\"code\\\":{\\\"coding\\\":[{\\\"system\\\":\\\"http://loinc.org\\\",\\\"code\\\":\\\"11111-1\\\",\\\"display\\\":\\\"Another test\\\"}]},\\\"subject\\\":{\\\"reference\\\":\\\"Patient/f001\\\",\\\"display\\\":\\\"P.vandeHeuvel\\\"},\\\"effectiveDateTime\\\":\\\"2013-04-02T09:30:10+01:00\\\",\\\"issued\\\":\\\"2013-04-03T15:30:10+01:00\\\",\\\"performer\\\":[{\\\"reference\\\":\\\"Practitioner/f005\\\",\\\"display\\\":\\\"A.Langeveld\\\"}],\\\"valueQuantity\\\":{\\\"value\\\":8.0,\\\"unit\\\":\\\"mmol/l\\\",\\\"system\\\":\\\"http://unitsofmeasure.org\\\",\\\"code\\\":\\\"mmol/L\\\"},\\\"interpretation\\\":[{\\\"coding\\\":[{\\\"system\\\":\\\"http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation\\\",\\\"code\\\":\\\"H\\\",\\\"display\\\":\\\"High\\\"}]}],\\\"referenceRange\\\":[{\\\"low\\\":{\\\"value\\\":3.1,\\\"unit\\\":\\\"mmol/l\\\",\\\"system\\\":\\\"http://unitsofmeasure.org\\\",\\\"code\\\":\\\"mmol/L\\\"},\\\"high\\\":{\\\"value\\\":6.2,\\\"unit\\\":\\\"mmol/l\\\",\\\"system\\\":\\\"http://unitsofmeasure.org\\\",\\\"code\\\":\\\"mmol/L\\\"}}]}}]}\"\n}" + }, + "preRequestScript": "", + "parameters": [], + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "User-Agent", + "value": "insomnia/9.1.0" + } + ], + "authentication": {}, + "metaSortKey": -1714756618324.5, + "isPrivate": false, + "pathParameters": [], + "settingStoreCookies": true, + "settingSendCookies": true, + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingRebuildPath": true, + "settingFollowRedirects": "global", + "_type": "request" + }, + { + "_id": "req_0877a3f267cf4689b3a30386da70dedf", + "parentId": "wrk_5b2338cd66814889ae6591e7a513166f", + "modified": 1714757111131, + "created": 1714756450578, + "url": "{{ _.serviceUrl }}/{{ _['api-version'] }}/swagger.yaml", + "name": "swagger", + "description": "", + "method": "GET", + "body": {}, + "preRequestScript": "", + "parameters": [], + "headers": [ + { + "name": "User-Agent", + "value": "insomnia/9.1.0" + } + ], + "authentication": {}, + "metaSortKey": -1714756450578, + "isPrivate": false, + "pathParameters": [], + "settingStoreCookies": true, + "settingSendCookies": true, + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingRebuildPath": true, + "settingFollowRedirects": "global", + "_type": "request" + }, + { + "_id": "env_1bbd43d1a44ae66f30c72e943eedde6791898111", + "parentId": "wrk_5b2338cd66814889ae6591e7a513166f", + "modified": 1714756918731, + "created": 1714756439855, + "name": "Base Environment", + "data": { + "serviceUrl": "", + "api-version": "2024-05-01-preview" + }, + "dataPropertyOrder": { + "&": [ + "serviceUrl", + "api-version" + ] + }, + "color": null, + "isPrivate": false, + "metaSortKey": 1714756439855, + "_type": "environment" + }, + { + "_id": "jar_1bbd43d1a44ae66f30c72e943eedde6791898111", + "parentId": "wrk_5b2338cd66814889ae6591e7a513166f", + "modified": 1714756439860, + "created": 1714756439860, + "name": "Default Jar", + "cookies": [], + "_type": "cookie_jar" + } + ] +} \ No newline at end of file diff --git a/docs/samples/Messages/CCD/Request/CCD.json b/docs/samples/Messages/CCD/Request/CCD.json new file mode 100644 index 000000000..41b60ba4c --- /dev/null +++ b/docs/samples/Messages/CCD/Request/CCD.json @@ -0,0 +1,5 @@ +{ + "InputDataString": " Continuity of Care Document (C-CDA) 1 Happy Valley Road Westerly RI 02891 Nelson Lisa River Valley Health Services 823 Main Street River Valley RI 028321 US ... ... ", + "InputDataFormat": "Ccda", + "RootTemplateName": "CCD" +} diff --git a/docs/samples/Messages/CCD/Response/Expected_Response_CCD.json b/docs/samples/Messages/CCD/Response/Expected_Response_CCD.json new file mode 100644 index 000000000..b913a07a1 --- /dev/null +++ b/docs/samples/Messages/CCD/Response/Expected_Response_CCD.json @@ -0,0 +1,188 @@ +{ + "result": { + "resourceType": "Bundle", + "type": "batch", + "entry": [ + { + "fullUrl": "urn:uuid:8a380dfe-677f-ca39-ea5a-be89a7d1fe11", + "resource": { + "resourceType": "Composition", + "id": "8a380dfe-677f-ca39-ea5a-be89a7d1fe11", + "identifier": { + "use": "official", + "value": "2.16.840.1.113883.3.109" + }, + "status": "final", + "type": { + "coding": [ + { + "code": "34133-9", + "display": "SUMMARIZATION OF EPISODE NOTE", + "system": "http://loinc.org" + } + ] + }, + "date": "2017-05-28T15:02:00-04:00", + "title": "Continuity of Care Document (C-CDA)", + "confidentiality": "N", + "subject": { + "reference": "Patient/44b52cd8-5266-7209-ab40-15fb16fc9ba9" + } + }, + "request": { + "method": "PUT", + "url": "Composition/8a380dfe-677f-ca39-ea5a-be89a7d1fe11" + } + }, + { + "fullUrl": "urn:uuid:44b52cd8-5266-7209-ab40-15fb16fc9ba9", + "resource": { + "resourceType": "Patient", + "id": "44b52cd8-5266-7209-ab40-15fb16fc9ba9", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient" + ] + }, + "identifier": [ + { + "system": "urn:oid:1.3.6.1.4.1.41179.2.4", + "value": "lisarnelson@direct.myphd.us" + }, + { + "system": "urn:oid:2.16.840.1.113883.1.111.12345", + "value": "12345-0828", + "assigner": { + "display": "River Valley Health Services local patient Medical Record Number" + } + } + ], + "name": [ + { + "use": "usual", + "family": "Nelson", + "given": [ + "Lisa" + ] + } + ], + "birthDate": "1962-08-28", + "gender": "female", + "extension": [ + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "code": "2106-3", + "display": "White" + } + }, + { + "url": "text", + "valueString": "White" + } + ] + }, + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "code": "2186-5", + "display": "Not Hispanic or Latino" + } + }, + { + "url": "text", + "valueString": "Not Hispanic or Latino" + } + ] + } + ], + "address": [ + { + "line": [ + "1 Happy Valley Road" + ], + "city": "Westerly", + "state": "RI", + "postalCode": "02891" + } + ], + "telecom": [ + { + "system": "phone", + "value": "+1-4013482345", + "use": "work" + }, + { + "system": "phone", + "value": "+1-4016412345", + "use": "home" + }, + { + "system": "email", + "value": "lisanelson@gmail.com" + }, + { + "system": "email", + "value": "lisarnelson@direct.myphd.us" + } + ], + "communication": [ + { + "language": { + "coding": [ + { + "system": "urn:ietf:bcp:47", + "code": "en", + "display": "English" + } + ] + }, + "preferred": true + } + ] + }, + "request": { + "method": "PUT", + "url": "Patient/44b52cd8-5266-7209-ab40-15fb16fc9ba9" + } + }, + { + "fullUrl": "urn:uuid:9629b055-681d-b1ec-df70-a7e6678ba6f4", + "resource": { + "resourceType": "DocumentReference", + "id": "9629b055-681d-b1ec-df70-a7e6678ba6f4", + "type": { + "coding": [ + { + "code": "34133-9", + "display": "SUMMARIZATION OF EPISODE NOTE", + "system": "http://loinc.org" + } + ] + }, + "date": "2024-02-22T15:35:13.238Z", + "status": "current", + "content": [ + { + "attachment": { + "contentType": "text/plain", + "data": "H4sIAAAAAAAACq1Y7XLiNhR9FdV/0p3WNv6AQAbYTYFsmAbIALvb6Z+OsAXWjC1RWSahT98r2xBjG8LurCdhjHR1v6VzRPfjaxSiHREx5aynWUZD+9jvqkE9lvuQxAEhEsn9lvRuJHmV5msc3qBAkHXv5gYkByFl1MPhkHtJRJhEsJLFd68x7WmBlNs703x5eTFeHIOLjWk3Gpb51+Rp4QUkwjplscTMI1q2qqclgt0F4a0Osnc7Jx++i+i6PGXC2GF6x73KNIwdpmNflubViNZH8HQFwWE04D5BHnz0tC8LzcxmVMhjHwnOZU+zDatltN2GYRmW5bTbDryAe5APwrLEPc8Gw38ehw143MZRB4m2IZYX9TQMG/5gwrB+bJl9WEbPi6sFnRN/V+vWysEtW3ftjqO7tytX76wtV/fabbuDrWZr7a4Oir23/Diu5Tg6qPJpDD7upziC0cWXyeR+Pv77fjmeTdHsAY2ex4vZcISms+VIUzrUozQs9jFEV+diC+IviGSKn2bj6eCYFipD0h9wJilLqNwjvkYDLAg69t6vA30wvP/QNTPRdBVZr4kn6Y4saUTQDocJ6LUb1m2jabetZgNaUoeSHWvmcbamPmijOAQjhd6YlqKechHhEC0CLCjbaO/E1zTsZk0qMlWPT7docGr44E+I2SbBG1JwhDD9rU8F8bjwl1hsiExH0tEtlhRUzXmehuPEL7qOHgkkjcYIo22yCqmHoHNkgCUKMAyytE0Eg9AighmEhlY4Jj7iDFaoDkOz8TBbAEoyFTgM9yjzfk3xKiQG0vUTy5nprEFh86h6G676t6zbjmEbrqa0yYAgHMd0o+zeJ5ILSIXKEVpzcaLu8AwpZECipUhieROj5yxwE7IZQ1MIhH1fkDgmMdKGS3UIoWdo09F0qZU9fNtAZ/wrbJ+QxlgwEsacffJTD4xovw18I4mPRT4+tQH1yu5k9TzJ1oSrQvkxivAerQhKoAyVxJ6UVOUvrz0kYzKfIqjS/OvjAp2Ptu5wsyz4tx23eRJ1OqI32nZbuxRjUAhyDjtPoK/QH2QPXuJQBmhBxI56UJGQQ+Mc/EUT4qtGQvO0o9E0iVZEaKiUF1XP08ZSo7EUgFP3Wa2fKCN9Cz3i7XZ/MD3n2O+aVbGKJg9c738jsDdFuO+a6dcac3A89+djpVG9VQS2HCZCtWf7DbvdsbpmYaRqkydMij1iSRg+hHjHBSDR9M9yS5iV2AEqQshWpDqjp3171g4HHIzf/WbpbsNy3HZaR/PCwse6hS3XurQwl48wDSW/U/sh3w4bNWSAyPUrz+2kkoK8Uar5Y+qASCN5ynC9IrHGEQ33/Wlqp2vmX2tFN9CxDP2bwCm8pgQqMQClT+Bl10ynquZNZb86jP0IuBG0HLi9I58J84konOIPJTh5AEYUAhWqed5DlhrkVIhyX+NAKaepoysqZFCER6vTstNdXiMcAdpBIy+g75O4EM6kFM4EC0GJfwUsvhPyWzyTouk63wT2ijhpW42W7pT8+hZQCVl+j4zYTvsqvwbDAZqDWcBNH41kAGT4DbxPnCPp5GfBk+2Jj+2W3qwwC4keYQDQ10OAe09QQMZ/oDXyOH7c6Tf2EUWJIvoSgKB+31zBWh2j3YZ3w7GPrPWCxQLf2ZyT3sJFBKAP7hBjCOVwhomE1AVjvh8NnNM1hwyY4TtgN2ImNlCT/+qzkALxMgAULnGrl0Chs4LmlD3BrCAb2JcwCsSLI8oU8MlMZJoqBxh8zm0iIFtlLvUOfrtG6xS3XeAw7q3Ths1Q85zD7jpX6vJ6bdx5mIqlHISPmnMal0NzlT0e7JwnaNbPIZADvkvrMgIqC3eMKn0cwI3mYj3OOFeoh9gF8Sf1YRwQD87JyyxxMILbzYlrH86V4g9g4swLE19R9wJjx1IKukpklgqMgpSNeeoGxQt9/Tva8wR5cA8ARM1LxWBFlM6qfi0/x/7FJe4JKQH6md4UihaM72znEzr6PQ38s8ln6mgK9pc0n+MD73O1ptNxHfvwO8BVi3MWVddRNVpqyXM6U2XGQBsBcSlDi3TqGvKcakoZczE/Z0j0VUS6hkxDgi6y6cyJjFH3vyzAev5eEarh0+bFk/6IDsd7ddes3L8Nw8hdiLacFaFEJTnxZAJ7+A/uFxzKl+Q2aqRUEAdtXbP8m1v/f1GSmZjFEwAA", + "hash": "ODQ0NzVhZTBjOTNjODM5YTk3MjVlNDAzNDY2ZWM4Zjc1MmMxNjYzNg==" + } + } + ] + }, + "request": { + "method": "PUT", + "url": "DocumentReference/9629b055-681d-b1ec-df70-a7e6678ba6f4" + } + } + ] + } +} diff --git a/docs/samples/Messages/FHIR/Request/FHIR_Bundle.json b/docs/samples/Messages/FHIR/Request/FHIR_Bundle.json new file mode 100644 index 000000000..5fef68c6b --- /dev/null +++ b/docs/samples/Messages/FHIR/Request/FHIR_Bundle.json @@ -0,0 +1,5 @@ +{ + "InputDataString": "{\"resourceType\":\"Bundle\",\"id\":\"bundle-response-medsallergies\",\"type\":\"batch-response\",\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"example\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2018-11-12T03:35:20.715Z\"},\"identifier\":[{\"use\":\"usual\",\"type\":{\"coding\":[{\"system\":\"http://terminology.hl7.org/CodeSystem/v2-0203\",\"code\":\"MR\"}]},\"system\":\"urn:oid:1.2.36.146.595.217.0.1\",\"value\":\"12345\",\"period\":{\"start\":\"2001-05-06\"},\"assigner\":{\"display\":\"AcmeHealthcare\"}}],\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Chalmers\",\"given\":[\"Peter\",\"James\"]},{\"use\":\"usual\",\"given\":[\"Jim\"]},{\"use\":\"maiden\",\"family\":\"Windsor\",\"given\":[\"Peter\",\"James\"],\"period\":{\"end\":\"2002\"}}],\"telecom\":[{\"use\":\"home\"},{\"system\":\"phone\",\"value\":\"(03)55556473\",\"use\":\"work\",\"rank\":1},{\"system\":\"phone\",\"value\":\"(03)34105613\",\"use\":\"mobile\",\"rank\":2},{\"system\":\"phone\",\"value\":\"(03)55558834\",\"use\":\"old\",\"period\":{\"end\":\"2014\"}}],\"gender\":\"male\",\"birthDate\":\"1974-12-25\",\"_birthDate\":{\"extension\":[{\"url\":\"http://hl7.org/fhir/StructureDefinition/patient-birthTime\",\"valueDateTime\":\"1974-12-25T14:35:45-05:00\"}]},\"deceasedBoolean\":false,\"address\":[{\"use\":\"home\",\"type\":\"both\",\"text\":\"534ErewhonStPeasantVille,Rainbow,Vic3999\",\"line\":[\"534ErewhonSt\"],\"city\":\"PleasantVille\",\"district\":\"Rainbow\",\"state\":\"Vic\",\"postalCode\":\"3999\",\"period\":{\"start\":\"1974-12-25\"}}],\"contact\":[{\"relationship\":[{\"coding\":[{\"system\":\"http://terminology.hl7.org/CodeSystem/v2-0131\",\"code\":\"N\"}]}],\"name\":{\"family\":\"duMarché\",\"_family\":{\"extension\":[{\"url\":\"http://hl7.org/fhir/StructureDefinition/humanname-own-prefix\",\"valueString\":\"VV\"}]},\"given\":[\"Bénédicte\"]},\"telecom\":[{\"system\":\"phone\",\"value\":\"+33(237)998327\"}],\"address\":{\"use\":\"home\",\"type\":\"both\",\"line\":[\"534ErewhonSt\"],\"city\":\"PleasantVille\",\"district\":\"Rainbow\",\"state\":\"Vic\",\"postalCode\":\"3999\",\"period\":{\"start\":\"1974-12-25\"}},\"gender\":\"female\",\"period\":{\"start\":\"2012\"}}],\"managingOrganization\":{\"reference\":\"Organization/1\"}}},{\"resource\":{\"resourceType\":\"Observation\",\"id\":\"f001\",\"identifier\":[{\"use\":\"official\",\"system\":\"http://www.bmc.nl/zorgportal/identifiers/observations\",\"value\":\"6323\"}],\"status\":\"final\",\"code\":{\"coding\":[{\"system\":\"http://loinc.org\",\"code\":\"15074-8\",\"display\":\"Glucose[Moles/volume]inBlood\"}]},\"subject\":{\"reference\":\"Patient/f001\",\"display\":\"P.vandeHeuvel\"},\"effectiveDateTime\":\"2013-04-02T09:30:10+01:00\",\"issued\":\"2013-04-03T15:30:10+01:00\",\"performer\":[{\"reference\":\"Practitioner/f005\",\"display\":\"A.Langeveld\"}],\"valueQuantity\":{\"value\":6.3,\"unit\":\"mmol/l\",\"system\":\"http://unitsofmeasure.org\",\"code\":\"mmol/L\"},\"interpretation\":[{\"coding\":[{\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation\",\"code\":\"H\",\"display\":\"High\"}]}],\"referenceRange\":[{\"low\":{\"value\":3.1,\"unit\":\"mmol/l\",\"system\":\"http://unitsofmeasure.org\",\"code\":\"mmol/L\"},\"high\":{\"value\":6.2,\"unit\":\"mmol/l\",\"system\":\"http://unitsofmeasure.org\",\"code\":\"mmol/L\"}}]}},{\"resource\":{\"resourceType\":\"Observation\",\"id\":\"f001\",\"identifier\":[{\"use\":\"official\",\"system\":\"http://www.bmc.nl/zorgportal/identifiers/observations\",\"value\":\"6324\"}],\"status\":\"final\",\"code\":{\"coding\":[{\"system\":\"http://loinc.org\",\"code\":\"11111-1\",\"display\":\"Anothertest\"}]},\"subject\":{\"reference\":\"Patient/f001\",\"display\":\"P.vandeHeuvel\"},\"effectiveDateTime\":\"2013-04-02T09:30:10+01:00\",\"issued\":\"2013-04-03T15:30:10+01:00\",\"performer\":[{\"reference\":\"Practitioner/f005\",\"display\":\"A.Langeveld\"}],\"valueQuantity\":{\"value\":8.0,\"unit\":\"mmol/l\",\"system\":\"http://unitsofmeasure.org\",\"code\":\"mmol/L\"},\"interpretation\":[{\"coding\":[{\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation\",\"code\":\"H\",\"display\":\"High\"}]}],\"referenceRange\":[{\"low\":{\"value\":3.1,\"unit\":\"mmol/l\",\"system\":\"http://unitsofmeasure.org\",\"code\":\"mmol/L\"},\"high\":{\"value\":6.2,\"unit\":\"mmol/l\",\"system\":\"http://unitsofmeasure.org\",\"code\":\"mmol/L\"}}]}}]}", + "InputDataFormat": "Fhir", + "RootTemplateName": "BundleToHl7v2" +} diff --git a/docs/samples/Messages/FHIR/Request/FHIR_STU3.json b/docs/samples/Messages/FHIR/Request/FHIR_STU3.json new file mode 100644 index 000000000..f7522db52 --- /dev/null +++ b/docs/samples/Messages/FHIR/Request/FHIR_STU3.json @@ -0,0 +1,5 @@ +{ + "InputDataString": "{\"resourceType\": \"Patient\", \"id\": 12434, \"animal\": {\"species\": {\"coding\": [ {\"system\": \"http://hl7.org/fhir/animal-species\", \"code\": \"canislf\", \"display\": \"Dog\"}]}}}", + "InputDataFormat": "Fhir_STU3", + "RootTemplateName": "Patient" +} diff --git a/docs/samples/Messages/FHIR/Response/Expected_Response_FHIR_STU3.json b/docs/samples/Messages/FHIR/Response/Expected_Response_FHIR_STU3.json new file mode 100644 index 000000000..1b37d0023 --- /dev/null +++ b/docs/samples/Messages/FHIR/Response/Expected_Response_FHIR_STU3.json @@ -0,0 +1,25 @@ +{ + "result": { + "resourceType": "Patient", + "id": 12434, + "extension": [ + { + "url": "http://hl7.org/fhir/r4/StructureDefinition/patient-animal", + "extension": [ + { + "url": "species", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/animal-species", + "code": "canislf", + "display": "Dog" + } + ] + } + } + ] + } + ] + } +} diff --git a/docs/samples/Messages/HL7v2/Request/ADT_A01.json b/docs/samples/Messages/HL7v2/Request/ADT_A01.json new file mode 100644 index 000000000..96f3d78a9 --- /dev/null +++ b/docs/samples/Messages/HL7v2/Request/ADT_A01.json @@ -0,0 +1,5 @@ +{ + "InputDataString": "MSH|^~\\&|SIMHOSP|SFAC|RAPP|RFAC|20200508131015||ADT^A01|517|T|2.3|||AL||44|ASCII\nEVN|A01|20200508131015|||C005^Whittingham^Sylvia^^^Dr^^^DRNBR^PRSNL^^^ORGDR|\nPID|1|3735064194^^^SIMULATOR MRN^MRN|3735064194^^^SIMULATOR MRN^MRN~2021051528^^^NHSNBR^NHSNMBR||", + "InputDataFormat": "Hl7v2", + "RootTemplateName": "ADT_A01" +} diff --git a/docs/samples/Messages/HL7v2/Response/Expected_Response_ADT_A01.json b/docs/samples/Messages/HL7v2/Response/Expected_Response_ADT_A01.json new file mode 100644 index 000000000..86f827a33 --- /dev/null +++ b/docs/samples/Messages/HL7v2/Response/Expected_Response_ADT_A01.json @@ -0,0 +1,264 @@ +{ + "result": { + "resourceType": "Bundle", + "type": "batch", + "timestamp": "2020-05-08T13:10:15Z", + "identifier": { + "value": "517" + }, + "id": "7dcb7d92-7a75-3d65-42f9-0f790afac4db", + "entry": [ + { + "fullUrl": "urn:uuid:aa521dd9-b613-0210-a661-82ce17e38fb3", + "resource": { + "resourceType": "MessageHeader", + "id": "aa521dd9-b613-0210-a661-82ce17e38fb3", + "source": { + "name": "SIMHOSP", + "_endpoint": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + } + }, + "destination": [ + { + "name": "RAPP", + "_endpoint": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + }, + "receiver": { + "reference": "Organization/ab0676d9-fa24-ffd3-327d-f5b4c26ae628" + } + } + ], + "meta": { + "tag": [ + { + "code": "T", + "system": "http://terminology.hl7.org/CodeSystem/v2-0103" + } + ] + }, + "eventCoding": { + "code": "A01", + "system": "http://terminology.hl7.org/CodeSystem/v2-0003", + "display": "ADT^A01" + }, + "sender": { + "reference": "Organization/33e23f3e-47b2-9c1b-c1f0-f964c2d28946" + } + }, + "request": { + "method": "PUT", + "url": "MessageHeader/aa521dd9-b613-0210-a661-82ce17e38fb3" + } + }, + { + "fullUrl": "urn:uuid:428efe87-9b32-2610-d2d2-0edfdc356ebe", + "resource": { + "resourceType": "Provenance", + "id": "428efe87-9b32-2610-d2d2-0edfdc356ebe", + "occurredDateTime": "2020-05-08T13:10:15Z", + "recorded": "2020-05-08T13:10:15Z", + "agent": [ + { + "type": { + "coding": [ + { + "code": "author", + "system": "http://terminology.hl7.org/CodeSystem/provenance-participant-type" + } + ] + }, + "who": { + "reference": "Organization/33e23f3e-47b2-9c1b-c1f0-f964c2d28946" + } + } + ], + "activity": { + "coding": [ + { + "display": "ADT^A01^" + } + ] + }, + "target": [ + { + "reference": "Bundle/7dcb7d92-7a75-3d65-42f9-0f790afac4db" + } + ] + }, + "request": { + "method": "PUT", + "url": "Provenance/428efe87-9b32-2610-d2d2-0edfdc356ebe" + } + }, + { + "fullUrl": "urn:uuid:33e23f3e-47b2-9c1b-c1f0-f964c2d28946", + "resource": { + "resourceType": "Organization", + "id": "33e23f3e-47b2-9c1b-c1f0-f964c2d28946", + "identifier": [ + { + "value": "SFAC", + "system": "http://example.com/v2-to-fhir-converter/Identifier/SFAC" + } + ], + "address": [ + { + "country": "44" + } + ] + }, + "request": { + "method": "PUT", + "url": "Organization/33e23f3e-47b2-9c1b-c1f0-f964c2d28946" + } + }, + { + "fullUrl": "urn:uuid:ab0676d9-fa24-ffd3-327d-f5b4c26ae628", + "resource": { + "resourceType": "Organization", + "id": "ab0676d9-fa24-ffd3-327d-f5b4c26ae628", + "identifier": [ + { + "value": "RFAC", + "system": "http://example.com/v2-to-fhir-converter/Identifier/RFAC" + } + ], + "address": [ + { + "country": "44" + } + ] + }, + "request": { + "method": "PUT", + "url": "Organization/ab0676d9-fa24-ffd3-327d-f5b4c26ae628" + } + }, + { + "fullUrl": "urn:uuid:9d697ec3-48c3-3e17-db6a-29a1765e22c6", + "resource": { + "resourceType": "Patient", + "id": "9d697ec3-48c3-3e17-db6a-29a1765e22c6", + "identifier": [ + { + "value": "3735064194", + "type": { + "coding": [ + { + "code": "MRN", + "display": "MRN" + } + ] + }, + "system": "http://example.com/v2-to-fhir-converter/Identifier/SIMULATOR-MRN" + }, + { + "value": "3735064194", + "type": { + "coding": [ + { + "code": "MRN", + "display": "MRN" + } + ] + }, + "system": "http://example.com/v2-to-fhir-converter/Identifier/SIMULATOR-MRN" + }, + { + "value": "2021051528", + "type": { + "coding": [ + { + "code": "NHSNMBR", + "display": "NHSNMBR" + } + ] + }, + "system": "http://example.com/v2-to-fhir-converter/Identifier/NHSNBR" + } + ] + }, + "request": { + "method": "PUT", + "url": "Patient/9d697ec3-48c3-3e17-db6a-29a1765e22c6" + } + }, + { + "fullUrl": "urn:uuid:4f1394cd-4fdc-2671-f985-0ef923c48c5c", + "resource": { + "resourceType": "Provenance", + "id": "4f1394cd-4fdc-2671-f985-0ef923c48c5c", + "recorded": "2020-05-08T13:10:15Z", + "agent": [ + { + "who": { + "reference": "Practitioner/07cb183d-bae8-cbc9-56cc-764ecfa02850" + } + } + ], + "activity": { + "text": "ADT^A01" + }, + "target": [ + { + "reference": "Bundle/7dcb7d92-7a75-3d65-42f9-0f790afac4db" + } + ] + }, + "request": { + "method": "PUT", + "url": "Provenance/4f1394cd-4fdc-2671-f985-0ef923c48c5c" + } + }, + { + "fullUrl": "urn:uuid:07cb183d-bae8-cbc9-56cc-764ecfa02850", + "resource": { + "resourceType": "Practitioner", + "id": "07cb183d-bae8-cbc9-56cc-764ecfa02850", + "identifier": [ + { + "value": "C005", + "system": "http://example.com/v2-to-fhir-converter/Identifier/DRNBR", + "type": { + "coding": [ + { + "code": "ORGDR", + "display": "ORGDR" + } + ] + } + } + ], + "name": [ + { + "family": "Whittingham", + "given": [ + "Sylvia" + ], + "prefix": [ + "Dr" + ], + "use": "PRSNL" + } + ] + }, + "request": { + "method": "PUT", + "url": "Practitioner/07cb183d-bae8-cbc9-56cc-764ecfa02850" + } + } + ] + } +} diff --git a/docs/samples/Messages/HL7v2/Response/Expected_Response_FHIR_Bundle.json b/docs/samples/Messages/HL7v2/Response/Expected_Response_FHIR_Bundle.json new file mode 100644 index 000000000..eedff2a01 --- /dev/null +++ b/docs/samples/Messages/HL7v2/Response/Expected_Response_FHIR_Bundle.json @@ -0,0 +1,3 @@ +{ + "result": "FHS|^~\\&|TestSystem|\nBHS|^~\\&|TestSystem|\nMSH|^~\\&|TestSystem||TransformationAgent||123||ORU^R01|1|T|2.5|\nPID|||12345|\nOBR||6323|15074-8^Glucose [Moles/volume] in Blood^LN||||||||||||||||||||||F|\nOBX|||||6.3|mmol/l|3.1-6.2|H|\nMSH|^~\\&|TestSystem||TransformationAgent||123||ORU^R01|1|T|2.5|\nPID|||12345|\nOBR||6324|11111-1^Another test^LN||||||||||||||||||||||F|\nOBX|||||8|mmol/l|3.1-6.2|H|\nBTS|1|\nFTS|1|\n" +} \ No newline at end of file diff --git a/docs/samples/Messages/JSON/Request/Sample_Json.json b/docs/samples/Messages/JSON/Request/Sample_Json.json new file mode 100644 index 000000000..5c2a3cbae --- /dev/null +++ b/docs/samples/Messages/JSON/Request/Sample_Json.json @@ -0,0 +1,5 @@ +{ + "InputDataString": "{\"PatientId\": 12434, \"MRN\": \"M0R1N2\",\"FirstName\": \"Jerry\", \"LastName\": \"Smith\", \"Phone Number\": [\"1234-5678\", \"1234-5679\"], \"Gender\": \"M\", \"DOB\": \"20010110\"}", + "InputDataFormat": "Json", + "RootTemplateName": "ExamplePatient" +} diff --git a/docs/samples/Messages/JSON/Response/Expected_Response_Sample_Json.json b/docs/samples/Messages/JSON/Response/Expected_Response_Sample_Json.json new file mode 100644 index 000000000..806b57cc9 --- /dev/null +++ b/docs/samples/Messages/JSON/Response/Expected_Response_Sample_Json.json @@ -0,0 +1,46 @@ +{ + "result": { + "resourceType": "Patient", + "id": "e8b835f7-0c46-4166-22ad-23e6783aaf54", + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "urn:oid:2.16.840.1.113883.19.5", + "value": "M0R1N2" + } + ], + "active": true, + "name": [ + { + "family": "Smith", + "given": [ + "Jerry" + ] + } + ], + "telecom": [ + { + "system": "phone", + "value": "1234-5678" + }, + { + "system": "phone", + "value": "1234-5679" + } + ], + "gender": "male", + "birthDate": "2001-01-10", + "managingOrganization": { + "reference": "Organization/2.16.840.1.113883.19.5", + "display": "Good Health Clinic" + } + } +}