Skip to content

Commit

Permalink
Openemr fixes openemr#5215 bulk export provenance, fixes openemr#5216
Browse files Browse the repository at this point in the history
…api docs (openemr#5217)

* Fixes openemr#5215 provenance algorithm corrections

Made the provenance use its own logic instead of relying on the bulk
export trait.  We first only grab resources that have been included in
the JOB (which are filtered by scopes and patient queries).  Then we
correctly set the patient filters on the individual sub resources.
Lastly instead of wrapping everything up in a processing result which is
memory and cpu intensive, we write things out to the stream as we go
through the system.

* Fixes openemr#5216 document fhir native apps

Update readme to include document for native apps.

* Fix styles
  • Loading branch information
adunsulag authored Apr 21, 2022
1 parent 8c3369f commit 2b3f2ef
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 11 deletions.
13 changes: 13 additions & 0 deletions FHIR_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- [Patient Export](FHIR_README.md#bulk-fhir-exports)
- [Group Export](FHIR_README.md#bulk-fhir-exports)
- [3rd Party SMART Apps](FHIR_README.md#3rd-party-smart-apps)
- [Native Applications](FHIR_README.md#native-applications)
- [For Developers](FHIR_README.md#for-developers)

## Overview
Expand Down Expand Up @@ -176,6 +177,18 @@ You can revoke an access token two ways. One from the API Client edit screen, f
The second way is if you have the fully encoded access token using the API Client Tools screen. Go to Admin->System->API Clients and then click on the Token Tools button. Paste in the entire encoded token and then select Parse Token. Information about the token will be displayed including the authenticated user that authorized the token. Now select the Revoke Token button to revoke the token. A success message will be displayed when the revocation completes. You can parse the token again to see that the token has been revoked.
## Native Applications
Interoperability requirements with OpenEMR for Native Applications
- Native applications wishing to use the OpenEMR FHIR API with refresh tokens MUST be capable of storing the refresh token in a secure manner similar to the requirements of storing a secret for confidential apps.
- Native applications must register their application as a confidential app
- Native applications must request the offline_scope in their initial API request in order to receive a refresh token
- Native application refresh tokens are valid for 3 months before they must be renewed.
- Native applications can only communicate with OpenEMR over a TLS secured channel in order to ensure the safe transmission of the refresh token.
- Native applications must use the Authorization Code grant flow in order to receive a refresh token.
It is recommended that native applications follow best practices for native client applications as outlined in RFC 8252 OAuth 2.0 for Native Apps.
## For Developers
FHIR endpoints are defined in the [primary routes file](_rest_routes.inc.php). The routes file maps an external, addressable
Expand Down
6 changes: 6 additions & 0 deletions src/FHIR/Export/ExportStreamWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

namespace OpenEMR\FHIR\Export;

use OpenEMR\Common\Logging\SystemLogger;
use OpenEMR\FHIR\R4\FHIRResource;

class ExportStreamWriter
Expand Down Expand Up @@ -81,6 +82,11 @@ public function append(FHIRResource $resource)
$this->incrementRecordCount();
$this->lastProcessedId = $resource->getId();
if ($this->willShutdown()) {
(new SystemLogger())->debug(
"ExportStreamWriter->append() reached shutdown time limit for export",
['lastProcessedId' => $this->lastProcessedId, 'resource' => $resource->get_fhirElementName()]
);

throw new ExportWillShutdownException("Export time has exceeded shutdown limit", 0, $this->lastProcessedId);
}
} catch (\JsonException $exception) {
Expand Down
16 changes: 16 additions & 0 deletions src/Services/FHIR/FhirEncounterService.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,22 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false)
}
}

public function createProvenanceResource($dataRecord = array(), $encode = false)
{
if (!($dataRecord instanceof FHIREncounter)) {
throw new \BadMethodCallException("Data record should be correct instance class");
}
$provenanceService = new FhirProvenanceService();
$author = null;
if (!empty($dataRecord->getParticipant())) {
// grab the first one for author
$participant = reset($dataRecord->getParticipant());
$author = $participant->getIndividual() ?? null;
}
$provenance = $provenanceService->createProvenanceForDomainResource($dataRecord, $author);
return $provenance;
}

/**
* Searches for OpenEMR records using OpenEMR search parameters
*
Expand Down
91 changes: 80 additions & 11 deletions src/Services/FHIR/FhirProvenanceService.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@

use OpenEMR\Common\Logging\SystemLogger;
use OpenEMR\Common\System\System;
use OpenEMR\FHIR\Export\ExportCannotEncodeException;
use OpenEMR\FHIR\Export\ExportException;
use OpenEMR\FHIR\Export\ExportJob;
use OpenEMR\FHIR\Export\ExportStreamWriter;
use OpenEMR\FHIR\Export\ExportWillShutdownException;
use OpenEMR\FHIR\R4\FHIRDomainResource\FHIROrganization;
use OpenEMR\FHIR\R4\FHIRDomainResource\FHIRProvenance;
use OpenEMR\FHIR\R4\FHIRElement\FHIRCodeableConcept;
Expand All @@ -39,7 +44,6 @@ class FhirProvenanceService extends FhirServiceBase implements IResourceUSCIGPro
{
use FhirServiceBaseEmptyTrait;
use BulkExportSupportAllOperationsTrait;
use FhirBulkExportDomainResourceTrait;

// Note: FHIR 4.0.1 id columns put a constraint on ids such that:
// Ids can be up to 64 characters long, and contain any combination of upper and lowercase ASCII letters,
Expand Down Expand Up @@ -248,22 +252,13 @@ private function getAllProvenanceRecordsFromServices($puuidBind = null)
// we only return provenances for
$servicesByResource = $this->serviceLocator->findServices(IResourceUSCIGProfileService::class);

$searchParams = ['_revinclude' => 'Provenance:target'];
foreach ($servicesByResource as $resource => $service) {
// if it doesn't support the readable service we've got issues
if ($resource == 'Provenance' || !($service instanceof IResourceReadableService)) {
continue;
}
try {
$serviceResult = $service->getAll($searchParams, $puuidBind);
// now loop through and grab all of our provenance resources
if ($serviceResult->hasData()) {
foreach ($serviceResult->getData() as $record) {
if ($record instanceof FHIRProvenance) {
$processingResult->addData($record);
}
}
}
$this->addAllProvenanceRecordsForService($processingResult, $service, [], $puuidBind);
} catch (SearchFieldException $ex) {
$systemLogger = new SystemLogger();
$systemLogger->error(get_class($this) . "->getAll() exception thrown", ['message' => $exception->getMessage(),
Expand All @@ -282,6 +277,20 @@ private function getAllProvenanceRecordsFromServices($puuidBind = null)
return $processingResult;
}

private function addAllProvenanceRecordsForService(ProcessingResult $processingResult, $service, array $searchParams, $puuidBind = null)
{
$searchParams['_revinclude'] = 'Provenance:target';
$serviceResult = $service->getAll($searchParams, $puuidBind);
// now loop through and grab all of our provenance resources
if ($serviceResult->hasData()) {
foreach ($serviceResult->getData() as $record) {
if ($record instanceof FHIRProvenance) {
$processingResult->addData($record);
}
}
}
}

/**
* Given a provenance record id retrieve the provenance record for the given resource and its uuid
* @param $id string in the format of <resource>:<uuid>
Expand Down Expand Up @@ -436,4 +445,64 @@ public function splitSurrogateKeyIntoParts($key)
];
return $key;
}
/**
* Grabs all the objects in my service that match the criteria specified in the ExportJob. If a
* $lastResourceIdExported is provided, The service executes the same data collection query it used previously and
* startes processing at the resource that is immediately after (ordered by date) the resource that matches the id of
* $lastResourceIdExported. This allows processing of the service to be resumed or paused.
* @param ExportStreamWriter $writer Object that writes out to a stream any object that extend the FhirResource object
* @param ExportJob $job The export job we are processing the request for. Holds all of the context information needed for the export service.
* @return void
* @throws ExportWillShutdownException Thrown if the export is about to be shutdown and all processing must be halted.
* @throws ExportException If there is an error in processing the export
* @throws ExportCannotEncodeException Thrown if the resource cannot be properly converted into the right format (ie JSON).
*/
public function export(ExportStreamWriter $writer, ExportJob $job, $lastResourceIdExported = null): void
{
if (!($this instanceof IResourceReadableService)) {
// we need to ensure we only get called in a method that implements the getAll method.
throw new \BadMethodCallException("Trait can only be used in classes that implement the " . IResourceReadableService::class . " interface");
}
$type = $job->getExportType();

// algorithm
// go through each resource and grab the related service
// check if the service is a PatientCompartment resource, if so, set the patient uuids to export
// if we are a Medication request since we are using RXCUI for our drug formulariesthere is no Provenance resource and we can just skip it

$servicesByResource = $this->serviceLocator->findServices(IResourceUSCIGProfileService::class);

$patientUuids = $job->getPatientUuidsToExport();

foreach ($job->getResources() as $resource) {
$searchParams = [];
$searchParams['_revinclude'] = 'Provenance:target';
if ($resource != "Provenance" && isset($servicesByResource[$resource]) && $servicesByResource[$resource] instanceof IResourceReadableService) {
$service = $servicesByResource[$resource];
if ($type == ExportJob::EXPORT_OPERATION_GROUP) {
// service supports filtering by patients so let's do that
if ($service instanceof IPatientCompartmentResourceService) {
$searchField = $service->getPatientContextSearchField();
$searchParams[$searchField->getName()] = implode(",", $patientUuids);
}
}

$serviceResult = $service->getAll($searchParams);
// now loop through and grab all of our provenance resources
if ($serviceResult->hasData()) {
foreach ($serviceResult->getData() as $record) {
if (!($record instanceof FHIRDomainResource)) {
throw new ExportException(self::class . " returned records that are not a valid fhir resource type for this class", 0, $lastResourceIdExported);
}
// we only want to write out provenance records
if (!($record instanceof FHIRProvenance)) {
continue;
}
$writer->append($record);
$lastResourceIdExported = $record->getId();
}
}
}
}
}
}

0 comments on commit 2b3f2ef

Please sign in to comment.