/*doSampleResources - Sample resources based on params & write each to file*/ ( $warning({'=========> doSampleWrapper start <=========' : $now()}); $originalWarning := $warning; $warning := function($message) {( $originalWarning($message); $appendFile($message & '\n', 'doSampleWrapper.log'); )}; /* set the maximum number of failed search attempts */ $maxFailedAttempts := 5; /* set a base interval (in milliseconds) for the delay - this will be doubled in each retry */ $baseDelayInterval := 10; /* global parameters */ $pageSize := 100; $maxRetriesForNewResource := 25; $strictToOriginalMinMaxDates := resourceType = 'Patient'; // we don't want to sample before and after each bin /*****************/ /*Override $resolve() to use $http and address "Request failed with status code 503"*/ $resolve := function($literal){( $warning({'$resolve called with the following params: literal = ': $literal,'$attemptNumber = ' : $attemptNumber, '$maxFailedAttempts = ' : $maxFailedAttempts}); $attempt := function($attemptNumber){( $warning({'$attempt called with following params: $attemptNumber = ' : $attemptNumber}); /* reset counter if undefined */ $attemptNumber := $exists($attemptNumber) ? $attemptNumber : 1; $attemptNumber > $maxFailedAttempts ? ( { 'resourceType': 'OperationOutcome', 'issue': [ { 'severity': 'error', 'code': 'transient', 'diagnostics': 'Reached maximum number of failed attempts for $resolve() on $literal := ' & $literal } ] } ) : ( $httpResponse := $http({ 'url': $literal }); $floor($httpResponse.status / 100) = 2 ? ( /* successful attempt */ $warning('=========> resolve() => Successful http call <========='); $httpResponse.data ) : ( /* failed attempt */ $warning('=========> resolve() => Failed http call <========='); $warning({'$literal := ' : $literal}); $warning({'$httpResponse := ' : $httpResponse}); $wait($baseDelayInterval * 2 * $attemptNumber); $attempt($attemptNumber + 1) ) ) )}; $attempt(); )}; /* define a new $search function that overrides the default one */ $search := function($resourceType, $params){( $attempt := function($attemptNumber){( /* reset counter if undefined */ $attemptNumber := $exists($attemptNumber) ? $attemptNumber : 1; $attemptNumber > $maxFailedAttempts ? ( { 'resourceType': 'OperationOutcome', 'issue': [ { 'severity': 'error', 'code': 'transient', 'diagnostics': 'Reached maximum number of failed search attempts for resourceType ' & $resourceType & ' and params: ' & $string($params) } ] } ) : ( $httpResponse := $http({ 'url': $resourceType, 'params': $params }); $floor($httpResponse.status / 100) = 2 ? ( /* successful attempt */ $warning('=========> $search() => Successful http call <========='); $httpResponse.data ) : ( /* failed attempt */ $warning('=========> $search() => Failed http call <========='); $warning('Request:'); $warning({'$resourceType := ' : $resourceType}); $warning({'$params := ' : $params}); $warning({'$httpResponse := ' : $httpResponse}); $wait($baseDelayInterval * 2 * $attemptNumber); $attempt($attemptNumber + 1) ) ) )}; $attempt(); )}; /* date generator function */ $newRandomDate := function($min, $max){( $minMs := $toMillis($min); $maxMs := $toMillis($max); $diffMs := $maxMs - $minMs; $randomMs := $random() * $diffMs; $fromMillis($minMs + $randomMs); )}; // function that converts a period object to a date string $periodToDate := function($period){( $type($period) = 'string' ? $period : $period.(start ? start : end) )}; /* function that calculates the absolute diff between dates in milliseconds */ $dateDiffMillis := function($date1, $date2){ ( $date1 := $periodToDate($date1); $date2 := $periodToDate($date2); $abs($toMillis($date1)-$toMillis($date2)) ) }; /* function that fetches resources above and below a requested date and returns them sorted by proximity */ $getClosestResources := function($resourceType, $date, $searchParam, $elementName) {( $warning('$getClosestResources called'); $warning({'$resourceType, $date, $searchParam, $elementName': [$resourceType, $date, $searchParam, $elementName]}); $minMaxDates := $strictToOriginalMinMaxDates ? { 'minDate': minDate, 'maxDate': maxDate } : { 'minDate': $fromMillis($toMillis($date)-31536000000), // 1 year before $date 'maxDate': $fromMillis($toMillis($date)+31536000000) // 1 year after $date }; /* get pages from above and below the date */ $geBundle := $search( $resourceType & '?' & $searchParam & '=ge' & $date & '&' & $searchParam & '=le' & $minMaxDates.maxDate & '&' & $searchParam &':missing=false' & '&_sort=' & $searchParam & '&_count=' & $pageSize ); /*$search($resourceType, {$searchParam: ['ge' & $date, 'le' & $fromMillis($toMillis($date)+6000000)], '_sort': $searchParam, '_count': $pageSize})*/ $warning({'$geBundle count': $count($geBundle.entry)}); $leBundle := $search( $resourceType & '?' & $searchParam & '=le' & $date & '&' & $searchParam & '=ge' & $minMaxDates.minDate & '&' & $searchParam &':missing=false' & '&_sort=-' & $searchParam & '&_count=' & $pageSize ); /*$search($resourceType, {$searchParam: ['le' & $date, 'ge' & $fromMillis($toMillis($date)-6000000)], '_sort': '-' & $searchParam, '_count': $pageSize})*/ $warning({'$leBundle count': $count($leBundle.entry)}); /* sort the results by absolute diff from the selected date */ [$geBundle.entry.resource[resourceType = $resourceType], $leBundle.entry.resource[resourceType = $resourceType]] // merge both search bundle arrays [$exists($eval($elementName))] // filter out resources without the relevant element ^($dateDiffMillis($date, $lookup($, $elementName))); // sort by proximity to the date )}; /* recursive function that adds a single new resource to an accumulating list */ /* educational note: */ /* note that the self-call is at the final step of the function. */ /* this is called "tail recursion" and it helps prevent stack overflow. */ /* see: https://docs.jsonata.org/programming#tail-call-optimization-tail-recursion */ $addResource := function($resourceType, $searchParam, $elementName, $minDate, $maxDate, $accumulating, $iterationCounter){( $warning('$addResource called with the following params:'); $warning({'$resourceType' : $resourceType, '$searchParam' : $searchParam, '$elementName' : $elementName, '$minDate' : $minDate, '$maxDate' : $maxDate, '$iterationCounter' : $iterationCounter, '$maxRetriesForNewResource' : $maxRetriesForNewResource,'$accumulating count' : $count($accumulating) /*'For values - uncomment this line in map' $accumulating*/}); /* reset counter to 1 if not stated otherwise */ $iterationCounter := $exists($iterationCounter) ? $iterationCounter : 1; /* initialize an empty list if accumulator not passed */ $accumulating := $exists($accumulating) ? $accumulating : []; /* generate a new random date */ $randomDate := $newRandomDate($minDate, $maxDate); /* get list of closest instances to the random date by chosen element (birthdate etc), sorted by proximity */ $searchResults := $getClosestResources($resourceType, $randomDate, $searchParam, $elementName); $warning({'$searchResults count': $count($searchResults)}); /* get the closest one not collected yet */ $filterred := $searchResults[$not(id in $accumulating)]; // $selection := $filterred[$count(filterred)*$random()]; $selection := $filterred[0]; $warning({'$selection count': $count($selection)}); $warning({'$selection :=': $selection}); /* check the results */ $exists($selection) ? ( /* found a new resource - add its id to the list and return the updated list */ [$accumulating, $selection.id] ) : ( /* no new resource found using this date */ /* if we have reached max iterations - stop looking */ $iterationCounter >= $maxRetriesForNewResource ? ( /* return accumulated list as-is */ $warning({'$iterationCounter' : $iterationCounter, '>= $maxRetriesForNewResource' : $maxRetriesForNewResource,'exit loop $addResource & set $iterationCounter=0 & return current $accumulating with id count of' : $count($accumulating)}); $accumulating ) : ( /* not reached max iterations - try again with a new random date */ $warning('$addResource loop did not reach max iterations - call $addResource again with a new random date + advance $iterationCounter + 1'); $addResource($resourceType, $searchParam, $elementName, $minDate, $maxDate, $accumulating, $iterationCounter + 1) /* ^ this is the "tail call" */ ) ) )}; /* wrapper function that does the collection of the list of resources */ $collectResources := function($resourceType, $amountToCollect, $minDate, $maxDate, $searchParam, $elementName){( $warning('$collectResources called'); $warning({'$resourceType, $amountToCollect, $minDate, $maxDate, $searchParam, $elementName': [$resourceType, $amountToCollect, $minDate, $maxDate, $searchParam, $elementName]}); /* iteration function that adds resources recursively, as long as: */ /* 1. the previous step succeeded in finding a new resource */ /* 2. the requested amount hasn't been collected yet */ $iterator := function($currentList, $previousList, $emptyListCounter) {( $warning('$iterator called with following params'); $warning({'$currentList count = ' : $count($currentList), '$previousList count = ': $count($previousList)}); $count($currentList) > 0 and ($count($currentList) = $count($previousList) or $count($currentList) >= $amountToCollect) ? ( /* the stop condition is met - return list and stop iterating */ $currentList ) : ( /* try to add one resource and continue */ $updatedList := $addResource($resourceType, $searchParam, $elementName, $minDate, $maxDate, $currentList); $warning({'$updatedList count': $count($updatedList)}); $updatedEmptyListCounter := $count($updatedList) = 0 and $count($currentList) = 0 ? ( $exists($emptyListCounter) ? $emptyListCounter + 1 : 1 ) : 0; $warning({'$updatedEmptyListCounter': $updatedEmptyListCounter}); $updatedEmptyListCounter > 2 ? ( // Adjust the threshold as needed $warning('Iterator stopping because the list has remained empty for too many consecutive iterations.'); $currentList ) : ( /* tail call */ $iterator($updatedList, $currentList, $updatedEmptyListCounter) ) ) )}; /* call the iterator with empty initial lists */ $ids := $iterator(); /* transform the list of resource id's to a list of references (ready for $resolve) */ $ids.($join([$resourceType, $], '/')) )}; /*call the wrapper function with relevant keys, values will come from inputcall the wrapper function with relevant keys, values will come from input*/ $instancesList := $collectResources(resourceType, amountToCollect, minDate, maxDate, searchParam, elementName); $warning('$instancesList => '&$instancesList); /*Write sampled resources to IO folder*/ /*resolve the list of resource ids to get the full resources*/ $instancesListResources := $instancesList.$resolve($); /*for each resource, create file by naming convention*/ $instancesListResources.$writeFile($,'['& $.resourceType &']_['& $.id &'].json'); /* Example input (derived from the above example) */ // NOTE: this does not need to be commented out. // Since it's not the last value in the expression - it has no effect on the output */ { "resourceType": "Patient", "amountToCollect": 1000, "minDate": "1900-01-01", "maxDate": "2020-01-01", "searchParam": "birthdate", "elementName": "birthDate" } ; // reached end of mapping - return a "completed" operation outcome { 'resourceType': 'OperationOutcome', 'issue': [ { 'severity': 'information', 'code': 'informational', 'diagnostics': 'doSampleResources mapping completed' } ] } )