adding Base64 methods
Adding tests for BaseClient Adding Fhir401Client Adding fixtures for BaseClient and Fhir401Client
This commit is contained in:
parent
0a5d71691f
commit
6425ea48f0
|
@ -6,3 +6,6 @@ Find & replace the following
|
||||||
- `fastenhealth` - find and replace this with your binary name
|
- `fastenhealth` - find and replace this with your binary name
|
||||||
- make sure you rename the folder as well.
|
- make sure you rename the folder as well.
|
||||||
|
|
||||||
|
# Running tests
|
||||||
|
|
||||||
|
- ng test --include='**/base_client.spec.ts'
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
|
||||||
import {BaseClient} from './base_client';
|
import {BaseClient} from './base_client';
|
||||||
import {Source} from '../../../models/database/source';
|
import {Source} from '../../../models/database/source';
|
||||||
|
import * as BaseClient_GetRequest from './fixtures/BaseClient_GetRequest.json';
|
||||||
|
import * as BaseClient_GetFhirVersion from './fixtures/BaseClient_GetFhirVersion.json';
|
||||||
|
|
||||||
|
|
||||||
class TestClient extends BaseClient {
|
class TestClient extends BaseClient {
|
||||||
constructor(source: Source) {
|
constructor(source: Source) {
|
||||||
|
@ -13,25 +15,19 @@ describe('BaseClient', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
client = new TestClient(new Source({
|
client = new TestClient(new Source({
|
||||||
"authorization_endpoint": "https://auth.logicahealth.org/authorize",
|
"authorization_endpoint": "https://fhirsandbox.healthit.gov/open/r4/authorize",
|
||||||
"token_endpoint": "https://auth.logicahealth.org/token",
|
"token_endpoint": "https://fhirsandbox.healthit.gov/open/r4/token",
|
||||||
"introspection_endpoint": "https://auth.logicahealth.org/introspect",
|
"introspection_endpoint": "",
|
||||||
"userinfo_endpoint": "",
|
"issuer": "https://fhirsandbox.healthit.go",
|
||||||
"scopes_supported": ["openid", "fhirUser", "patient/*.read", "offline_access"],
|
"api_endpoint_base_url": "https://fhirsandbox.healthit.gov/secure/r4/fhir",
|
||||||
"issuer": "https://auth.logicahealth.org",
|
"client_id": "9ad3ML0upIMiawLVdM5-DiPinGcv7M",
|
||||||
"grant_types_supported": ["authorization_code"],
|
"redirect_uri": "https://lighthouse.fastenhealth.com/sandbox/callback/healthit",
|
||||||
"response_types_supported": ["code"],
|
|
||||||
"aud": "",
|
|
||||||
"code_challenge_methods_supported": ["S256"],
|
|
||||||
"api_endpoint_base_url": "https://api.logicahealth.org/fastenhealth/data",
|
|
||||||
"client_id": "12b14c49-a4da-42f7-9e6f-2f19db622962",
|
|
||||||
"redirect_uri": "https://lighthouse.fastenhealth.com/sandbox/callback/logica",
|
|
||||||
"confidential": false,
|
"confidential": false,
|
||||||
"source_type": "logica",
|
"source_type": "healthit",
|
||||||
"patient": "smart-1288992",
|
"patient": "placeholder",
|
||||||
"access_token": "xxx.xxx.xxx",
|
"access_token": "2e1be8c72d4d5225aae264a1fb7e1d3e",
|
||||||
"refresh_token": "xxx.xxx.",
|
"refresh_token": "",
|
||||||
"expires_at": 1664949030,
|
"expires_at": 16649837100, //aug 11, 2497 (for testing)
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -41,14 +37,35 @@ describe('BaseClient', () => {
|
||||||
|
|
||||||
describe('GetRequest', () => {
|
describe('GetRequest', () => {
|
||||||
it('should make an authorized request', async () => {
|
it('should make an authorized request', async () => {
|
||||||
const resp = await client.GetRequest("Patient/smart-1288992")
|
|
||||||
|
|
||||||
expect(resp).toEqual({
|
//setup
|
||||||
resourceType: "Patient",
|
let response = new Response(JSON.stringify(BaseClient_GetRequest));
|
||||||
id: "source:aetna:patient"
|
Object.defineProperty(response, "url", { value: `${client.source.api_endpoint_base_url}/Patient/${client.source.patient}`});
|
||||||
});
|
spyOn(window, "fetch").and.returnValue(Promise.resolve(response));
|
||||||
|
|
||||||
|
//test
|
||||||
|
const resp = await client.GetRequest(`Patient/${client.source.patient}`)
|
||||||
|
|
||||||
|
//expect
|
||||||
|
expect(resp.resourceType).toEqual("Patient");
|
||||||
|
expect(resp.id).toEqual("123d41e1-0f71-4e9f-8eb2-d1b1330201a6");
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('GetFhirVersion', () => {
|
||||||
|
it('should make an authorized request', async () => {
|
||||||
|
//setup
|
||||||
|
let response = new Response(JSON.stringify(BaseClient_GetFhirVersion));
|
||||||
|
Object.defineProperty(response, "url", { value: `${client.source.api_endpoint_base_url}/metadata`});
|
||||||
|
spyOn(window, "fetch").and.returnValue(Promise.resolve(response));
|
||||||
|
|
||||||
|
//test
|
||||||
|
const resp = await client.GetFhirVersion()
|
||||||
|
|
||||||
|
//expect
|
||||||
|
expect(resp).toEqual("4.0.1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {Source} from '../../../models/database/source';
|
import {Source} from '../../../models/database/source';
|
||||||
import * as Oauth from '@panva/oauth4webapi';
|
import * as Oauth from '@panva/oauth4webapi';
|
||||||
import {IResourceInterface} from '../../interface';
|
import {IResourceRaw} from '../../interface';
|
||||||
|
|
||||||
// BaseClient is an abstract/partial class, its intended to be used by FHIR clients, and generically handle OAuth requests.
|
// BaseClient is an abstract/partial class, its intended to be used by FHIR clients, and generically handle OAuth requests.
|
||||||
export abstract class BaseClient {
|
export abstract class BaseClient {
|
||||||
|
@ -26,8 +26,14 @@ export abstract class BaseClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async GetFhirVersion(): Promise<any> {
|
||||||
|
return this.GetRequest("metadata")
|
||||||
|
.then((resp) => {
|
||||||
|
return resp.fhirVersion
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
public async GetRequest(resourceSubpathOrNext: string): Promise<IResourceInterface> {
|
public async GetRequest(resourceSubpathOrNext: string): Promise<any> {
|
||||||
|
|
||||||
//check if the url is absolute
|
//check if the url is absolute
|
||||||
let resourceUrl: string
|
let resourceUrl: string
|
||||||
|
@ -53,6 +59,22 @@ export abstract class BaseClient {
|
||||||
// return err
|
// return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Protected methods
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
protected GetPatientBundle(patientId: string): Promise<any> {
|
||||||
|
return this.GetRequest(`Patient/${patientId}/$everything`)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected GetPatient(patientId: string): Promise<IResourceRaw> {
|
||||||
|
return this.GetRequest(`Patient/${patientId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Private methods
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private async refreshExpiredTokenIfRequired(source: Source): Promise<Source> {
|
private async refreshExpiredTokenIfRequired(source: Source): Promise<Source> {
|
||||||
//check if token has expired, and a refreshtoken is available
|
//check if token has expired, and a refreshtoken is available
|
||||||
// Note: source.expires_at is in seconds, Date.now() is in milliseconds.
|
// Note: source.expires_at is in seconds, Date.now() is in milliseconds.
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import {FHIR401Client} from './fhir401_r4_client';
|
||||||
|
import {Source} from '../../../models/database/source';
|
||||||
|
import * as FHIR401Client_ProcessBundle from './fixtures/FHIR401Client_ProcessBundle.json';
|
||||||
|
import {IResourceBundleRaw} from '../../interface';
|
||||||
|
import {ResourceFhir} from '../../../models/database/resource_fhir';
|
||||||
|
import {NewRepositiory} from '../../../database/pouchdb_repository';
|
||||||
|
import {Base64} from '../../../utils/base64';
|
||||||
|
|
||||||
|
class TestClient extends FHIR401Client {
|
||||||
|
constructor(source: Source) {
|
||||||
|
super(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ProcessBundle(bundle: IResourceBundleRaw): Promise<ResourceFhir[]> {
|
||||||
|
return super.ProcessBundle(bundle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('FHIR401Client', () => {
|
||||||
|
let client: TestClient;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
client = new TestClient(new Source({
|
||||||
|
"_id": "source:aetna:12345",
|
||||||
|
"authorization_endpoint": "https://fhirsandbox.healthit.gov/open/r4/authorize",
|
||||||
|
"token_endpoint": "https://fhirsandbox.healthit.gov/open/r4/token",
|
||||||
|
"introspection_endpoint": "",
|
||||||
|
"issuer": "https://fhirsandbox.healthit.go",
|
||||||
|
"api_endpoint_base_url": "https://fhirsandbox.healthit.gov/secure/r4/fhir",
|
||||||
|
"client_id": "9ad3ML0upIMiawLVdM5-DiPinGcv7M",
|
||||||
|
"redirect_uri": "https://lighthouse.fastenhealth.com/sandbox/callback/healthit",
|
||||||
|
"confidential": false,
|
||||||
|
"source_type": "healthit",
|
||||||
|
"patient": "placeholder",
|
||||||
|
"access_token": "2e1be8c72d4d5225aae264a1fb7e1d3e",
|
||||||
|
"refresh_token": "",
|
||||||
|
"expires_at": 16649837100, //aug 11, 2497 (for testing)
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(client).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ProcessBundle', () => {
|
||||||
|
it('should correctly wrap each BundleEntry with ResourceFhir', async () => {
|
||||||
|
|
||||||
|
//setup
|
||||||
|
|
||||||
|
//test
|
||||||
|
const resp = await client.ProcessBundle(FHIR401Client_ProcessBundle)
|
||||||
|
//expect
|
||||||
|
expect(resp.length).toEqual(206);
|
||||||
|
expect(resp[0].source_resource_id).toEqual("c088b7af-fc41-43cc-ab80-4a9ab8d47cd9");
|
||||||
|
expect(resp[0].source_resource_type).toEqual("Patient");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('SyncAll', () => {
|
||||||
|
it('should correctly add resources to the database', async () => {
|
||||||
|
//setup
|
||||||
|
let response = new Response(JSON.stringify(FHIR401Client_ProcessBundle));
|
||||||
|
Object.defineProperty(response, "url", { value: `${client.source.api_endpoint_base_url}/Patient/${client.source.patient}/$everything`});
|
||||||
|
spyOn(window, "fetch").and.returnValue(Promise.resolve(response));
|
||||||
|
const db = NewRepositiory("fastentest")
|
||||||
|
|
||||||
|
//test
|
||||||
|
const resp = await client.SyncAll(db)
|
||||||
|
const firstResourceFhir = resp[0]
|
||||||
|
const resourceIdParts = firstResourceFhir.split(":")
|
||||||
|
|
||||||
|
//expect
|
||||||
|
expect(resp.length).toEqual(206);
|
||||||
|
expect(firstResourceFhir).toEqual('resource_fhir:c291cmNlOmFldG5hOjEyMzQ1:Patient:c088b7af-fc41-43cc-ab80-4a9ab8d47cd9');
|
||||||
|
expect(Base64.Decode(resourceIdParts[1])).toEqual("source:aetna:12345");
|
||||||
|
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
|
@ -0,0 +1,62 @@
|
||||||
|
import {IClient, IResourceBundleRaw, IResourceRaw} from '../../interface';
|
||||||
|
import {BaseClient} from './base_client';
|
||||||
|
import {Source} from '../../../models/database/source';
|
||||||
|
import {IDatabaseRepository} from '../../../database/interface';
|
||||||
|
import {ResourceFhir} from '../../../models/database/resource_fhir';
|
||||||
|
|
||||||
|
export class FHIR401Client extends BaseClient implements IClient {
|
||||||
|
|
||||||
|
//clients extending this class must validate fhirVersion matches using conformance/metadata url.
|
||||||
|
fhirVersion = "4.0.1"
|
||||||
|
|
||||||
|
constructor(source: Source) {
|
||||||
|
super(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async SyncAll(db: IDatabaseRepository): Promise<string[]> {
|
||||||
|
const bundle = await this.GetPatientBundle(this.source.patient)
|
||||||
|
|
||||||
|
const wrappedResourceModels = await this.ProcessBundle(bundle)
|
||||||
|
//todo, create the resources in dependency order
|
||||||
|
|
||||||
|
//TODO bulk insert
|
||||||
|
// for(let dbModel of wrappedResourceModels){
|
||||||
|
// db.CreateResource(dbModel)
|
||||||
|
// }
|
||||||
|
console.log(wrappedResourceModels)
|
||||||
|
return db.CreateResources(wrappedResourceModels)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async SyncAllBundle(db: IDatabaseRepository, bundleFile: any): Promise<any> {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Protected methods
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
protected async ProcessBundle(bundle: IResourceBundleRaw): Promise<ResourceFhir[]> {
|
||||||
|
// console.log(bundle)
|
||||||
|
// process each entry in bundle
|
||||||
|
return bundle.entry
|
||||||
|
.filter((bundleEntry) => {
|
||||||
|
return bundleEntry.resource.id // keep this entry if it has an ID, skip otherwise.
|
||||||
|
})
|
||||||
|
.map((bundleEntry) => {
|
||||||
|
const wrappedResourceModel = new ResourceFhir()
|
||||||
|
wrappedResourceModel.source_id = this.source._id
|
||||||
|
wrappedResourceModel.source_resource_id = bundleEntry.resource.id
|
||||||
|
wrappedResourceModel.source_resource_type = bundleEntry.resource.resourceType
|
||||||
|
wrappedResourceModel.resource_raw = bundleEntry.resource
|
||||||
|
// TODO find a way to safely/consistently get the resource updated date (and other metadata) which shoudl be added to the model.
|
||||||
|
// wrappedResourceModel.updated_at = bundleEntry.resource.meta?.lastUpdated
|
||||||
|
return wrappedResourceModel
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Private methods
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,159 @@
|
||||||
|
{
|
||||||
|
"resourceType": "Patient",
|
||||||
|
"id": "123d41e1-0f71-4e9f-8eb2-d1b1330201a6",
|
||||||
|
"meta": {
|
||||||
|
"versionId": "1.0"
|
||||||
|
},
|
||||||
|
"extension": [
|
||||||
|
{
|
||||||
|
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race",
|
||||||
|
"extension": [
|
||||||
|
{
|
||||||
|
"url": "ombCategory",
|
||||||
|
"valueCoding": {
|
||||||
|
"system": "urn:oid:2.16.840.1.113883.6.238",
|
||||||
|
"code": "2054-5",
|
||||||
|
"display": "Black or African American"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "text",
|
||||||
|
"valueString": "Black or African American"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity",
|
||||||
|
"extension": [
|
||||||
|
{
|
||||||
|
"url": "ombCategory",
|
||||||
|
"valueCoding": {
|
||||||
|
"system": "urn:oid:2.16.840.1.113883.6.238",
|
||||||
|
"code": "2135-2",
|
||||||
|
"display": "Hispanic or Latino"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "text",
|
||||||
|
"valueString": "Hispanic or Latino"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName",
|
||||||
|
"valueString": "Ramona980 Franco581"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex",
|
||||||
|
"valueCode": "M"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
|
||||||
|
"valueCode": "masked"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"identifier": [
|
||||||
|
{
|
||||||
|
"system": "https://github.com/synthetichealth/synthea",
|
||||||
|
"value": "123d41e1-0f71-4e9f-8eb2-d1b1330201a6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": {
|
||||||
|
"coding": [
|
||||||
|
{
|
||||||
|
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||||
|
"code": "MR",
|
||||||
|
"display": "Medical Record Number"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"system": "http://hospital.smarthealthit.org",
|
||||||
|
"value": "123d41e1-0f71-4e9f-8eb2-d1b1330201a6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": {
|
||||||
|
"coding": [
|
||||||
|
{
|
||||||
|
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||||
|
"code": "SS",
|
||||||
|
"display": "Social Security Number"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"system": "http://hl7.org/fhir/sid/us-ssn",
|
||||||
|
"value": "999-50-6731"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": {
|
||||||
|
"coding": [
|
||||||
|
{
|
||||||
|
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||||
|
"code": "DL",
|
||||||
|
"display": "Drivers License"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"system": "urn:oid:2.16.840.1.113883.4.3.25",
|
||||||
|
"value": "S99957478"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": [
|
||||||
|
{
|
||||||
|
"use": "official",
|
||||||
|
"family": "Gutiérrez115",
|
||||||
|
"given": [
|
||||||
|
"Hugo693"
|
||||||
|
],
|
||||||
|
"prefix": [
|
||||||
|
"Mr."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"telecom": [
|
||||||
|
{
|
||||||
|
"system": "phone",
|
||||||
|
"value": "555-914-6475",
|
||||||
|
"use": "home"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gender": "male",
|
||||||
|
"birthDate": "1969-01-12",
|
||||||
|
"address": [
|
||||||
|
{
|
||||||
|
"line": [
|
||||||
|
"[\"293 Douglas Ramp\"]"
|
||||||
|
],
|
||||||
|
"city": "Natick",
|
||||||
|
"state": "MA",
|
||||||
|
"postalCode": "01999",
|
||||||
|
"country": "US",
|
||||||
|
"period": {
|
||||||
|
"start": "1969-01-12T00:00:00+00:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"maritalStatus": {
|
||||||
|
"coding": [
|
||||||
|
{
|
||||||
|
"system": "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus",
|
||||||
|
"code": "M",
|
||||||
|
"display": "M"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "M"
|
||||||
|
},
|
||||||
|
"multipleBirthBoolean": false,
|
||||||
|
"communication": [
|
||||||
|
{
|
||||||
|
"language": {
|
||||||
|
"coding": [
|
||||||
|
{
|
||||||
|
"system": "urn:ietf:bcp:47",
|
||||||
|
"code": "es",
|
||||||
|
"display": "Spanish"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,14 +1,45 @@
|
||||||
import {IDatabaseRepository} from '../database/interface';
|
import {IDatabaseRepository} from '../database/interface';
|
||||||
|
|
||||||
export interface IClient {
|
export interface IClient {
|
||||||
|
fhirVersion: string
|
||||||
GetRequest(resourceSubpath: string): Promise<any>
|
GetRequest(resourceSubpath: string): Promise<any>
|
||||||
|
GetFhirVersion(): Promise<any>
|
||||||
SyncAll(db: IDatabaseRepository): Promise<any>
|
SyncAll(db: IDatabaseRepository): Promise<any>
|
||||||
|
|
||||||
//Manual client ONLY functions
|
//Manual client ONLY functions
|
||||||
SyncAllBundle(db: IDatabaseRepository, bundleFile: any): Promise<any>
|
SyncAllBundle(db: IDatabaseRepository, bundleFile: any): Promise<any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IResourceInterface {
|
//This is the "raw" Fhir resource
|
||||||
|
export interface IResourceRaw {
|
||||||
resourceType: string
|
resourceType: string
|
||||||
id?: string
|
id?: string
|
||||||
|
meta?: IResourceMetaRaw
|
||||||
|
}
|
||||||
|
// This is the "raw" Fhir Bundle resource
|
||||||
|
export interface IResourceBundleRaw {
|
||||||
|
resourceType: string
|
||||||
|
id?: string
|
||||||
|
entry: IResourceBundleEntryRaw[]
|
||||||
|
total?: number
|
||||||
|
link?: IResourceBundleLinkRaw[]
|
||||||
|
meta?: IResourceMetaRaw
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResourceBundleLinkRaw {
|
||||||
|
id?: string
|
||||||
|
relation: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResourceBundleEntryRaw {
|
||||||
|
id?: string
|
||||||
|
fullUrl?: string
|
||||||
|
resource: IResourceRaw
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResourceMetaRaw {
|
||||||
|
id?: string
|
||||||
|
versionId?: string
|
||||||
|
lastUpdated: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export enum DocType {
|
export enum DocType {
|
||||||
Source= "source",
|
Source = "source",
|
||||||
|
ResourceFhir = "resource_fhir"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import {Source} from '../models/database/source';
|
import {Source} from '../models/database/source';
|
||||||
|
import {ResourceFhir} from '../models/database/resource_fhir';
|
||||||
// import {SourceSummary} from '../../app/models/fasten/source-summary';
|
// import {SourceSummary} from '../../app/models/fasten/source-summary';
|
||||||
|
|
||||||
export interface IDatabaseDocument {
|
export interface IDatabaseDocument {
|
||||||
|
_id?: string
|
||||||
|
_rev?: string
|
||||||
|
doc_type: string
|
||||||
populateId(): void
|
populateId(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,4 +35,10 @@ export interface IDatabaseRepository {
|
||||||
DeleteSource(source_id: string): Promise<boolean>
|
DeleteSource(source_id: string): Promise<boolean>
|
||||||
// GetSourceSummary(source_id: string): Promise<SourceSummary>
|
// GetSourceSummary(source_id: string): Promise<SourceSummary>
|
||||||
GetSources(): Promise<IDatabasePaginatedResponse>
|
GetSources(): Promise<IDatabasePaginatedResponse>
|
||||||
|
|
||||||
|
|
||||||
|
CreateResource(resource: ResourceFhir): Promise<string>
|
||||||
|
CreateResources(resources: ResourceFhir[]): Promise<string[]>
|
||||||
|
GetResource(resource_id: string): Promise<ResourceFhir>
|
||||||
|
GetResources(): Promise<IDatabasePaginatedResponse>
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ describe('PouchdbRepository', () => {
|
||||||
|
|
||||||
const createdSource = await repository.GetSource(createdId)
|
const createdSource = await repository.GetSource(createdId)
|
||||||
|
|
||||||
expect(createdSource.docType).toEqual(DocType.Source);
|
expect(createdSource.doc_type).toEqual(DocType.Source);
|
||||||
expect(createdSource.patient).toEqual('patient');
|
expect(createdSource.patient).toEqual('patient');
|
||||||
expect(createdSource.source_type).toEqual(SourceType.Aetna);
|
expect(createdSource.source_type).toEqual(SourceType.Aetna);
|
||||||
expect(createdSource.access_token).toEqual('hello-world');
|
expect(createdSource.access_token).toEqual('hello-world');
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {IDatabasePaginatedResponse, IDatabaseDocument, IDatabaseRepository} from
|
||||||
import * as PouchDB from 'pouchdb/dist/pouchdb';
|
import * as PouchDB from 'pouchdb/dist/pouchdb';
|
||||||
// import * as PouchDB from 'pouchdb';
|
// import * as PouchDB from 'pouchdb';
|
||||||
import {DocType} from './constants';
|
import {DocType} from './constants';
|
||||||
|
import {ResourceFhir} from '../models/database/resource_fhir';
|
||||||
|
|
||||||
export function NewRepositiory(databaseName: string = 'fasten'): IDatabaseRepository {
|
export function NewRepositiory(databaseName: string = 'fasten'): IDatabaseRepository {
|
||||||
return new PouchdbRepository(databaseName)
|
return new PouchdbRepository(databaseName)
|
||||||
|
@ -44,6 +45,32 @@ export class PouchdbRepository implements IDatabaseRepository {
|
||||||
return this.deleteDocument(source_id)
|
return this.deleteDocument(source_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async CreateResource(resource: ResourceFhir): Promise<string> {
|
||||||
|
return this.createDocument(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async CreateResources(resources: ResourceFhir[]): Promise<string[]> {
|
||||||
|
return this.createBulk(resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async GetResource(resource_id: string): Promise<ResourceFhir> {
|
||||||
|
return this.getDocument(resource_id)
|
||||||
|
.then((doc) => {
|
||||||
|
return new ResourceFhir(doc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async GetResources(): Promise<IDatabasePaginatedResponse> {
|
||||||
|
return this.findDocumentByDocType(DocType.ResourceFhir)
|
||||||
|
.then((docWrapper) => {
|
||||||
|
|
||||||
|
docWrapper.rows = docWrapper.rows.map((result) => {
|
||||||
|
return new ResourceFhir(result.doc)
|
||||||
|
})
|
||||||
|
return docWrapper
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
// CRUD Operators
|
// CRUD Operators
|
||||||
|
@ -73,6 +100,15 @@ export class PouchdbRepository implements IDatabaseRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create multiple documents, returns a list of generated ids
|
||||||
|
private createBulk(docs: IDatabaseDocument[]): Promise<string[]> {
|
||||||
|
return this.GetDB()
|
||||||
|
.bulkDocs(docs.map((doc) => { doc.populateId(); return doc }))
|
||||||
|
.then((results): string[] => {
|
||||||
|
return results.map((result) => result.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private getDocument(id: string): Promise<any> {
|
private getDocument(id: string): Promise<any> {
|
||||||
return this.GetDB()
|
return this.GetDB()
|
||||||
.get(id)
|
.get(id)
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import {DocType} from '../../database/constants';
|
||||||
|
import {IResourceRaw} from '../../conduit/interface';
|
||||||
|
import {Base64} from '../../utils/base64';
|
||||||
|
|
||||||
|
export class ResourceFhir {
|
||||||
|
_id?: string
|
||||||
|
_rev?: string
|
||||||
|
doc_type: string
|
||||||
|
|
||||||
|
created_at?: Date
|
||||||
|
updated_at?: Date
|
||||||
|
source_id: string
|
||||||
|
source_resource_type: string
|
||||||
|
source_resource_id: string
|
||||||
|
|
||||||
|
resource_raw: IResourceRaw
|
||||||
|
|
||||||
|
constructor(object?: any) {
|
||||||
|
if(object){
|
||||||
|
object.doc_type = DocType.ResourceFhir
|
||||||
|
return Object.assign(this, object)
|
||||||
|
} else{
|
||||||
|
this.doc_type = DocType.ResourceFhir
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
populateId(){
|
||||||
|
//TODO: source_id should be base64 encoded (otherwise we get nested : chars)
|
||||||
|
this._id = `${this.doc_type}:${Base64.Encode(this.source_id)}:${this.source_resource_type}:${this.source_resource_id}`
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import {DocType} from '../../../lib/database/constants';
|
||||||
export class Source extends LighthouseSourceMetadata{
|
export class Source extends LighthouseSourceMetadata{
|
||||||
_id?: string
|
_id?: string
|
||||||
_rev?: string
|
_rev?: string
|
||||||
docType: string
|
doc_type: string
|
||||||
source_type: SourceType
|
source_type: SourceType
|
||||||
|
|
||||||
patient: string
|
patient: string
|
||||||
|
@ -16,11 +16,11 @@ export class Source extends LighthouseSourceMetadata{
|
||||||
|
|
||||||
constructor(object: any) {
|
constructor(object: any) {
|
||||||
super()
|
super()
|
||||||
object.docType = DocType.Source
|
object.doc_type = DocType.Source
|
||||||
return Object.assign(this, object)
|
return Object.assign(this, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
populateId(){
|
populateId(){
|
||||||
this._id = `source:${this.source_type}:${this.patient}`
|
this._id = `${this.doc_type}:${this.source_type}:${this.patient}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
export class Base64 {
|
||||||
|
public static Encode(data: string): string {
|
||||||
|
return btoa(data)
|
||||||
|
}
|
||||||
|
public static Decode(data: string): string {
|
||||||
|
return atob(data)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"resolveJsonModule": true,
|
||||||
"outDir": "./out-tsc/spec",
|
"outDir": "./out-tsc/spec",
|
||||||
"types": [
|
"types": [
|
||||||
"jasmine",
|
"jasmine",
|
||||||
|
|
Loading…
Reference in New Issue