@synanetics/syn-utils
v6.0.1
Published
synanetics util functionality
Maintainers
Keywords
Readme
@synanetics/syn-utils
A collection of common utility functions used across multiple Synanetics repositories
Usage
const {
getPrefix, removePrefix
} = require('@synanetics/syn-utils');
// or
import {
getPrefix, removePrefix
} from '@synanetics/syn-utils';API
Paging
Function: pageBuilder
The pageBuilder function takes in an array of sorted sourceBundles and pageConfig, and returns a bundle page with those with the combined resources from all the bundles.
Arguments
function pageBuilder(sourceBundles: SourceBundle[], config: PageConfig) => PageOutput
interface SourceBundle {
sourceId: string;
bundle: Bundle;
}
interface SortConfig {
fhirSorter: FhirSorter;
sortTerms: string[];
allowChainedSort?: boolean;
}
interface FhirIncluderConfig {
searchParameters: URLSearchParams;
includer: FhirIncluder | {
model: Model;
searchParameters: (InvariantSearchParameter | CompiledInvariantSearchParameter)[];
};
}
interface PageConfig {
pagingBaseUrl: URL;
pageSize?: number;
sortConfig?: SortConfig;
currentPageNum?: number;
partialPage?: Bundle;
includeConfig?: FhirIncluderConfig;
}
interface PageOutput {
completePage: boolean;
page: Bundle;
sourceBundles: SourceBundle[];
bundleToReplace?: string;
}- sourceBundles
- The Bundles to combine into a single page
- All Bundles must have a 'Total'
- At least one Bundle must have entries
- config
- The config defines how the page is to be built
- pagingBaseUrl
- The url to which the paging links can be appended
- e.g https://fhir.store.nhs.uk/patient?_queryId=uuid ->
- {relation: 'self', url: 'https://fhir.store.nhs.uk/patient?_queryId=uuid&_page=4'}
- {relation: 'first', url: 'https://fhir.store.nhs.uk/patient?_queryId=uuid&_page=1'}
- {relation: 'last', url: 'https://fhir.store.nhs.uk/patient?_queryId=uuid&_page=9'}
- {relation: 'previous', url: 'https://fhir.store.nhs.uk/patient?_queryId=uuid&_page=3'}
- {relation: 'next', url: 'https://fhir.store.nhs.uk/patient?_queryId=uuid&_page=5'}
- pageSize
- The desired maximum page size
- Must be a positive integer
- Will default to 100 if not provided
- sortConfig?
- This is an optional parameter, if it is not provided the pageBuilder will iterate over each source to build the page
- fhirSorter
- An instance of the FhirSorter from the @synanetics/fhir-sort package
- sortTerms
- An array of strings that will be used to sort the resources in the Bundles
- e.g ['family', '_lastUpdated']
- allowChainedSort
- A boolean to allow/disallow chained sorting
- For details on chained sorting see the @synanetics/fhir-sort package README
- This parameter is optional and will default to false if not provided
- currentPageNum?
- The number of the page being built
- e.g If you are calling the pageBuilder to build the 3rd page in a set of 5, this value should be 3
- This value will default to 1 if not provided
- partialPage?
- This is an optional parameter
- This parameter should only be provided when the pageBuilder previously returned an incomplete page
- This happens when the pageBuilder has emptied an input bundle and requires a replacement
- When you recall pageBuilder with the new bundle, you will pass in the incomplete page in partialPage returned by the first call
- includeConfig?
- This is an optional parameter that configures how included resources are handled
- If not provided, no included resources will be present in the final Bundle
- searchParameters
- The URLSearchParams that were used to create the original Bundle includes
- e.g. for a search with
_include=Patient:organization, these search parameters would contain that include parameter
- includer
- Configuration for calculating the necessary includes for a given page
- Can be either:
- An instance of FhirIncluder from the @synanetics/syn-utils package, or
- An object containing:
- model: A fhirpath Model instance
- searchParameters: An array of InvariantSearchParameter or CompiledInvariantSearchParameter objects
- These will be used to determine which included resources are referenced by the matches on the page
Return Values
- completePage
- This is a boolean
- This is true when:
- The pageBuilder was able to build a page of the desired maximum size
- The page builder has emptied all the bundles and built the final page
- This is false when:
- The page builder emptied one of the input bundles before completing the page and has determined it needs a replacement
- page
- This is a FHIR Bundle
- Its total will be the combined total of all bundles
- It will have paging links
- Its entries will be populated from the input bundles
- Outcomes come first
- Any outcomes from all input bundles will appear first on the page
- These can be in any order
- Matches come second
- A sorted, if sortTerms are provided, subset of the matches from all the input bundles will come after the outcomes
- This will most often not contain all the matches from all the inputs, as the page size will likely be smaller than the number of matches provided
- Includes come third
- Any includes referenced in the matches will come after the matches
- Each include will be referenced at least once by at least one match
- Each include will only appear once
- Outcomes come first
- sourceBundles
- The sourceBundles outputted by the page builder are similar to the ones inputted
- The key difference is the output bundles will not contain any matches or outcomes already put into a page
- e.g If we have input bundles with 5 matches each for 5 sources, and a page size of 10, the output bundles may contain 3 matches each for 5 sources.
- Bundles that are emptied of matches will be returned by the pageBuilder, they are passed back to the pageBuilder as part of sourceBundles in future calls to keep accurate totals for subsequent pages.
- bundleToReplace?
- If the pageBuilder empties a paginated Bundle that is not on its last page, it needs to request a new bundle
- When this occurs it populates this optional string with the source id of the bundle to be replaced
Usage
This is an example flow where the first call returns a complete page, and the second call requires a replacement bundle.
First Call:
- FHIR query received requiring pagination
- Gather the sorted Bundles from each source
- Call the pageBuilder with the source Bundles, and desired config for page 1
- pageBuilder returns completed page, and partially consumed input bundles
- Cache the page, and partially consumed input bundles
- Return the page
Second Call (with emptied Bundle):
- Second page requested
- Retrieve partially consumed input bundles from cache
- Call the pageBuilder with the partially consumed Bundles, and desired config for page 2
- pageBuilder responds with an incomplete page, partially consumed Bundles, and requests a replacement Bundle from "source1"
- Retrieve the next Bundle from "source1"
- Call the pageBuilder with the partially consumed Bundles, the fresh Bundle from "source1", desired config for page 2 and the incomplete page previously returned
- pageBuilder returns completed page, and partially consumed input bundles
- Cache the page, and partially consumed input bundles
- Return the page
Includes
Class: FhirIncluder
The FhirIncluder class is used to calculate which included resources should be present in a paginated Bundle based on the matches in that page. It evaluates FHIR search parameters to determine which included resources are referenced by the match resources.
Constructor
class FhirIncluder {
constructor(
model: Model,
searchParameters: (InvariantSearchParameter | CompiledInvariantSearchParameter)[]
)
}
interface InvariantSearchParameter {
id?: string;
expression?: string;
code: string;
base?: string[];
target?: string[];
}
interface CompiledInvariantSearchParameter extends InvariantSearchParameter {
compiledExpression: Compile<{ async: false }>;
}- model
- A fhirpath Model instance for evaluating FHIRPath expressions
- This is used to evaluate search parameter expressions against FHIR resources
- searchParameters
- An array of search parameter definitions
- Each search parameter should include:
- code: The search parameter code (e.g., 'organization', 'patient')
- expression: The FHIRPath expression for extracting references (e.g., 'Patient.managingOrganization')
- base: An array of resource types this parameter applies to (e.g., ['Patient'])
- target: An array of resource types this parameter can reference (e.g., ['Organization'])
- id: Optional unique identifier for the search parameter
- compiledExpression: Optional precompiled fhirpath expression for the search parameter
Method: include
include(request: IncludeRequest): IncludeResult
interface IncludeRequest {
query: URLSearchParams;
matches: InvariantBundleEntry[];
includes: InvariantBundleEntry[];
}
interface IncludeResult {
included: InvariantBundleEntry[];
}
interface InvariantBundleEntry {
resource?: { resourceType: string; id?: string };
}- request
- The request object containing:
- query: URLSearchParams containing
_include,_revinclude,_include:iterate, and_revinclude:iterateparameters - matches: Array of bundle entries with search.mode='match' from the page
- includes: Array of all available bundle entries with search.mode='include' to choose from
- query: URLSearchParams containing
- The request object containing:
- returns
- An object containing the
includedarray of bundle entries that should be included in the page - The result will only contain unique entries (duplicates are removed)
- Supports:
_include=(* | [resource]:* | [resource]:[parameter] | [resource]:[parameter]:[target]): Include resources referenced by match resources_revinclude=(* | [resource]:* | [resource]:[parameter] | [resource]:[parameter]:[target]): Include resources that reference the match resources_include:iterateor_include:recurse=(* | [resource]:* | [resource]:[parameter] | [resource]:[parameter]:[target]): Include resources referenced by the first level of includes_revinclude:iterateor_revinclude:recurse=(* | [resource]:* | [resource]:[parameter] | [resource]:[parameter]:[target]): Include resources that reference the first level of includes
- An object containing the
Usage
import { FhirIncluder } from '@synanetics/syn-utils';
import { compile } from 'fhirpath';
// model to use when compiling fhirpath expressions
import model from 'fhirpath/fhir-context/r4/index.js'
// Define search parameters
const searchParameters = [
{
code: 'organization',
expression: 'Patient.managingOrganization',
base: ['Patient'],
target: ['Organization'],
},
{
code: 'patient',
expression: 'Observation.subject',
base: ['Observation'],
target: ['Patient'],
}
];
// Create the includer
const includer = new FhirIncluder(model, searchParameters);
// Use it to calculate includes for a page
const result = includer.include({
query: new URLSearchParams('_include=Patient:organization'),
matches: [
{ resource: { resourceType: 'Patient', id: '123', managingOrganization: { reference: 'Organization/abc' } } }
],
includes: [
{ resource: { resourceType: 'Organization', id: 'abc' } },
{ resource: { resourceType: 'Organization', id: 'xyz' } }
]
});
// result.included will contain only the Organization/abc entry
// since it's the only one referenced by the matchesThe FhirIncluder is written such that once it has compiled a particular search parameter (determined by the id property) it will not need to compile it again if the FhirIncluder is reused.
The FhirIncluder can currently resolve Literal references in both absolute and relative forms, including references to specific versions of resources. It cannot resolve Logical or Canonical references at present.
Where absolute references are used the FhirIncluder will find the target resource by the fullUrl of the
Bundle entries, which must match the reference (excluding any _history component).
e.g. the absolute reference
{
"reference": "https://some-url/Patient/123"
}will be matched by the Bundle.entry
{
"fullUrl": "https://some-url/Patient/123",
"resource": {
"resourceType": "Patient",
"id": "123"
}
}only when the fullUrl is present for the Bundle.entry
Similarly where a reference, either absolute or relative, refers to a specific version of a resource, the FhirIncluder will find the target resource by the meta.versionId present on the resource.
e.g. the absolute reference
{
"reference": "https://some-url/Patient/123/_history/2"
}will be matched by the Bundle.entry
{
"fullUrl": "https://some-url/Patient/123",
"resource": {
"resourceType": "Patient",
"id": "123",
"meta": {
"versionId": "2"
}
}
}only when the fullUrl is present for the Bundle.entry and matches the reference, (ignoring the _history section of the reference) and the resource.meta.versionId property is present and matches the _history component of the reference.
The FhirIncluder will make the best effort attempt to maintain only unique includes in the final output. Uniquness of entries is determined by the entry.fullUrl, entry.resource.resourceType, entry.resource.id and entry.resource.meta.versionId properties only. Any entries that do not have a fullUrl and/or a resourceType and id at a minimum cannot be determined to be unique or not and will not be included in the final output.
Prefix
Defaults
The prefix functions have default configuration values/functions which are as below.
const defaultPrefixDelimiter: string = '.';
const defaultEscapeRegex = (string: string): string => string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
const defaultPrefixRegex: string = `^(?:.*/)?(?<prefix>.*?)${defaultEsc
apeRegex(defaultPrefixDelimiter)}.*`;Function: getPrefix
The get prefix function takes in a string and returns a prefix that matches the provided arguments.
Arguments
function getPrefix(value: string, validPrefixes?: string[], prefixRegex: string = defaultPrefixRegex) => string- value
- The string to retrieve the prefix from
- If no value is provided an error will be thrown
- validPrefixes
- An array of valid prefix strings
- If this is not provided, all prefixes are considered valid
- dpPrefixRegex
- Defaults to defaultDpPrefixRegex
- This regex is used to find the prefix in value
Usage
//Expect value: wow
getPrefix('wow.theRest');
//Expect value: apple
getPrefix('apple.pie', ['apple', 'pear']);
// Expect error: No value provided
getPrefix();
//Expect Error: No valid prefixes found
getPrefix('this has no prefix');
//Expect Error: No valid prefixes found
getPrefix('tomato.pie', ['apple', 'pear']);Function: removePrefix
The get prefix function takes in a string and returns a prefix that matches the provided arguments.
Arguments
function removePrefix(value: string, prefix: string, prefixDelimiter: string = defaultPrefixDelimiter, escapeRegex: (string: string) => string = defaultEscapeRegex) => string - value
- The string to remove the prefix from
- If no value is provided an error will be thrown
- prefix
- The prefix to remove
- If no prefix is provided an error will be thrown
- prefixDelimiter
- Defaults to defaultPrefixDelimiter
- This string is used to split prefix from value
- escapeRegex
- Defaults to defaultEscapeRegex
- This regex is applied to the prefixDelimiter to escape protected characters
Usage
//Expect value: theRest
removePrefix('wow.theRest', 'wow');
//Expect value: apple.pie
removePrefix('apple.pie', 'pear');
// Expect error: No value provided
removePrefix();
// Expect error: No prefix provided
removePrefix('wow');Function: removePrefixFromFhirObj
The removePrefixFromFhirObj function removes a prefix string from all ids and reference ids in a FHIR object
function removePrefixFromFhirObj<T extends (fhir3.FhirResource | fhir4.FhirResource)>(
input: T,
prefix: string,
prefixDelimiter?: string,
maxDepth?: number,
) => TArguments
- input is a FHIR stu3/r4 resource as defined in the @types/fhir package such as a Patient resource
- prefix is a string such as 'HUB' that should be removed from all ids and reference ids
- prefixDelimiter is a string that is used to delimit where a prefix ends and an id begins, this defaults to '.'
- maxDepth is a number that represents the maximum recursion depth that is allowed before an error is thrown, this defaults to 15
Usage
//Input resource:
const patient: Patient = {
id: 'HUB.pat1',
resourceType: 'Patient',
generalPractitioner: [
{
reference: '#contained',
},
{
reference: 'Organization/HUB.org1',
},
{
reference: 'Organization/HUB.org1',
},
{
reference: 'Practitioner/HUB.DDCR.pra1',
},
{
reference: 'Practitioner/HUB.pra2/_history/4',
},
],
managingOrganization: {
reference: 'url/fhir/Organization/HUB.DDCR.org2',
},
};
const unprefixedPatient: Patient = removePrefixFromFhirObj(patient, 'HUB');
// expect unprefixedPatient to equal expectedPatient
const expectedPatient: Patient = {
id: 'pat1',
resourceType: 'Patient',
generalPractitioner: [
{
reference: '#contained',
},
{
reference: 'Organization/org1',
},
{
reference: 'Organization/org1',
},
{
reference: 'Practitioner/DDCR.pra1',
},
{
reference: 'Practitioner/pra2/_history/4',
},
],
managingOrganization: {
reference: 'url/fhir/Organization/DDCR.org2',
},
};Reference
Interface: ReferenceDetails
interface ReferenceDetails {
original: string;
contained: boolean;
version: number;
id?: string;
type?: string;
criteria?: string;
url?: string;
relative?: string;
}Function: getReferenceDetails
The get getReferenceDetails function takes in a reference string and extracts the details into a ReferenceDetails object
Arguments
function getReferenceDetails(referenceString: string) => ReferenceDetails- referenceString
- The string to extract the details from
- If no referenceString is provided an error will be thrown
Usage
//Expect value:
const expectedValue = {
original: '#p1',
contained: true,
version: 1,
id: 'p1'
}
getReferenceDetails('#p1');
//Expect value:
const expectedValue = {
original: 'ValueSet?url=http://hl7.org/fhir/ValueSet/my-valueset&version=0.8',
contained: false,
version: 1,
type: 'ValueSet',
criteria: 'url=http://hl7.org/fhir/ValueSet/my-valueset&version=0.8',
}
getReferenceDetails('ValueSet?url=http://hl7.org/fhir/ValueSet/my-valueset&version=0.8');
//Expect value:
const expectedValue = {
original: 'url/fhir/r4/Patient/12345',
contained: false,
version: 1,
type: 'Patient',
id: '12345',
url: 'url/fhir/r4/Patient/12345',
relative: 'Patient/12345',
}
getReferenceDetails('url/fhir/r4/Patient/12345');
//Expect value:
const expectedValue = {
original: 'Patient/12345/_history/5',
contained: false,
version: 5,
type: 'Patient',
id: '12345',
relative: 'Patient/12345',
}
getReferenceDetails('Patient/12345/_history/5');Function: getUniqueRelativeReferences
The get getUniqueRelativeReferences function takes in a fhir resource and extracts unique relative reference strings
function getUniqueRelativeReferences(
input: Record<any, any>,
relativeRefs?: Set<string>,
maxDepth?: string,
) => Set<string>Arguments
- input is a FHIR resource as defined in the @types/fhir package such as a Patient resource
- relativeRefs is a set of unique relative reference strings, e.g Patient/12345, this defaults to an empty set
- maxDepth is a number that represents the maximum recursion depth that is allowed before an error is thrown, this defaults to 15
Usage
//Input resource:
const patient: Patient = {
resourceType: 'Patient',
generalPractitioner: [
{
reference: '#notRelative',
},
{
reference: 'Organization/1',
},
{
reference: 'Organization/1',
},
{
reference: 'Practitioner/1',
},
{
reference: 'Practitioner/2/_history/4',
},
],
managingOrganization: {
reference: 'url/fhir/Organization/2',
},
};
//Expect set to contain 'Organization/1', 'Practitioner/1', 'Practitioner/2', 'Organization/2'
getUniqueRelativeReferences(patient);Rebase References
Function: rebaseReferences
A function to prefix any identifiers or relative references with the supplied string.
Usage:
const resource = {
id: 'DEF.123',
subject: {
reference: 'Patient/DEF.456',
},
};
const rebasedResource = rebaseReferences(resource, 'ABC');
expect(resource).toEqual({
id: 'ABC.DEF.123',
subject: {
reference: 'Patient/ABC.DEF.456',
},
})