npm package: @synanetics/syn-utils
Description: synanetics util functionality
Author: Daniel Bayley
Latest version: 6.1.1

# `@synanetics/syn-utils`

A collection of common utility functions used across multiple Synanetics repositories

- [`@synanetics/syn-utils`](#synaneticssyn-utils)
    - [Usage](#usage)
    - [API](#api)
        - [Paging](#paging)
            - [Function: pageBuilder](#function-pagebuilder)
        - [Includes](#includes)
            - [Class: FhirIncluder](#class-fhirincluder)
        - [Prefix](#prefix)
            - [Defaults](#defaults)
            - [Function: getPrefix](#function-getprefix)
            - [Function: removePrefix](#function-removeprefix)
            - [Function: removePrefixFromFhirObj](#function-removeprefixfromfhirobj)
        - [Reference](#reference)
            - [Interface: ReferenceDetails](#interface-referencedetails)
            - [Function: getReferenceDetails](#function-getreferencedetails)
            - [Function: getUniqueRelativeReferences](#function-getuniquerelativereferences)
        - [Rebase References](#rebaseReference)
            - [Function: rebaseReferences](#function-rebaseReference)


## Usage

```typescript
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

```typescript
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
* 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:

 1. FHIR query received requiring pagination
 1. Gather the sorted Bundles from each source
 1. Call the pageBuilder with the source Bundles, and desired config for page 1
 1. pageBuilder returns completed page, and partially consumed input bundles
 1. Cache the page, and partially consumed input bundles
 1. Return the page

Second Call (with emptied Bundle):

 1. Second page requested
 1. Retrieve partially consumed input bundles from cache
 1. Call the pageBuilder with the partially consumed Bundles, and desired config for page 2
 1. pageBuilder responds with an incomplete page, partially consumed Bundles, and requests a replacement Bundle from "source1"
 1. Retrieve the next Bundle from "source1"
 1. Call the pageBuilder with the partially consumed Bundles, the fresh Bundle from "source1", desired config for page 2 and the incomplete page previously returned
 1. pageBuilder returns completed page, and partially consumed input bundles
 1. Cache the page, and partially consumed input bundles
 1. 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

```typescript
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

```typescript
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:iterate` parameters
        * 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
* returns
    * An object containing the `included` array 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:iterate` or `_include:recurse` = `(* | [resource]:* | [resource]:[parameter] | [resource]:[parameter]:[target])`: Include resources referenced by the first level of includes
        * `_revinclude:iterate` or `_revinclude:recurse` = `(* | [resource]:* | [resource]:[parameter] | [resource]:[parameter]:[target])`: Include resources that reference the first level of includes

Usage

```typescript
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 matches
```

The `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

```json
{
    "reference": "https://some-url/Patient/123"
}
```
will be matched by the Bundle.entry

```json
{
    "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
```json
{
    "reference": "https://some-url/Patient/123/_history/2"
}
```
will be matched by the Bundle.entry

```json
{
    "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.

```typescript
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

```typescript
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
```javascript
//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

```typescript
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
```javascript
//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

```typescript
function removePrefixFromFhirObj<T extends (fhir3.FhirResource | fhir4.FhirResource)>(  
    input: T,
    prefix: string,
    prefixDelimiter?: string,
    maxDepth?: number,
  ) => T
```

Arguments
* 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
```typescript
//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

```typescript
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

```typescript
function getReferenceDetails(referenceString: string) => ReferenceDetails
```

* referenceString
    * The string to extract the details from
    * If no referenceString is provided an error will be thrown

Usage
```javascript
//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


```typescript
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
```javascript
//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:
```typescript
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',
    },
})
```