provide a consistent way to retrun results from WebWorkers.
fixed Username field in login page. added support for CORS relay fixed spec files.
This commit is contained in:
parent
6af72266f6
commit
4f90a9eedb
|
@ -23,3 +23,9 @@ docker run --rm -it -p 5984:5984 -v `pwd`/.couchdb/data:/opt/couchdb/data -v `pw
|
|||
- WebUI:
|
||||
- username: `testuser`
|
||||
- password: `testuser`
|
||||
|
||||
|
||||
# Running tests
|
||||
|
||||
- ng test --include='**/base_client.spec.ts'
|
||||
- ng test --include='lib/**/*.spec.ts'
|
||||
|
|
|
@ -6,6 +6,4 @@ Find & replace the following
|
|||
- `fastenhealth` - find and replace this with your binary name
|
||||
- make sure you rename the folder as well.
|
||||
|
||||
# Running tests
|
||||
|
||||
- ng test --include='**/base_client.spec.ts'
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//TODO, there are security implications to this, we need to make sure we lock this down.
|
||||
func CORSProxy(c *gin.Context) {
|
||||
//appConfig := c.MustGet("CONFIG").(config.Interface)
|
||||
corsUrl := fmt.Sprintf("https://%s", strings.TrimPrefix(c.Param("proxyPath"), "/"))
|
||||
|
||||
remote, err := url.Parse(corsUrl)
|
||||
remote.RawQuery = c.Request.URL.Query().Encode()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
proxy := httputil.ReverseProxy{}
|
||||
//Define the director func
|
||||
//This is a good place to log, for example
|
||||
proxy.Director = func(req *http.Request) {
|
||||
req.Header = c.Request.Header
|
||||
req.Header.Add("X-Forwarded-Host", req.Host)
|
||||
req.Header.Add("X-Origin-Host", remote.Host)
|
||||
req.Host = remote.Host
|
||||
req.URL.Scheme = remote.Scheme
|
||||
req.URL.Host = remote.Host
|
||||
log.Printf(c.Param("proxyPath"))
|
||||
req.URL.Path = remote.Path
|
||||
|
||||
//TODO: throw an error if the remote.Host is not allowed
|
||||
}
|
||||
|
||||
proxy.ModifyResponse = func(r *http.Response) error {
|
||||
//b, _ := ioutil.ReadAll(r.Body)
|
||||
//buf := bytes.NewBufferString("Monkey")
|
||||
//buf.Write(b)
|
||||
//r.Body = ioutil.NopCloser(buf)
|
||||
r.Header.Set("Access-Control-Allow-Methods", "GET,HEAD")
|
||||
r.Header.Set("Access-Control-Allow-Credentials", "true")
|
||||
r.Header.Set("Access-Control-Allow-Origin", "*")
|
||||
return nil
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(c.Writer, c.Request)
|
||||
}
|
|
@ -44,6 +44,8 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
|
|||
api.GET("/metadata/source", handler.GetMetadataSource)
|
||||
|
||||
r.Any("/database/*proxyPath", handler.CouchDBProxy)
|
||||
r.GET("/cors/*proxyPath", handler.CORSProxy)
|
||||
r.OPTIONS("/cors/*proxyPath", handler.CORSProxy)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,4 +4,6 @@ export class SourceSyncMessage {
|
|||
source: Source
|
||||
userIdentifier: string
|
||||
encryptionKey?: string
|
||||
|
||||
response?: any
|
||||
}
|
||||
|
|
|
@ -7,18 +7,15 @@
|
|||
|
||||
<form (ngSubmit)="signinSubmit()" #userForm="ngForm">
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<input [(ngModel)]="existingUser.username" name="username" #username="ngModel" required minlength="2" type="text" class="form-control" placeholder="Enter your email">
|
||||
<label>Username</label>
|
||||
<input [(ngModel)]="existingUser.username" name="username" #username="ngModel" required minlength="2" type="text" class="form-control" placeholder="Enter your username">
|
||||
|
||||
<div *ngIf="username.invalid && (username.dirty || username.touched)" class="alert alert-danger">
|
||||
<div *ngIf="username.errors?.['required']">
|
||||
Email is required.
|
||||
Username is required.
|
||||
</div>
|
||||
<div *ngIf="username.errors?.['minlength']">
|
||||
Email must be at least 4 characters long.
|
||||
</div>
|
||||
<div *ngIf="username.errors?.['email']">
|
||||
Email is not a valid email address.
|
||||
Username must be at least 4 characters long.
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- form-group -->
|
||||
|
|
|
@ -14,6 +14,8 @@ import {SourceType} from '../../../lib/models/database/source_types';
|
|||
import {QueueService} from '../../workers/queue.service';
|
||||
import {ToastService} from '../../services/toast.service';
|
||||
import {ToastNotification, ToastType} from '../../models/fasten/toast';
|
||||
import {SourceSyncMessage} from '../../models/queue/source-sync-message';
|
||||
import {UpsertSummary} from '../../../lib/models/fasten/upsert-summary';
|
||||
// If you dont import this angular will import the wrong "Location"
|
||||
|
||||
export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
|
||||
|
@ -192,7 +194,7 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
expires_at: parseInt(getAccessTokenExpiration(payload, new BrowserAdapter())),
|
||||
})
|
||||
|
||||
await this.fastenDb.CreateSource(dbSourceCredential).then(console.log)
|
||||
await this.fastenDb.UpsertSource(dbSourceCredential).then(console.log)
|
||||
this.queueSourceSyncWorker(sourceType as SourceType, dbSourceCredential)
|
||||
|
||||
})
|
||||
|
@ -245,29 +247,40 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
// so that we can show incompelte statuses
|
||||
this.queueService.runSourceSyncWorker(source)
|
||||
.subscribe((msg) => {
|
||||
const sourceSyncMessage = JSON.parse(msg)
|
||||
const sourceSyncMessage = JSON.parse(msg) as SourceSyncMessage
|
||||
delete this.status[sourceType]
|
||||
// window.location.reload();
|
||||
|
||||
console.log("source sync-all response:", sourceSyncMessage)
|
||||
//remove item from available sources list, add to connected sources.
|
||||
this.availableSourceList.splice(this.availableSourceList.findIndex((item) => item.metadata.source_type == sourceType), 1);
|
||||
this.connectedSourceList.push({source: sourceSyncMessage.source, metadata: this.metadataSources[sourceType]})
|
||||
if(this.connectedSourceList.findIndex((item) => item.metadata.source_type == sourceType) == -1){
|
||||
//only add this as a connected source if its "new"
|
||||
this.connectedSourceList.push({source: sourceSyncMessage.source, metadata: this.metadataSources[sourceType]})
|
||||
}
|
||||
|
||||
const toastNotificaiton = new ToastNotification()
|
||||
toastNotificaiton.type = ToastType.Success
|
||||
toastNotificaiton.message = `Successfully connected ${sourceType}`
|
||||
this.toastService.show(toastNotificaiton)
|
||||
const toastNotification = new ToastNotification()
|
||||
toastNotification.type = ToastType.Success
|
||||
toastNotification.message = `Successfully connected ${sourceType}`
|
||||
|
||||
const upsertSummary = sourceSyncMessage.response as UpsertSummary
|
||||
if(upsertSummary && upsertSummary.totalResources != upsertSummary.updatedResources.length){
|
||||
toastNotification.message += `\n (total: ${upsertSummary.totalResources}, updated: ${upsertSummary.updatedResources.length})`
|
||||
} else if(upsertSummary){
|
||||
toastNotification.message += `\n (total: ${upsertSummary.totalResources})`
|
||||
}
|
||||
|
||||
this.toastService.show(toastNotification)
|
||||
},
|
||||
(err) => {
|
||||
delete this.status[sourceType]
|
||||
// window.location.reload();
|
||||
|
||||
const toastNotificaiton = new ToastNotification()
|
||||
toastNotificaiton.type = ToastType.Error
|
||||
toastNotificaiton.message = `An error occurred while accessing ${sourceType}: ${err}`
|
||||
toastNotificaiton.autohide = false
|
||||
this.toastService.show(toastNotificaiton)
|
||||
const toastNotification = new ToastNotification()
|
||||
toastNotification.type = ToastType.Error
|
||||
toastNotification.message = `An error occurred while accessing ${sourceType}: ${err}`
|
||||
toastNotification.autohide = false
|
||||
this.toastService.show(toastNotification)
|
||||
console.error(err)
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,13 +21,14 @@ export class SourceSyncWorker implements DoWork<string, string> {
|
|||
const client = NewClient(sourceSyncMessage.source.source_type, sourceSyncMessage.source)
|
||||
//TODO: validate the FHIR version from the datasource matches the client
|
||||
// if the source token has been refreshed, we need to store it in the DB.
|
||||
// await db.CreateSource()
|
||||
// await db.UpsertSource()
|
||||
|
||||
console.log("!!!!!!!!!!!!!!STARTING WORKER SYNC!!!!!!!!!", sourceSyncMessage)
|
||||
return client.SyncAll(db)
|
||||
.then((resp) => {
|
||||
console.log("!!!!!!!!!!!!!COMPLETE WORKER SYNC!!!!!!!!!!", resp)
|
||||
return JSON.stringify(resp)
|
||||
sourceSyncMessage.response = resp
|
||||
return JSON.stringify(sourceSyncMessage)
|
||||
|
||||
})
|
||||
.catch((err) => {
|
||||
|
|
|
@ -2,6 +2,7 @@ import {IClient} from '../interface';
|
|||
import {FHIR401Client} from './base/fhir401_r4_client';
|
||||
import {Source} from '../../models/database/source';
|
||||
import {IDatabaseRepository} from '../../database/interface';
|
||||
import {UpsertSummary} from '../../models/fasten/upsert-summary';
|
||||
|
||||
export class AetnaClient extends FHIR401Client implements IClient {
|
||||
constructor(source: Source) {
|
||||
|
@ -13,11 +14,11 @@ export class AetnaClient extends FHIR401Client implements IClient {
|
|||
* @param db
|
||||
* @constructor
|
||||
*/
|
||||
async SyncAll(db: IDatabaseRepository): Promise<string[]> {
|
||||
async SyncAll(db: IDatabaseRepository): Promise<UpsertSummary> {
|
||||
const bundle = await this.GetResourceBundlePaginated("Patient")
|
||||
|
||||
const wrappedResourceModels = await this.ProcessBundle(bundle)
|
||||
//todo, create the resources in dependency order
|
||||
return await db.CreateResources(wrappedResourceModels)
|
||||
return await db.UpsertResources(wrappedResourceModels)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import {IClient} from '../interface';
|
|||
import {FHIR401Client} from './base/fhir401_r4_client';
|
||||
import {Source} from '../../models/database/source';
|
||||
import {IDatabaseRepository} from '../../database/interface';
|
||||
import {UpsertSummary} from '../../models/fasten/upsert-summary';
|
||||
|
||||
export class AthenaClient extends FHIR401Client implements IClient {
|
||||
constructor(source: Source) {
|
||||
|
@ -13,7 +14,7 @@ export class AthenaClient extends FHIR401Client implements IClient {
|
|||
* @param db
|
||||
* @constructor
|
||||
*/
|
||||
async SyncAll(db: IDatabaseRepository): Promise<string[]> {
|
||||
async SyncAll(db: IDatabaseRepository): Promise<UpsertSummary> {
|
||||
const supportedResources: string[] = [
|
||||
"AllergyIntolerance",
|
||||
//"Binary",
|
||||
|
|
|
@ -55,6 +55,13 @@ export abstract class BaseClient {
|
|||
} else {
|
||||
resourceUrl = resourceSubpathOrNext
|
||||
}
|
||||
if(this.source.cors_relay_required){
|
||||
//this endpoint requires a CORS relay
|
||||
//get the path to the Fasten server, and append `cors/` and then append the request url
|
||||
let resourceParts = new URL(resourceUrl)
|
||||
resourceUrl = this.getCORSProxyPath() + `${resourceParts.hostname}${resourceParts.pathname}${resourceParts.search}`
|
||||
}
|
||||
|
||||
|
||||
//refresh the source if required
|
||||
this.source = await this.refreshExpiredTokenIfRequired(this.source)
|
||||
|
@ -80,6 +87,12 @@ export abstract class BaseClient {
|
|||
// Private methods
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private getCORSProxyPath(): string {
|
||||
const basePath = globalThis.location.pathname.split('/web').slice(0, 1)[0];
|
||||
|
||||
return `${globalThis.location.origin}${basePath || '/'}cors/`
|
||||
}
|
||||
|
||||
private async refreshExpiredTokenIfRequired(source: Source): Promise<Source> {
|
||||
//check if token has expired, and a refreshtoken is available
|
||||
// Note: source.expires_at is in seconds, Date.now() is in milliseconds.
|
||||
|
|
|
@ -4,9 +4,11 @@ import {IResourceBundleRaw} from '../../interface';
|
|||
import {ResourceFhir} from '../../../models/database/resource_fhir';
|
||||
import {NewRepositiory} from '../../../database/pouchdb_repository';
|
||||
import {Base64} from '../../../utils/base64';
|
||||
import * as PouchDB from 'pouchdb/dist/pouchdb';
|
||||
|
||||
// @ts-ignore
|
||||
import * as FHIR401Client_ProcessBundle from './fixtures/FHIR401Client_ProcessBundle.json';
|
||||
import {IDatabaseRepository} from '../../../database/interface';
|
||||
|
||||
class TestClient extends FHIR401Client {
|
||||
constructor(source: Source) {
|
||||
|
@ -59,23 +61,36 @@ describe('FHIR401Client', () => {
|
|||
})
|
||||
|
||||
describe('SyncAll', () => {
|
||||
let repository: IDatabaseRepository;
|
||||
|
||||
beforeEach(async () => {
|
||||
repository = NewRepositiory(null, null, new PouchDB("FHIR401Client-testing"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if(repository){
|
||||
const db = await repository.GetDB()
|
||||
db.destroy() //wipe the db.
|
||||
}
|
||||
})
|
||||
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 resp = await client.SyncAll(repository)
|
||||
const firstResourceFhir = resp[0]
|
||||
const resourceIdParts = firstResourceFhir.split(":")
|
||||
|
||||
//expect
|
||||
expect(resp.length).toEqual(206);
|
||||
expect(resp.totalResources).toEqual(206);
|
||||
expect(resp.updatedResources).toEqual([]);
|
||||
expect(firstResourceFhir).toEqual('resource_fhir:c291cmNlOmFldG5hOjEyMzQ1:Patient:c088b7af-fc41-43cc-ab80-4a9ab8d47cd9');
|
||||
expect(Base64.Decode(resourceIdParts[1])).toEqual("source:aetna:12345");
|
||||
|
||||
|
||||
});
|
||||
})
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import {BaseClient} from './base_client';
|
|||
import {Source} from '../../../models/database/source';
|
||||
import {IDatabaseRepository} from '../../../database/interface';
|
||||
import {ResourceFhir} from '../../../models/database/resource_fhir';
|
||||
import {UpsertSummary} from '../../../models/fasten/upsert-summary';
|
||||
|
||||
export class FHIR401Client extends BaseClient implements IClient {
|
||||
|
||||
|
@ -18,13 +19,13 @@ export class FHIR401Client extends BaseClient implements IClient {
|
|||
* @param db
|
||||
* @constructor
|
||||
*/
|
||||
public async SyncAll(db: IDatabaseRepository): Promise<string[]> {
|
||||
public async SyncAll(db: IDatabaseRepository): Promise<UpsertSummary> {
|
||||
const bundle = await this.GetPatientBundle(this.source.patient)
|
||||
|
||||
const wrappedResourceModels = await this.ProcessBundle(bundle)
|
||||
//todo, create the resources in dependency order
|
||||
|
||||
return db.CreateResources(wrappedResourceModels)
|
||||
return db.UpsertResources(wrappedResourceModels)
|
||||
|
||||
}
|
||||
|
||||
|
@ -35,7 +36,7 @@ export class FHIR401Client extends BaseClient implements IClient {
|
|||
* @param resourceNames
|
||||
* @constructor
|
||||
*/
|
||||
public async SyncAllByResourceName(db: IDatabaseRepository, resourceNames: string[]): Promise<string[]>{
|
||||
public async SyncAllByResourceName(db: IDatabaseRepository, resourceNames: string[]): Promise<UpsertSummary>{
|
||||
//Store the Patient
|
||||
const patientResource = await this.GetPatient(this.source.patient)
|
||||
|
||||
|
@ -45,7 +46,7 @@ export class FHIR401Client extends BaseClient implements IClient {
|
|||
patientResourceFhir.source_resource_id = patientResource.id
|
||||
patientResourceFhir.resource_raw = patientResource
|
||||
|
||||
await db.CreateResource(patientResourceFhir)
|
||||
const upsertSummary = await db.UpsertResource(patientResourceFhir)
|
||||
|
||||
//error map storage.
|
||||
let syncErrors = {}
|
||||
|
@ -56,7 +57,9 @@ export class FHIR401Client extends BaseClient implements IClient {
|
|||
try {
|
||||
let bundle = await this.GetResourceBundlePaginated(`${resourceType}?patient=${this.source.patient}`)
|
||||
let wrappedResourceModels = await this.ProcessBundle(bundle)
|
||||
await db.CreateResources(wrappedResourceModels)
|
||||
let resourceUpsertSummary = await db.UpsertResources(wrappedResourceModels)
|
||||
upsertSummary.updatedResources = upsertSummary.updatedResources.concat(resourceUpsertSummary.updatedResources)
|
||||
upsertSummary.totalResources += resourceUpsertSummary.totalResources
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`An error occurred while processing ${resourceType} bundle ${this.source.patient}`)
|
||||
|
@ -66,7 +69,7 @@ export class FHIR401Client extends BaseClient implements IClient {
|
|||
}
|
||||
|
||||
//TODO: correctly return newly inserted documents
|
||||
return []
|
||||
return upsertSummary
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -75,7 +78,7 @@ export class FHIR401Client extends BaseClient implements IClient {
|
|||
* @param bundleFile
|
||||
* @constructor
|
||||
*/
|
||||
public async SyncAllFromBundleFile(db: IDatabaseRepository, bundleFile: any): Promise<any> {
|
||||
public async SyncAllFromBundleFile(db: IDatabaseRepository, bundleFile: any): Promise<UpsertSummary> {
|
||||
return Promise.reject(new Error("not implemented"));
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import {IClient} from '../interface';
|
|||
import {FHIR401Client} from './base/fhir401_r4_client';
|
||||
import {Source} from '../../models/database/source';
|
||||
import {IDatabaseRepository} from '../../database/interface';
|
||||
import {UpsertSummary} from '../../models/fasten/upsert-summary';
|
||||
|
||||
export class BlueButtonClient extends FHIR401Client implements IClient {
|
||||
constructor(source: Source) {
|
||||
|
@ -13,7 +14,7 @@ export class BlueButtonClient extends FHIR401Client implements IClient {
|
|||
* @param db
|
||||
* @constructor
|
||||
*/
|
||||
async SyncAll(db: IDatabaseRepository): Promise<string[]> {
|
||||
async SyncAll(db: IDatabaseRepository): Promise<UpsertSummary> {
|
||||
const supportedResources: string[] = [
|
||||
"ExplanationOfBenefit",
|
||||
"Coverage",
|
||||
|
|
|
@ -2,6 +2,7 @@ import {IClient} from '../interface';
|
|||
import {FHIR401Client} from './base/fhir401_r4_client';
|
||||
import {Source} from '../../models/database/source';
|
||||
import {IDatabaseRepository} from '../../database/interface';
|
||||
import {UpsertSummary} from '../../models/fasten/upsert-summary';
|
||||
|
||||
export class CernerClient extends FHIR401Client implements IClient {
|
||||
constructor(source: Source) {
|
||||
|
@ -15,7 +16,7 @@ export class CernerClient extends FHIR401Client implements IClient {
|
|||
* @param db
|
||||
* @constructor
|
||||
*/
|
||||
async SyncAll(db: IDatabaseRepository): Promise<string[]> {
|
||||
async SyncAll(db: IDatabaseRepository): Promise<UpsertSummary> {
|
||||
const supportedResources: string[] = [
|
||||
"AllergyIntolerance",
|
||||
"CarePlan",
|
||||
|
|
|
@ -2,6 +2,7 @@ import {IClient} from '../interface';
|
|||
import {FHIR401Client} from './base/fhir401_r4_client';
|
||||
import {Source} from '../../models/database/source';
|
||||
import {IDatabaseRepository} from '../../database/interface';
|
||||
import {UpsertSummary} from '../../models/fasten/upsert-summary';
|
||||
|
||||
export class EpicClient extends FHIR401Client implements IClient {
|
||||
constructor(source: Source) {
|
||||
|
@ -15,7 +16,7 @@ export class EpicClient extends FHIR401Client implements IClient {
|
|||
* @param db
|
||||
* @constructor
|
||||
*/
|
||||
async SyncAll(db: IDatabaseRepository): Promise<string[]> {
|
||||
async SyncAll(db: IDatabaseRepository): Promise<UpsertSummary> {
|
||||
const supportedResources: string[] = [
|
||||
"AllergyIntolerance",
|
||||
"CarePlan",
|
||||
|
|
|
@ -2,6 +2,7 @@ import {IClient} from '../interface';
|
|||
import {FHIR401Client} from './base/fhir401_r4_client';
|
||||
import {Source} from '../../models/database/source';
|
||||
import {IDatabaseRepository} from '../../database/interface';
|
||||
import {UpsertSummary} from '../../models/fasten/upsert-summary';
|
||||
|
||||
export class HealthITClient extends FHIR401Client implements IClient {
|
||||
constructor(source: Source) {
|
||||
|
@ -15,7 +16,7 @@ export class HealthITClient extends FHIR401Client implements IClient {
|
|||
* @param db
|
||||
* @constructor
|
||||
*/
|
||||
async SyncAll(db: IDatabaseRepository): Promise<string[]> {
|
||||
async SyncAll(db: IDatabaseRepository): Promise<UpsertSummary> {
|
||||
const supportedResources: string[] = [
|
||||
"AllergyIntolerance",
|
||||
"CarePlan",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {IDatabaseRepository} from '../database/interface';
|
||||
import {UpsertSummary} from '../models/fasten/upsert-summary';
|
||||
|
||||
export interface IClient {
|
||||
fhirVersion: string
|
||||
|
@ -10,13 +11,13 @@ export interface IClient {
|
|||
* @param db
|
||||
* @constructor
|
||||
*/
|
||||
SyncAll(db: IDatabaseRepository): Promise<any>
|
||||
SyncAll(db: IDatabaseRepository): Promise<UpsertSummary>
|
||||
|
||||
|
||||
SyncAllByResourceName(db: IDatabaseRepository, resourceNames: string[]): Promise<string[]>
|
||||
SyncAllByResourceName(db: IDatabaseRepository, resourceNames: string[]): Promise<UpsertSummary>
|
||||
|
||||
//Manual client ONLY functions
|
||||
SyncAllFromBundleFile(db: IDatabaseRepository, bundleFile: any): Promise<any>
|
||||
SyncAllFromBundleFile(db: IDatabaseRepository, bundleFile: any): Promise<UpsertSummary>
|
||||
}
|
||||
|
||||
//This is the "raw" Fhir resource
|
||||
|
|
|
@ -3,6 +3,7 @@ import {ResourceFhir} from '../models/database/resource_fhir';
|
|||
import {SourceSummary} from '../models/fasten/source-summary';
|
||||
import {Summary} from '../models/fasten/summary';
|
||||
import {User} from '../models/fasten/user';
|
||||
import {UpsertSummary} from '../models/fasten/upsert-summary';
|
||||
// import {SourceSummary} from '../../app/models/fasten/source-summary';
|
||||
|
||||
export interface IDatabaseDocument {
|
||||
|
@ -28,7 +29,7 @@ export interface IDatabaseRepository {
|
|||
// GetUserByEmail(context.Context, string) (*models.User, error)
|
||||
// GetCurrentUser(context.Context) *models.User
|
||||
|
||||
CreateSource(source: Source): Promise<string>
|
||||
UpsertSource(source: Source): Promise<UpsertSummary>
|
||||
GetSource(source_id: string): Promise<Source>
|
||||
DeleteSource(source_id: string): Promise<boolean>
|
||||
GetSourceSummary(source_id: string): Promise<SourceSummary>
|
||||
|
@ -40,8 +41,8 @@ export interface IDatabaseRepository {
|
|||
// GetResourceBySourceId(context.Context, string, string) (*models.ResourceFhir, error)
|
||||
// ListResources(context.Context, models.ListResourceQueryOptions) ([]models.ResourceFhir, error)
|
||||
// GetPatientForSources(ctx context.Context) ([]models.ResourceFhir, error)
|
||||
CreateResource(resource: ResourceFhir): Promise<string>
|
||||
CreateResources(resources: ResourceFhir[]): Promise<string[]>
|
||||
UpsertResource(resource: ResourceFhir): Promise<UpsertSummary>
|
||||
UpsertResources(resources: ResourceFhir[]): Promise<UpsertSummary>
|
||||
GetResource(resource_id: string): Promise<ResourceFhir>
|
||||
GetResources(): Promise<IDatabasePaginatedResponse>
|
||||
GetResourcesForSource(source_id: string, source_resource_type?: string): Promise<IDatabasePaginatedResponse>
|
||||
|
|
|
@ -3,17 +3,19 @@ import {NewRepositiory} from './pouchdb_repository';
|
|||
import {SourceType} from '../models/database/source_types';
|
||||
import {Source} from '../models/database/source';
|
||||
import {DocType} from './constants';
|
||||
import * as PouchDB from 'pouchdb/dist/pouchdb';
|
||||
|
||||
describe('PouchdbRepository', () => {
|
||||
let repository: IDatabaseRepository;
|
||||
|
||||
beforeEach(async () => {
|
||||
repository = NewRepositiory();
|
||||
repository = NewRepositiory(null, null, new PouchDB("PouchdbRepository-testing"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if(repository){
|
||||
await repository.GetDB().destroy() //wipe the db.
|
||||
const db = await repository.GetDB()
|
||||
db.destroy() //wipe the db.
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -24,24 +26,25 @@ describe('PouchdbRepository', () => {
|
|||
|
||||
describe('CreateSource', () => {
|
||||
it('should return an id', async () => {
|
||||
const createdId = await repository.CreateSource(new Source({
|
||||
const createdId = await repository.UpsertSource(new Source({
|
||||
patient: 'patient',
|
||||
source_type: SourceType.Aetna,
|
||||
}))
|
||||
|
||||
expect(createdId).toEqual("source:aetna:patient");
|
||||
expect(createdId.totalResources).toEqual(1);
|
||||
expect(createdId.updatedResources[0]).toEqual("source:aetna:patient");
|
||||
});
|
||||
})
|
||||
|
||||
describe('GetSource', () => {
|
||||
it('should return an source', async () => {
|
||||
const createdId = await repository.CreateSource(new Source({
|
||||
const createdResource = await repository.UpsertSource(new Source({
|
||||
patient: 'patient',
|
||||
source_type: SourceType.Aetna,
|
||||
access_token: 'hello-world',
|
||||
}))
|
||||
|
||||
const createdSource = await repository.GetSource(createdId)
|
||||
const createdSource = await repository.GetSource(createdResource.updatedResources[0])
|
||||
|
||||
expect(createdSource.doc_type).toEqual(DocType.Source);
|
||||
expect(createdSource.patient).toEqual('patient');
|
||||
|
@ -52,13 +55,13 @@ describe('PouchdbRepository', () => {
|
|||
|
||||
describe('DeleteSource', () => {
|
||||
it('should delete a source', async () => {
|
||||
const createdId = await repository.CreateSource(new Source({
|
||||
const createdResource = await repository.UpsertSource(new Source({
|
||||
patient: 'patient-to-delete',
|
||||
source_type: SourceType.Aetna,
|
||||
access_token: 'hello-world',
|
||||
}))
|
||||
console.log(createdId)
|
||||
const deletedSource = await repository.DeleteSource(createdId)
|
||||
console.log(createdResource)
|
||||
const deletedSource = await repository.DeleteSource(createdResource.updatedResources[0])
|
||||
|
||||
expect(deletedSource).toBeTruthy();
|
||||
});
|
||||
|
@ -66,19 +69,19 @@ describe('PouchdbRepository', () => {
|
|||
|
||||
describe('GetSources', () => {
|
||||
it('should return a list of sources', async () => {
|
||||
await repository.CreateSource(new Source({
|
||||
await repository.UpsertSource(new Source({
|
||||
patient: 'patient1',
|
||||
source_type: SourceType.Aetna,
|
||||
access_token: 'hello-world1',
|
||||
}))
|
||||
|
||||
await repository.CreateSource(new Source({
|
||||
await repository.UpsertSource(new Source({
|
||||
patient: 'patient2',
|
||||
source_type: SourceType.Aetna,
|
||||
access_token: 'hello-world2',
|
||||
}))
|
||||
|
||||
await repository.CreateSource(new Source({
|
||||
await repository.UpsertSource(new Source({
|
||||
patient: 'patient3',
|
||||
source_type: SourceType.Aetna,
|
||||
access_token: 'hello-world3',
|
||||
|
|
|
@ -9,6 +9,7 @@ import {Base64} from '../utils/base64';
|
|||
import * as PouchDB from 'pouchdb/dist/pouchdb';
|
||||
import * as PouchCrypto from 'crypto-pouch';
|
||||
import {PouchdbUpsert} from './plugins/upsert';
|
||||
import {UpsertSummary} from '../models/fasten/upsert-summary';
|
||||
PouchDB.plugin(PouchCrypto);
|
||||
|
||||
|
||||
|
@ -42,8 +43,8 @@ PouchDB.plugin(PouchCrypto);
|
|||
* Eventually this method should dyanmically dtermine the version of the repo to return from the env.
|
||||
* @constructor
|
||||
*/
|
||||
export function NewRepositiory(userIdentifier?: string, encryptionKey?: string): IDatabaseRepository {
|
||||
return new PouchdbRepository(userIdentifier, encryptionKey)
|
||||
export function NewRepositiory(userIdentifier?: string, encryptionKey?: string, localPouchDb?: PouchDB.Database): IDatabaseRepository {
|
||||
return new PouchdbRepository(userIdentifier, encryptionKey, localPouchDb)
|
||||
}
|
||||
|
||||
export class PouchdbRepository implements IDatabaseRepository {
|
||||
|
@ -60,7 +61,7 @@ export class PouchdbRepository implements IDatabaseRepository {
|
|||
* @param userIdentifier
|
||||
* @param encryptionKey
|
||||
*/
|
||||
constructor(userIdentifier?: string, encryptionKey?: string) {
|
||||
constructor(userIdentifier?: string, encryptionKey?: string, localPouchDb?: PouchDB.Database) {
|
||||
this.remotePouchEndpoint = `${globalThis.location.protocol}//${globalThis.location.host}${this.getBasePath()}/database`
|
||||
|
||||
//setup PouchDB Plugins
|
||||
|
@ -71,6 +72,10 @@ export class PouchdbRepository implements IDatabaseRepository {
|
|||
this.encryptionKey = encryptionKey
|
||||
// this.enableSync(userIdentifier)
|
||||
}
|
||||
if(localPouchDb){
|
||||
console.warn("using local pouchdb, this should only be used for testing")
|
||||
this.pouchDb = localPouchDb
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -96,7 +101,7 @@ export class PouchdbRepository implements IDatabaseRepository {
|
|||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// Source
|
||||
|
||||
public async CreateSource(source: Source): Promise<string> {
|
||||
public async UpsertSource(source: Source): Promise<UpsertSummary> {
|
||||
return this.upsertDocument(source);
|
||||
}
|
||||
|
||||
|
@ -156,11 +161,11 @@ export class PouchdbRepository implements IDatabaseRepository {
|
|||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// Resource
|
||||
|
||||
public async CreateResource(resource: ResourceFhir): Promise<string> {
|
||||
public async UpsertResource(resource: ResourceFhir): Promise<UpsertSummary> {
|
||||
return this.upsertDocument(resource);
|
||||
}
|
||||
|
||||
public async CreateResources(resources: ResourceFhir[]): Promise<string[]> {
|
||||
public async UpsertResources(resources: ResourceFhir[]): Promise<UpsertSummary> {
|
||||
return this.upsertBulk(resources);
|
||||
}
|
||||
|
||||
|
@ -228,7 +233,7 @@ export class PouchdbRepository implements IDatabaseRepository {
|
|||
|
||||
|
||||
// update/insert a new document. Returns a promise of the generated id.
|
||||
protected upsertDocument(newDoc: IDatabaseDocument) : Promise<string> {
|
||||
protected upsertDocument(newDoc: IDatabaseDocument) : Promise<UpsertSummary> {
|
||||
// make sure we always "populate" the ID for every document before submitting
|
||||
newDoc.populateId()
|
||||
|
||||
|
@ -285,22 +290,29 @@ export class PouchdbRepository implements IDatabaseRepository {
|
|||
})
|
||||
|
||||
})
|
||||
.then(( result ): string => {
|
||||
return( result.id );
|
||||
.then(( result ): UpsertSummary => {
|
||||
// // success, res is {rev: '1-xxx', updated: true, id: 'myDocId'}
|
||||
const updateSummary = new UpsertSummary()
|
||||
updateSummary.totalResources = 1
|
||||
|
||||
if(result.updated){
|
||||
updateSummary.updatedResources = [result.id]
|
||||
}
|
||||
);
|
||||
return updateSummary;
|
||||
});
|
||||
}
|
||||
|
||||
protected upsertBulk(docs: IDatabaseDocument[]): Promise<string[]> {
|
||||
return this.GetDB()
|
||||
.then((db) => {
|
||||
|
||||
return Promise.all(docs.map((doc) => {
|
||||
doc.populateId();
|
||||
return this.upsertDocument(doc)
|
||||
}))
|
||||
|
||||
})
|
||||
protected upsertBulk(docs: IDatabaseDocument[]): Promise<UpsertSummary> {
|
||||
return Promise.all(docs.map((doc) => {
|
||||
doc.populateId();
|
||||
return this.upsertDocument(doc)
|
||||
})).then((results) => {
|
||||
return results.reduce((prev, current ) => {
|
||||
prev.totalResources += current.totalResources
|
||||
prev.updatedResources = prev.updatedResources.concat(current.updatedResources)
|
||||
return prev
|
||||
}, new UpsertSummary())
|
||||
})
|
||||
}
|
||||
|
||||
protected getDocument(id: string): Promise<any> {
|
||||
|
@ -333,44 +345,6 @@ export class PouchdbRepository implements IDatabaseRepository {
|
|||
})
|
||||
}
|
||||
|
||||
//DEPRECATED
|
||||
/**
|
||||
* create multiple documents, returns a list of generated ids
|
||||
* @deprecated
|
||||
* @param docs
|
||||
* @protected
|
||||
*/
|
||||
protected createBulk(docs: IDatabaseDocument[]): Promise<string[]> {
|
||||
return this.GetDB()
|
||||
.then((db) => {
|
||||
return db.bulkDocs(docs.map((doc) => { doc.populateId(); return doc }))
|
||||
})
|
||||
.then((results): string[] => {
|
||||
return results.map((result) => result.id)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* create a new document. Returns a promise of the generated id.
|
||||
* @deprecated
|
||||
* @param doc
|
||||
* @protected
|
||||
*/
|
||||
protected createDocument(doc: IDatabaseDocument) : Promise<string> {
|
||||
// make sure we always "populate" the ID for every document before submitting
|
||||
doc.populateId()
|
||||
|
||||
// NOTE: All friends are given the key-prefix of "friend:". This way, when we go
|
||||
// to query for friends, we can limit the scope to keys with in this key-space.
|
||||
|
||||
return this.GetDB()
|
||||
.then((db) => db.put(doc))
|
||||
.then(( result ): string => {
|
||||
return( result.id );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// Sync private/protected methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import {Source} from '../database/source';
|
||||
import {ResourceFhir} from '../database/resource_fhir';
|
||||
import {ResourceTypeCounts} from './source-summary';
|
||||
|
||||
export class UpsertSummary {
|
||||
updatedResources: string[] = []
|
||||
totalResources: number = 0
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true, //without this the "dom" and the "webworker" libs conflict
|
||||
|
||||
"resolveJsonModule": true,
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
|
|
Loading…
Reference in New Issue