From 4f90a9eedb5b0f4cfe22f598f39b17bedb609230 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Wed, 12 Oct 2022 18:47:12 -0700 Subject: [PATCH] provide a consistent way to retrun results from WebWorkers. fixed Username field in login page. added support for CORS relay fixed spec files. --- CONTRIBUTING.md | 6 ++ README.md | 4 +- backend/pkg/web/handler/cors_proxy.go | 52 +++++++++++ backend/pkg/web/server.go | 2 + .../app/models/queue/source-sync-message.ts | 2 + .../auth-signin/auth-signin.component.html | 11 +-- .../medical-sources.component.ts | 37 +++++--- .../src/app/workers/source-sync.worker.ts | 5 +- frontend/src/lib/conduit/fhir/aetna_client.ts | 5 +- .../src/lib/conduit/fhir/athena_client.ts | 3 +- .../src/lib/conduit/fhir/base/base_client.ts | 13 +++ .../fhir/base/fhir401_r4_client.spec.ts | 21 ++++- .../conduit/fhir/base/fhir401_r4_client.ts | 17 ++-- .../src/lib/conduit/fhir/bluebutton_client.ts | 3 +- .../src/lib/conduit/fhir/cerner_client.ts | 3 +- frontend/src/lib/conduit/fhir/epic_client.ts | 3 +- .../src/lib/conduit/fhir/healthit_client.ts | 3 +- frontend/src/lib/conduit/interface.ts | 7 +- frontend/src/lib/database/interface.ts | 7 +- .../lib/database/pouchdb_repository.spec.ts | 27 +++--- .../src/lib/database/pouchdb_repository.ts | 90 +++++++------------ .../src/lib/models/fasten/upsert-summary.ts | 8 ++ frontend/tsconfig.spec.json | 3 +- 23 files changed, 214 insertions(+), 118 deletions(-) create mode 100644 backend/pkg/web/handler/cors_proxy.go create mode 100644 frontend/src/lib/models/fasten/upsert-summary.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 97379df2..365c1726 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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' diff --git a/README.md b/README.md index 9dc55377..2e961d9d 100644 --- a/README.md +++ b/README.md @@ -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' + diff --git a/backend/pkg/web/handler/cors_proxy.go b/backend/pkg/web/handler/cors_proxy.go new file mode 100644 index 00000000..31fc2ea5 --- /dev/null +++ b/backend/pkg/web/handler/cors_proxy.go @@ -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) +} diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index 7bc546bc..3a20f628 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -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) } } diff --git a/frontend/src/app/models/queue/source-sync-message.ts b/frontend/src/app/models/queue/source-sync-message.ts index 4e22e512..6bde5b7f 100644 --- a/frontend/src/app/models/queue/source-sync-message.ts +++ b/frontend/src/app/models/queue/source-sync-message.ts @@ -4,4 +4,6 @@ export class SourceSyncMessage { source: Source userIdentifier: string encryptionKey?: string + + response?: any } diff --git a/frontend/src/app/pages/auth-signin/auth-signin.component.html b/frontend/src/app/pages/auth-signin/auth-signin.component.html index 3db5b09b..e4fa1e2e 100644 --- a/frontend/src/app/pages/auth-signin/auth-signin.component.html +++ b/frontend/src/app/pages/auth-signin/auth-signin.component.html @@ -7,18 +7,15 @@
- - + +
- Email is required. + Username is required.
- Email must be at least 4 characters long. -
-
- Email is not a valid email address. + Username must be at least 4 characters long.
diff --git a/frontend/src/app/pages/medical-sources/medical-sources.component.ts b/frontend/src/app/pages/medical-sources/medical-sources.component.ts index 3fe74d32..e01548e7 100644 --- a/frontend/src/app/pages/medical-sources/medical-sources.component.ts +++ b/frontend/src/app/pages/medical-sources/medical-sources.component.ts @@ -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) }); } diff --git a/frontend/src/app/workers/source-sync.worker.ts b/frontend/src/app/workers/source-sync.worker.ts index e378ec98..a3ede192 100644 --- a/frontend/src/app/workers/source-sync.worker.ts +++ b/frontend/src/app/workers/source-sync.worker.ts @@ -21,13 +21,14 @@ export class SourceSyncWorker implements DoWork { 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) => { diff --git a/frontend/src/lib/conduit/fhir/aetna_client.ts b/frontend/src/lib/conduit/fhir/aetna_client.ts index cc3c57cd..fff74203 100644 --- a/frontend/src/lib/conduit/fhir/aetna_client.ts +++ b/frontend/src/lib/conduit/fhir/aetna_client.ts @@ -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 { + async SyncAll(db: IDatabaseRepository): Promise { 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) } } diff --git a/frontend/src/lib/conduit/fhir/athena_client.ts b/frontend/src/lib/conduit/fhir/athena_client.ts index 05493fff..9c471a64 100644 --- a/frontend/src/lib/conduit/fhir/athena_client.ts +++ b/frontend/src/lib/conduit/fhir/athena_client.ts @@ -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 { + async SyncAll(db: IDatabaseRepository): Promise { const supportedResources: string[] = [ "AllergyIntolerance", //"Binary", diff --git a/frontend/src/lib/conduit/fhir/base/base_client.ts b/frontend/src/lib/conduit/fhir/base/base_client.ts index aecf2ae6..a0fc57c7 100644 --- a/frontend/src/lib/conduit/fhir/base/base_client.ts +++ b/frontend/src/lib/conduit/fhir/base/base_client.ts @@ -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 { //check if token has expired, and a refreshtoken is available // Note: source.expires_at is in seconds, Date.now() is in milliseconds. diff --git a/frontend/src/lib/conduit/fhir/base/fhir401_r4_client.spec.ts b/frontend/src/lib/conduit/fhir/base/fhir401_r4_client.spec.ts index b94e9db6..04320aed 100644 --- a/frontend/src/lib/conduit/fhir/base/fhir401_r4_client.spec.ts +++ b/frontend/src/lib/conduit/fhir/base/fhir401_r4_client.spec.ts @@ -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"); + }); }) diff --git a/frontend/src/lib/conduit/fhir/base/fhir401_r4_client.ts b/frontend/src/lib/conduit/fhir/base/fhir401_r4_client.ts index d6779c9a..fa0e59a4 100644 --- a/frontend/src/lib/conduit/fhir/base/fhir401_r4_client.ts +++ b/frontend/src/lib/conduit/fhir/base/fhir401_r4_client.ts @@ -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 { + public async SyncAll(db: IDatabaseRepository): Promise { 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{ + public async SyncAllByResourceName(db: IDatabaseRepository, resourceNames: string[]): Promise{ //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 { + public async SyncAllFromBundleFile(db: IDatabaseRepository, bundleFile: any): Promise { return Promise.reject(new Error("not implemented")); } diff --git a/frontend/src/lib/conduit/fhir/bluebutton_client.ts b/frontend/src/lib/conduit/fhir/bluebutton_client.ts index cb8569cd..c4dc3074 100644 --- a/frontend/src/lib/conduit/fhir/bluebutton_client.ts +++ b/frontend/src/lib/conduit/fhir/bluebutton_client.ts @@ -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 { + async SyncAll(db: IDatabaseRepository): Promise { const supportedResources: string[] = [ "ExplanationOfBenefit", "Coverage", diff --git a/frontend/src/lib/conduit/fhir/cerner_client.ts b/frontend/src/lib/conduit/fhir/cerner_client.ts index 8120dfd3..3d5147bf 100644 --- a/frontend/src/lib/conduit/fhir/cerner_client.ts +++ b/frontend/src/lib/conduit/fhir/cerner_client.ts @@ -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 { + async SyncAll(db: IDatabaseRepository): Promise { const supportedResources: string[] = [ "AllergyIntolerance", "CarePlan", diff --git a/frontend/src/lib/conduit/fhir/epic_client.ts b/frontend/src/lib/conduit/fhir/epic_client.ts index bbb0ca55..f7348b88 100644 --- a/frontend/src/lib/conduit/fhir/epic_client.ts +++ b/frontend/src/lib/conduit/fhir/epic_client.ts @@ -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 { + async SyncAll(db: IDatabaseRepository): Promise { const supportedResources: string[] = [ "AllergyIntolerance", "CarePlan", diff --git a/frontend/src/lib/conduit/fhir/healthit_client.ts b/frontend/src/lib/conduit/fhir/healthit_client.ts index 32ace05d..24c47547 100644 --- a/frontend/src/lib/conduit/fhir/healthit_client.ts +++ b/frontend/src/lib/conduit/fhir/healthit_client.ts @@ -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 { + async SyncAll(db: IDatabaseRepository): Promise { const supportedResources: string[] = [ "AllergyIntolerance", "CarePlan", diff --git a/frontend/src/lib/conduit/interface.ts b/frontend/src/lib/conduit/interface.ts index 61b4c980..1e0ff982 100644 --- a/frontend/src/lib/conduit/interface.ts +++ b/frontend/src/lib/conduit/interface.ts @@ -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 + SyncAll(db: IDatabaseRepository): Promise - SyncAllByResourceName(db: IDatabaseRepository, resourceNames: string[]): Promise + SyncAllByResourceName(db: IDatabaseRepository, resourceNames: string[]): Promise //Manual client ONLY functions - SyncAllFromBundleFile(db: IDatabaseRepository, bundleFile: any): Promise + SyncAllFromBundleFile(db: IDatabaseRepository, bundleFile: any): Promise } //This is the "raw" Fhir resource diff --git a/frontend/src/lib/database/interface.ts b/frontend/src/lib/database/interface.ts index 68e07ed0..8f8dbc7c 100644 --- a/frontend/src/lib/database/interface.ts +++ b/frontend/src/lib/database/interface.ts @@ -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 + UpsertSource(source: Source): Promise GetSource(source_id: string): Promise DeleteSource(source_id: string): Promise GetSourceSummary(source_id: string): Promise @@ -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 - CreateResources(resources: ResourceFhir[]): Promise + UpsertResource(resource: ResourceFhir): Promise + UpsertResources(resources: ResourceFhir[]): Promise GetResource(resource_id: string): Promise GetResources(): Promise GetResourcesForSource(source_id: string, source_resource_type?: string): Promise diff --git a/frontend/src/lib/database/pouchdb_repository.spec.ts b/frontend/src/lib/database/pouchdb_repository.spec.ts index 2bf3895a..ccdadbf2 100644 --- a/frontend/src/lib/database/pouchdb_repository.spec.ts +++ b/frontend/src/lib/database/pouchdb_repository.spec.ts @@ -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', diff --git a/frontend/src/lib/database/pouchdb_repository.ts b/frontend/src/lib/database/pouchdb_repository.ts index 7d0ecc0a..06cafed9 100644 --- a/frontend/src/lib/database/pouchdb_repository.ts +++ b/frontend/src/lib/database/pouchdb_repository.ts @@ -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 { + public async UpsertSource(source: Source): Promise { return this.upsertDocument(source); } @@ -156,11 +161,11 @@ export class PouchdbRepository implements IDatabaseRepository { /////////////////////////////////////////////////////////////////////////////////////// // Resource - public async CreateResource(resource: ResourceFhir): Promise { + public async UpsertResource(resource: ResourceFhir): Promise { return this.upsertDocument(resource); } - public async CreateResources(resources: ResourceFhir[]): Promise { + public async UpsertResources(resources: ResourceFhir[]): Promise { 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 { + protected upsertDocument(newDoc: IDatabaseDocument) : Promise { // 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 { - return this.GetDB() - .then((db) => { - - return Promise.all(docs.map((doc) => { - doc.populateId(); - return this.upsertDocument(doc) - })) - - }) + protected upsertBulk(docs: IDatabaseDocument[]): Promise { + 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 { @@ -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 { - 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 { - // 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 /////////////////////////////////////////////////////////////////////////////////////// diff --git a/frontend/src/lib/models/fasten/upsert-summary.ts b/frontend/src/lib/models/fasten/upsert-summary.ts new file mode 100644 index 00000000..e37600fa --- /dev/null +++ b/frontend/src/lib/models/fasten/upsert-summary.ts @@ -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 +} diff --git a/frontend/tsconfig.spec.json b/frontend/tsconfig.spec.json index 9d0ff75e..c4b50b01 100644 --- a/frontend/tsconfig.spec.json +++ b/frontend/tsconfig.spec.json @@ -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": [